C# Tipps - Interoperabilität mit nicht verwaltetem Code

Veröffentlicht: 26. Okt 2005

Von Allen Jones

Auszug aus dem Buch "Microsoft Visual C#.NET - Programmier-Rezepte - Hunderte von Lösungen und Codebeispielen aus der Praxis".

Das nächste Mal, wenn Sie auf ein Problem in Ihrem C#-Code stoßen, das schwierig zu lösen scheint, werfen Sie doch einen Blick in dieses Buch. Hier finden Sie Lösungen und Best Practices zum Schreiben effizienten Codes in C#.

´Microsoft Visual C#.NET Programmierrezepte´ ist Ihre Quelle für hunderte von C#- und .NET Framework-Programmier-Szenarien und -Aufgaben, die mit Hilfe eines konsistenten Problem/Lösungs-Ansatzes erläutert und gelöst werden.

Der logische Aufbau des Buchs erlaubt Ihnen, schnell die Themen zu finden, die Sie interessieren und damit praktische Beispiele, Code-Schnipsel, Best Practices und undokumentierte Vorgehensweisen, die Ihnen helfen, Ihre Arbeit schnell und effizient zu erledigen.

Sämtliche Codebeispiele sind von der Microsoft Press-Website downloadbar.

Microsoft Press


Diesen Artikel können Sie dank freundlicher Unterstützung von Microsoft Press auf MSDN Online lesen. Microsoft Press ist ein Partner von MSDN Online.


Zur Partnerübersichtsseite von MSDN Online

Das Microsoft .NET Framework ist eine extrem anspruchsvolle Plattform, die eine neue Sprache (C#), eine verwaltete Laufzeit (die CLR), eine Plattform für Webanwendungen (Microsoft ASP.NET) und eine umfangreiche Klassenbibliothek zur Erstellung von Anwendungen miteinander kombiniert. Doch so umfassend das .NET Framework auch ist, stellt es nicht alle Funktionen zur Verfügung, die in einem nicht verwalteten Code verfügbar sind. Gegenwärtig enthält das .NET Framework nicht alle Funktionen, die die Win32-API anbietet. Viele Unternehmen verwenden deshalb proprietäre Lösungen, die mit auf COM basierenden Sprachen wie zum Beispiel Microsoft Visual Basic 6 und Microsoft Visual C++ 6 erstellt wurden.

Glücklicherweise verlangt Microsoft nicht von den Unternehmen, die Codebasis aufzugeben, die bereits vorhanden war, als der Wechsel zur .NET-Plattform erfolgte. Das Framework ist stattdessen mit Interoperabilitätsfunktionen ausgestattet, die es Ihnen ermöglichen, von Ihren .NET-Anwendungen aus alten Code zu benutzen und sogar so auf .NET- Assemblies zuzugreifen, als würde es sich dabei um COM-Komponenten handeln. Die Rezepte in diesem Kapitel behandeln die folgenden Themen:

Auf dieser Seite

 Eine Funktion in einer nicht verwalteten DLL aufrufen
 Das Handle eines Steuerelements, Fensters oder einer Datei abrufen
 Eine nicht verwaltete Funktion aufrufen, die eine Struktur verwendet
 Eine nicht verwaltete Funktion aufrufen, die einen Rückruf verwendet
 Nicht verwaltete Fehlerinformationen abrufen
 Eine COM-Komponente in einem .NET-Client verwenden
 Eine COM-Komponente schnell freigeben
 Optionale Parameter verwenden
 Ein ActiveX-Steuerelement in einem .NET-Client verwenden
 Eine .NET-Komponente über COM zur Verfügung stellen

Eine Funktion in einer nicht verwalteten DLL aufrufen

Aufgabe
Sie möchten eine C-Funktion in einer DLL aufrufen. Diese Funktion kann ein Bestandteil der Win32-API oder Ihres eigenen alten Codes sein.

Lösung
Deklarieren Sie in Ihrem C#-Code eine Methode, mit der Sie auf die nicht verwaltete Funktion zugreifen. Deklarieren Sie diese Methode als extern und static, und verwenden Sie das Attribut System.Runtime.InteropServices.DllImportAttribute, um die DLL-Datei und den Namen der nicht verwalteten Funktionen festzulegen.

Beschreibung

Um eine C-Funktion aus einer externen Bibliothek zu verwenden, müssen Sie diese lediglich entsprechend deklarieren. Die CLR kümmert sich dann um den Rest. Sie lädt die DLL in den Speicher, wenn die Funktion aufgerufen wird, und führt das Marshaling der .NET-Datentypen in die entsprechenden C-Datentypen durch. Der .NET-Dienst, der diese plattformübergreifende Ausführung ermöglicht, trägt den Namen PInvoke (Platform Invoke, Plattformaufruf). Er führt die genannten Schritte in der Regel nahtlos aus. Bisweilen ist jedoch etwas mehr Arbeit erforderlich, z. B. wenn Sie speicherinterne Strukturen, Rückrufe oder variable Zeichenfolgen unterstützen müssen.

PInvoke wird häufig verwendet, um auf die Funktionalität der Win32-API zuzugreifen. Dies gilt besonders dann, wenn die API Funktionen enthält, die nicht in den verwalteten Klassen des .NET Frameworks zu finden sind. Dieses Buch enthält an vielen Stellen Beispiele für diese Art des Einsatzes von PInvoke. Es gibt drei Bibliotheken, die den Kern der Win32-API bilden:

  • Kernel32.dll enthält spezifische Betriebssystemfunktionenen, wie zum Beispiel das Laden von Prozessen, den Kontextwechsel und Datei- und Speicher-E/A-Funktionen.

  • User32.dll enthält die Funktionalität für die Arbeit mit Fenstern, Menüs, Dialogfeldern, Symbolen usw.

  • GDI32.dll stellt grafische Funktionen zur Verfügung, um direkt in Fenster, Menüs und auf die Oberflächen von Steuerelementen zu zeichnen. Die Bibliothek enthält auch Druckfunktionen.

Betrachten Sie beispielsweise die Win32-API-Funktionen zum Schreiben und Lesen von INI-Dateien, wie zum Beispiel GetPrivateProfileString und WritePrivateProfileString in Kernel32.dll. Das .NET Framework enthält keine Klassen, die diese Funktionalität ummanteln. Sie können diese Funktionen jedoch wie folgt mit dem Attribut DllImportAttribute importieren:

[DllImport("kernel32.DLL", EntryPoint="WritePrivateProfileString")]
private static extern bool WritePrivateProfileString(string lpAppName,
  string lpKeyName, string lpString, string lpFileName);

Die Argumente, die in der Signatur der Methode WritePrivateProfileString angegeben sind, müssen mit denen der DLL-Methode übereinstimmen. Andernfalls tritt ein Laufzeitfehler auf, wenn Sie versuchen, die Methode aufzurufen. Denken Sie daran, keinen Methodenkörper zu definieren, da die Deklaration eine Methode in der DLL referenziert. In diesem Beispiel ist der EntryPoint-Abschnitt des Attributs DllImportAttribute optional. Sie müssen EntryPoint nicht angeben, wenn der deklarierte Funktionsname mit dem Funktionsnamen in der externen Bibliothek übereinstimmt.

Das folgende Beispiel zeigt die benutzerdefinierte Klasse IniFileWrapper, die diese Methoden privat deklariert und anschließend öffentliche Methoden hinzufügt, die die deklarierten Methoden basierend auf der aktuell ausgewählten Datei aufrufen:

using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public class IniFileWrapper {

    private string filename;

    public string Filename {
        get {return filename;}
    }

    public IniFileWrapper(string filename) {
        this.filename = filename;
    }

    [DllImport("kernel32.dll", EntryPoint="GetPrivateProfileString")]
    private static extern int GetPrivateProfileString(string lpAppName,
      string lpKeyName, string lpDefault, StringBuilder lpReturnedString,
      int nSize, string lpFileName);

    [DllImport("kernel32.dll", EntryPoint="WritePrivateProfileString")]
    private static extern bool WritePrivateProfileString(string lpAppName, 
      string lpKeyName, string lpString, string lpFileName);

    public string GetIniValue(string section, string key) {

        StringBuilder buffer = new StringBuilder();
        string sDefault = "";
        if (GetPrivateProfileString(section, key, sDefault,
          buffer, buffer.Capacity, filename) != 0) {

            return buffer.ToString();
        } else {
            return null;
        }
    }

    public bool WriteIniValue(string section, string key, string value) {

        return WritePrivateProfileString(section, key, value, filename);
    }
}

Es gibt noch andere Win32-API-Funktionen zum Abrufen von INI-Dateiinformationen. Dazu zählen auch Methoden, die bestimmte Abschnitte einer INI-Datei auslesen. Das Beispiel verwendet diese Methoden jedoch nicht.

TIPP: die Methode GetPrivateProfileString wird mit einem StringBuilder-Parameter (lpReturnedString) deklariert. Der Grund hierfür besteht darin, dass diese Zeichenfolge variabel sein muss - wenn der Aufruf beendet ist, enthält die Zeichenfolge die zurückgegebene INI-Dateiinformation. Wenn Sie eine veränderliche Zeichenfolge benötigen, müssen Sie anstelle der Klasse String ein StringBuilder-Objekt verwenden. Bisweilen müssen Sie das StringBuilder-Objekt mit einem Zeichenpuffer erstellen, der eine feste Größe aufweist, und diese Puffergröße in Form eines weiteren Parameters an die Funktion übergeben. Sie können die gewünschte Zeichenzahl im Konstruktor von StringBuilder angeben.

Sie können dieses Programm einfach testen. Erstellen Sie zunächst die nachfolgend aufgeführte INI-Datei.

[SampleSection]
Key1=Value1
Key2=Value2
Key3=Value3

Jetzt lassen Sie den folgenden Code ausführen. Dabei handelt es sich um eine Konsolenanwendung, die INI-Werte liest und schreibt.

public class IniTest {

    private static void Main() {

        IniFileWrapper ini = new IniFileWrapper(
          Application.StartupPath + "\\initest.ini");

        string val = ini.GetIniValue("SampleSection", "Key1");
        Console.WriteLine("Wert von Key1 in [SampleSection] ist: " + val);

        ini.WriteIniValue("SampleSection", "Key1", "New Value");
        val = ini.GetIniValue("SampleSection", "Key1");
        Console.WriteLine("Wert von Key1 in [SampleSection] ist jetzt: " + val);

        ini.WriteIniValue("SampleSection", "Key1", "Value1");
        Console.ReadLine();
    }
}

 

Das Handle eines Steuerelements, Fensters oder einer Datei abrufen

Aufgabe
Sie möchten eine nicht verwaltete Funktion aufrufen, die das Handle eines Steuerelements, Fensters oder einer Datei verlangt.

Lösung
Viele Klassen, wie zum Beispiel alle von Control abgeleiteten Klassen und die Klasse FileStream, geben ein Handle über eine Eigenschaft namens Handle und in Form einer IntPtr-Struktur zurück. Andere Klassen bieten ähnliche Informationen an. Die Klasse System.Diagnostics.Process stellt beispielsweise zusätzlich zur Eigenschaft Handle die Eigenschaft Process.MainWindowsHandle zur Verfügung.

Beschreibung

Das .NET Framework verbirgt nicht die zu Grunde liegenden Details, wie zum Beispiel die Betriebssystem-Handles, die für Steuerelemente und Fenster verwendet werden. Obwohl Sie diese Informationen in der Regel nicht benötigen, können Sie sie abrufen, wenn Sie nicht verwaltete Funktionen aufrufen müssen, die diese Daten verlangen. Viele Microsoft Windows-API-Funktionen erwarten beispielsweise ein Steuerelement- oder Fenster-Handle.

Betrachten Sie beispielsweise die in Abbildung 1 dargestellte Windows-Anwendung. Sie besteht aus einem einzelnen Fenster, das unabhängig vom Fokus immer über allen anderen Fenstern angezeigt wird. (Sie erzielen dieses Verhalten, indem Sie die Eigenschaft Form.TopMost auf true setzen.) Das Formular enthält außerdem einen Timer, der die WinAPI-Funktionen GetForegroundWindow und GetWindowText regelmäßig aufruft, um zu ermitteln, welches Fenster gegenwärtig fokussiert ist.

Informationen über das aktive Fenster abrufen
Abbildung 1: Informationen über das aktive Fenster abrufen

Das Beispiel weist noch eine Besonderheit auf. Der Code verwendet auch die Eigenschaft Form.Handle, um das Handle des Hauptanwendungsformulars abzurufen. Er vergleicht dieses Handle anschließend mit dem Handle des aktiven Formulars, um zu überprüfen, ob die aktuelle Anwendung fokussiert ist. Der vollständige Formularcode ist nachfolgend aufgeführt:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Text;
 
public class ActiveWindowInfo : System.Windows.Forms.Form {

    // (Der Designer-Code ist hier nicht aufgeführt.)

    private System.Windows.Forms.Timer tmrRefresh;
    private System.Windows.Forms.Label lblCurrent;
    private System.Windows.Forms.Label lblHandle;
    private System.Windows.Forms.Label lblCaption;

    [DllImport("user32.dll")]
    private static extern int GetForegroundWindow();

    [DllImport("user32.dll")]
    private static extern int GetWindowText(int hWnd, StringBuilder text, 
      int count);

    private void tmrRefresh_Tick(object sender, System.EventArgs e) {

        int chars = 256;
        StringBuilder buff = new StringBuilder(chars);
        int handle = GetForegroundWindow();

        if (GetWindowText(handle, buff, chars) > 0) {

            lblCaption.Text = buff.ToString();
            lblHandle.Text = handle.ToString();
            if (new IntPtr(handle) == this.Handle) {
                lblCurrent.Text = "True";
            } else {
                lblCurrent.Text = "False";
            }
        }
    }
}

TIPP: Die Infrastruktur von Windows Forms verwaltet die Handles. Dies geschieht vom Entwickler und Benutzer unbemerkt. Das Ändern einiger Formulareigenschaften kann die CLR dazu veranlassen, ein neues Handle zu generieren. Aus diesem Grund sollten Sie das Handle immer abrufen, bevor Sie es benutzen (anstatt es für lange Zeit in einer Membervariablen zu speichern).

 

Eine nicht verwaltete Funktion aufrufen, die eine Struktur verwendet

Aufgabe
Sie möchten eine nicht verwaltete Funktion aufrufen, die eine Struktur als Parameter erwartet.

Lösung
Definieren Sie die Struktur in Ihrem C#-Code. Benutzen Sie das Attribut System.Runtime.InteropServices.StructLayoutAttribute, um festzulegen, wie die Struktur im Speicher angelegt werden soll. Benutzen Sie die statische Methode SizeOf der Klasse System.Runtime.Interop.Marshal, wenn Sie die Größe der nicht verwalteten Struktur in Bytes angeben möchten.

Beschreibung

Wenn Sie mit C#-Code arbeiten, können Sie Speicherreservierungen nicht direkt steuern. Stattdessen verschiebt die CLR die Daten im Speicher nach Belieben, um die Performance zu optimieren. Dies kann zu Problemen führen, wenn Sie mit alten C-Funktionen interagieren, die erwarten, ein sequenzielles Layout der Strukturen im Speicher vorzufinden. Mit .NET können Sie dieses Problem glücklicherweise lösen, indem Sie das Attribut StructLayoutAttribute verwenden, mit dem Sie festlegen, wie die Member einer Klasse oder Struktur im Speicher abgelegt werden sollen.

Betrachten Sie beispielsweise die nicht verwaltete Funktion GetVersionEx in der Datei Kernel32.dll. Diese Funktion erwartet einen Zeiger auf eine OSVERSIONINFO-Struktur und verwendet ihn, um Informationen über die aktuelle Betriebssystemversion zurückzugeben. Um die OSVERSIONINFO-Struktur in einem C#-Code zu verwenden, müssen Sie sie wie folgt mit dem Attribut StructLayoutAttribute definieren:

[StructLayout(LayoutKind.Sequential)]
public class OSVERSIONINFO {

    public int dwOSVERSIONINFOSize;
    public int dwMajorVersion;
    public int dwMinorVersion;
    public int dwBuildNumber;
    public int dwPlatformId;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
    public String szCSDVersion;
}

Beachten Sie, dass diese Struktur ebenfalls das Attribut System.Runtime.InteropServices.MarshalAsAttribute verwendet, das für Zeichenfolgen fester Länge erforderlich ist. In diesem Beispiel legt MarshalAsAttribute fest, dass die Zeichenfolge als Wert übergeben und einen Puffer mit einer Länge von 128 Zeichen enthalten wird, wie in der Struktur OSVERSIONINFO angegeben. Es wird das sequenzielle Layout verwendet, was bedeutet, dass die Datentypen in der Struktur in der Reihenfolge angeordnet werden, in der sie in der Klasse oder Struktur aufgeführt sind. Wenn Sie das sequenzielle Layout verwenden, können Sie ebenfalls das Packing für die Struktur konfigurieren, indem Sie im Konstruktor von StructLayoutAttribute ein benanntes Pack-Feld angeben. Der Vorgabewert ist 8, sodass die Struktur in 8 Byte großen Paketen aufgebaut wird.

Sie können anstelle des sequenziellen Layouts auch LayoutKind.Explicit verwenden. In diesem Fall müssen Sie das Byte-Offset jedes Feldes mit FieldOffsetAttribute definieren. Dieses Layout ist nützlich, wenn Sie mit einer unregelmäßig komprimierten Struktur oder mit einer Struktur arbeiten, in der Sie auf einige nicht verwendete Felder verzichten möchten. Das folgende Beispiel definiert die OSVERSIONINFO-Klasse mit einem expliziten Layout:

[StructLayout(LayoutKind.Explicit)]
public class OSVERSIONINFO {

    [FieldOffset(0)] public int dwOSVERSIONINFOSize;
    [FieldOffset(4)]public int dwMajorVersion;
    [FieldOffset(8)]public int dwMinorVersion;
    [FieldOffset(12)]public int dwBuildNumber;
    [FieldOffset(16)]public int dwPlatformId;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]    
    [FieldOffset(20)]public String szCSDVersion;
}

Nachdem Sie nun die von der GetVersionEx-Funktion verwendete Struktur definiert haben, können Sie die Funktion selbst deklarieren und benutzen. Die folgende Konsolenanwendung zeigt den Code, den Sie dazu benötigen. Beachten Sie, dass die Attribute InAttribute und OutAttribute auf den Parameter OSVERSIONINFO angewendet werden, um anzugeben, dass das Marshaling für diese Struktur durchgeführt werden soll, wenn sie an die Funktion übergeben und von dieser zurückgegeben wird. Der Code setzt außerdem die Methode Marshal.SizeOf ein, um die Größe zu berechnen, die die gemarshallte Struktur im Speicher belegt.

using System;
using System.Runtime.InteropServices;

public class CallWithStructure
{

    // (OSVersionInfo-Klasse ist hier nicht afugeführt.)

    [DllImport("kernel32.dll")]
    public static extern bool GetVersionEx([In, Out] OSVersionInfo osvi);
    
    private static void Main()
    {
        OSVersionInfo osvi = new OSVersionInfo();
        osvi.dwOSVersionInfoSize = Marshal.SizeOf(osvi);

        GetVersionEx(osvi);
        
        Console.WriteLine("Klassengröße: " + osvi.dwOSVersionInfoSize);
        Console.WriteLine("Hauptversion: " + osvi.dwMajorVersion);
        Console.WriteLine("Nebenversion: " + osvi.dwMinorVersion);
        Console.WriteLine("Build-Zahl: " + osvi.dwBuildNumber);
        Console.WriteLine("Plattform-ID: " + osvi.dwPlatformId);
        Console.WriteLine("CSD-Version: " + osvi.szCSDVersion);
        Console.WriteLine("Plattform: " + Environment.OSVersion.Platform);
        Console.WriteLine( "Version: " + Environment.OSVersion.Version);
        Console.ReadLine();
    }
}

Wenn Sie die Anwendung auf einem Windows XP-System ausführen, erhalten Sie eine ähnliche Ausgabe wie die folgende:

Klassengröße: 148
Hauptversion: 5
Nebenversion: 1
Build-Zahl: 2600
Plattform-ID: 2
CSD-Version: Service Pack 1
Plattform: Win32NT
Version: 5.1.2600.0

 

Eine nicht verwaltete Funktion aufrufen, die einen Rückruf verwendet

Aufgabe
Sie möchten eine nicht verwaltete Funktion aufrufen, die eine Methode in Ihrem Code aufrufen soll.

Lösung
Erstellen Sie einen Delegaten mit der für den Rückruf erforderlichen Signatur. Benutzen Sie diesen Delegaten, wenn Sie die nicht verwalteten Funktionen definieren und verwenden.

Beschreibung

Viele Win32-API-Funktionen benutzen Rückrufe. Wenn Sie beispielsweise die Namen aller gegenwärtig geöffneten Fenster abrufen möchten, können Sie die nicht verwaltete Funktion EnumWindows aufrufen, die sich in der Datei User32.dll befindet. Wenn Sie diese Funktion aufrufen, müssen Sie einen Zeiger zur Verfügung stellen, der auf eine Funktion in Ihrem Code verweist. Das Windows-Betriebssystem ruft dann diese Funktion wiederholt auf. Dieser Aufruf erfolgt für jedes gefundene Fenster. Dabei wird das jeweilige Fenster-Handle an Ihren Code übergeben.

Das .NET Framework ermöglicht es Ihnen, solche Rückrufszenarien zu verwenden, ohne auf Zeiger und unsichere Codeblöcke zurückgreifen zu müssen. Sie können stattdessen einen Delegaten definieren, der auf Ihre Rückruffunktion verweist. Wenn Sie beispielsweise den Delegaten der EnumWindows-Funktion übergeben, marshallt die CLR ihn automatisch zu dem benötigten nicht verwalteten Funktionszeiger.

Nachfolgend ist eine Konsolenanwendung aufgeführt, die EnumWindows mit einem Rückruf verwendet, um die Namen aller geöffneten Fenster anzuzeigen.

using System;
using System.Text;
using System.Runtime.InteropServices;

public class GetWindows
{
    // Die Signatur für die Rückrufmethode.
    public delegate bool CallBack(int hwnd, int lParam);

    // Die nicht verwaltete Funktion, die den Rückruf auslösen wird,
    // wenn sie die geöffneten Fenster auflistet.
    [DllImport("user32.dll")]
    public static extern int EnumWindows(CallBack callback, int param); 

    [DllImport("user32.dll")]
    public static extern int GetWindowText(int hWnd, StringBuilder lpString, int nMaxCount);

    private static void Main()
    {
        CallBack callBack = new CallBack(DisplayWindowInfo);

        // Anfordern, dass das Betriebssystem alle Fenster auflistet und Ihren
        // Rückruf mit dem Handle jedes Fensters auslöst.
        EnumWindows(callBack, 0);

        Console.ReadLine();
    }

    // Die Methode, die den Rückruf empfängt. Der zweite Parameter wird nicht
    // verwendet, ist aber erforderlich, um der Signatur des Rückrufs gerecht zu werden.
    public static bool DisplayWindowInfo(int hWnd, int lParam) 
    { 
        int chars = 100;
        StringBuilder buf = new StringBuilder(chars);
        if (GetWindowText(hWnd, buf, chars) != 0)
        {
            Console.WriteLine(buf);
        }
        return true;
    }
}

 

Nicht verwaltete Fehlerinformationen abrufen

Aufgabe
Sie möchten Fehlerinformationen abrufen (entweder einen Fehlercode oder eine Fehlermeldung), die Aufschluss darüber geben, weshalb ein Win32-API-Aufruf fehlgeschlagen ist.

Lösung
Setzen Sie in der Deklaration der nicht verwalteten Methode das Feld SetLastError des Attributs DllImportAttribute auf true. Wenn ein Fehler auftritt, während Sie die Methode ausführen, rufen Sie die statische Methode Marshal.GetLastWin32Error auf, um den Fehlercode zu ermitteln. Um eine Textbeschreibung zu einem bestimmten Fehlercode zu erhalten, benutzen Sie die nicht verwaltete Funktion FormatMessage.

Beschreibung

Sie können Fehlerinformationen nicht direkt abrufen, wenn Sie die nicht verwaltete Funktion GetLastError verwenden. Der Grund hierfür besteht darin, dass der von GetLastError zurückgegebene Fehlercode nicht unbedingt Aufschluss über den Fehler gibt, der von der von Ihnen verwendeten nicht verwalteten Funktion verursacht wurde. Er kann stattdessen von anderen .NET Framework-Klassen oder der CLR gesetzt werden. Sie können jedoch die Fehlerinformation sicher abrufen, indem Sie die statische Methode Marshal.GetLastWin32Error verwenden. Diese Methode sollte direkt nach dem nicht verwalteten Aufruf aufgerufen werden. Sie gibt die Fehlerinformation nur einmal zurück. (Alle weiteren Aufrufe von GetLastWin32Error würden einfach den Fehlercode 127 zurückgeben.) Sie müssen außerdem das SetLastError-Feld des DllImportAttribute-Attributs auf true setzen, um festzulegen, dass die von dieser Funktion generierten Fehler abgefangen werden sollen.

[DllImport("user32.dll", SetLastError=true)]

Sie können zusätzliche Informationen aus dem Win32-Fehlercode extrahieren, wenn Sie die nicht verwaltete Funktion FormatMessage verwenden, die sich in der Datei Kernel32.dll befindet.

Die folgende Konsolenanwendung versucht, ein Meldungsfeld anzuzeigen, stellt aber ein ungültiges Fenster-Handle zur Verfügung. Die Fehlerinformation wird mit Marshal.GetLastWin32Error abgerufen, und die dazugehörige Textinformation wird mit FormatMessage ermittelt.

using System;
using System.Runtime.InteropServices;

public class TestError
{
    [DllImport("kernel32.dll")]
    private unsafe static extern int FormatMessage(int dwFlags, int lpSource, 
        int dwMessageId, int dwLanguageId, ref String lpBuffer, int nSize, int Arguments);

    [DllImport("user32.dll", SetLastError=true)]
    public static extern int MessageBox(int hWnd, 
        string pText ,
        string pCaption ,
        int uType);

    private static void Main()
    {
        int badWindowHandle = 453;
        MessageBox(badWindowHandle, "Message", "Caption", 0);
        
        int errorCode = Marshal.GetLastWin32Error();
        Console.WriteLine(errorCode);
        Console.WriteLine(GetErrorMessage(errorCode));

        Console.ReadLine();
    }

    // GetErrorMessage formatiert eine Fehlermeldung gemäß des Eingabewertes
    // errorCode und gibt diese Meldung zurück.
    public static string GetErrorMessage(int errorCode)
    {
        int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
        int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
        int FORMAT_MESSAGE_FROM_SYSTEM  = 0x00001000;

        int messageSize = 255;
        string lpMsgBuf = "";
        int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
    
        int retVal = FormatMessage(dwFlags, 0, errorCode, 0, ref lpMsgBuf, messageSize, 0);
        if (0 == retVal) 
        {
            return null;
        }
        else
        {
            return lpMsgBuf;
        }
    }
}

Nachfolgend ist die Ausgabe des Programms aufgeführt:

1400
Ungültiges Fensterhandle

 

Eine COM-Komponente in einem .NET-Client verwenden

Aufgabe
Sie möchten eine COM-Komponente in einem .NET-Client verwenden.

Lösung
Benutzen Sie eine primäre Interop-Assembly, sofern verfügbar. Generieren Sie andernfalls einen RCW (Runtime Callable Wrapper; einen Wrapper, der von der Laufzeit aufgerufen werden kann). Verwenden Sie dazu das Programm Tlbimp.exe zum Importieren von Typbibliotheken oder in Visual Studio .NET das Dialogfeld Verweis hinzufügen.

Beschreibung

Das .NET Framework unterstützt die Interoperabilität mit COM. Damit .NET-Clients mit COM-Komponenten interagieren können, verwendet .NET einen RCW - eine spezielle .NET-Proxyklasse, die als Vermittler zwischen Ihrem .NET-Code und der COM-Komponente agiert. Der RCW ist für alle Details verantwortlich. Dazu zählen auch das Marshaling der Datentypen, der Einsatz traditioneller COM-Schnittstellen und die Behandlung von COM-Ereignissen.

Ihnen stehen die drei folgenden Möglichkeiten zur Verfügung, um einen RCW zu verwenden:

  • Fordern Sie einen RCW vom Autor der originalen COM-Komponente an. In diesem Fall wird der RCW als PIA (Primary Interop Assembly) bezeichnet.

  • Generieren Sie einen RCW mithilfe von Visual Studio .NET oder unter Verwendung des Befehlszeilenhilfsmittels Tlbimp.exe.

  • Generieren Sie einen eigenen RCW, indem Sie die Typen im Namespace System.Runtime.InteropServices verwenden. (Dies kann sehr mühsam und kompliziert sein.)

Wenn Sie einen RCW mit Visual Studio .NET erstellen möchten, wählen Sie einfach im Menü Projekt den Befehl Verweis hinzufügen. Öffnen Sie die Registerkarte COM, und wählen Sie die gewünschte Komponente aus. Wenn Sie auf OK klicken, werden Sie dazu aufgefordert, Ihre Absicht zu bestätigen, sodass der RCW generiert werden kann. Die Interop-Assembly wird anschließend erzeugt und Ihren Projektverweisen hinzugefügt. Danach können Sie den Objektbrowser einsetzen, um die verfügbaren Namespaces und Klassen zu untersuchen.

Wenn Sie nicht mit Visual Studio .NET arbeiten, können Sie eine Wrapper-Assembly mit dem Befehlszeilenhilfsmittel Tlbimp.exe erstellen, das ein Bestandteil des .NET Frameworks ist. Die einzig erforderliche Information, die dieses Programm erwartet, ist der Name der Datei, die die COM-Komponente enthält. Die folgende Anweisung erstellt beispielsweise einen RCW mit dem Standarddateinamen und -Namespace, vorausgesetzt, die Datei MyCOMComponent.dll befindet sich im aktuellen Verzeichnis.

tlbimp MyCOMComponent.dll

Würde MyCOMComponent mit einer Typbibliothek namens MyClasses arbeiten, hätte die generierte RCW-Datei den Namen MyClasses.dll. In diesem Fall würde der RCW seine Klassen über den Namespace MyClasses zur Verfügung stellen. Sie können diese Optionen wie in der MSDN-Referenz beschrieben auch mit Befehlszeilenparametern konfigurieren. Sie können beispielsweise /out:[Dateiname] verwenden, um einen anderen Assembly-Dateinamen anzugeben, und /namespace:[Namespacename], um einen anderen Namespace für die erzeugten Klassen festzulegen. Sie verfügen außerdem über die Möglichkeit, mit /keyfile:[Schlüsseldateiname] eine Schlüsseldatei zu bestimmen, sodass die Komponente signiert wird und einen starken Namen erhält und im GAC (Global Assembly Cache) abgelegt werden kann. Benutzen Sie den Parameter /primary, um eine PIA zu erstellen.

Wenn möglich, sollten Sie immer eine PIA anstelle eines selbst generierten RCW verwenden. Wenn Sie sich für primäre Interop-Assemblys (PIA) entscheiden, ist die Chance größer, dass diese wie erwartet funktionieren. Der Grund hierfür besteht darin, dass sie vom Publisher der ursprünglichen Komponente erstellt werden. Sie können außerdem zusätzliche .NET-Feinheiten und -Erweiterungen enthalten. Wenn auf Ihrem System eine PIA für eine COM-Komponente registriert ist, verwendet Visual Studio .NET automatisch diese PIA, sobald Sie einen Verweis auf die Komponente hinzufügen. Das .NET Framework enthält beispielsweise die Assembly adodb.dll, die es Ihnen ermöglicht, die klassischen ADO-COM-Objekte zu verwenden. Wenn Sie einen Verweis auf die Komponente Microsoft ActiveX Data Objects hinzufügen, wird diese Interop-Assembly automatisch verwendet. Kein neuer RCW wird generiert. Ein weiteres Beispiel ist eine PIA von Microsoft Office XP, die die .NET-Unterstützung der Office-Automatisierung verbessert. Sie müssen diese Assembly jedoch von der MSDN-Website herunterladen (https://msdn.microsoft.com/downloads/list/office.asp).

Das folgende Beispiel zeigt, wie Sie COM-Interop verwenden können, um von einer .NET Framework-Anwendung aus auf die klassischen ADO-Objekte zuzugreifen:

using System;

public class ADOClassic {

    private static void Main() {

        ADODB.Connection con = new ADODB.Connection();
        string connectionString = "Provider=SQLOLEDB.1;" +
          "Data Source=localhost;" +
          "Initial Catalog=Northwind;Integrated Security=SSPI";
        con.Open(connectionString, null, null, 0);

        object recordsAffected;
        ADODB.Recordset rs = con.Execute("SELECT * From Customers",
          out recordsAffected, 0);

        while (rs.EOF != true) {

            Console.WriteLine(rs.Fields["CustomerID"].Value);
            rs.MoveNext();
        }
        Console.ReadLine();
    }
}

 

Eine COM-Komponente schnell freigeben

Aufgabe
Sie möchten sicherstellen, dass eine COM-Komponente sofort aus dem Speicher entfernt wird und nicht auf die Garbage Collection wartet. Oder Sie möchten sicherstellen, dass die COM-Objekte in einer bestimmten Reihenfolge freigegeben werden.

Lösung
Geben Sie den Verweis auf das zu Grunde liegende COM-Objekt frei, indem Sie die statische Methode Marshal.ReleaseComObject verwenden und ihr den entsprechenden RCW übergeben.

Beschreibung

COM verwendet die Verweiszählung, um zu ermitteln, wann Objekte freigegeben werden müssen. Wenn Sie einen RCW einsetzen, wird der Verweis auf das zu Grunde liegende COM-Objekt auch dann beibehalten, wenn die Garbage Collection das RCW-Objekt aus dem Speicher entfernt. Demgemäß können Sie nicht kontrollieren, wann oder in welcher Reihenfolge COM-Objekte entsorgt werden.

Um diese Einschränkung zu umgehen, können Sie die Methode Marshal.ReleaseComObject verwenden. Im ADO-Beispiel aus " Eine COM-Komponente in einem .NET-Client verwenden " könnten Sie beispielsweise die zu Grunde liegenden ADO-COM-Objekte Recordset und Connection freigeben, indem Sie am Ende des Codes die folgenden beiden Zeilen hinzufügten:

Marshal.ReleaseComObject(rs);
Marshal.ReleaseComObject(con);

HINWEIS: **** Technisch betrachtet, gibt die Methode ReleaseComObject das COM-Objekt
eigentlich nicht frei. Sie verringert lediglich den Verweiszähler. Wenn der Verweiszähler 0 erreicht, wird das COM-Objekt freigegeben. Wenn Sie mit verschiedenen Codeabschnitten arbeiten, die alle dieselbe Instanz eines COM-Objekts verwenden, muss jeder Abschnitt das Objekt freigeben, bevor es aus dem Speicher entfernt wird.

 

Optionale Parameter verwenden

Aufgabe
Sie möchten eine Methode einer COM-Komponente aufrufen, ohne alle erforderlichen Parameter angeben zu müssen.

Lösung
Benutzen Sie das Feld Type.Missing.

Beschreibung

Das .NET Framework macht umfassenden Gebrauch von der Methodenüberladung. Die meisten Methoden sind mehrmals überladen, sodass Sie die Version aufrufen können, die nur die von Ihnen benötigten Parameter verlangt. COM hingegen unterstützt nicht die Methodenüberladung. COM-Komponenten verwenden stattdessen normalerweise Methoden mit vielen optionalen Parametern. C# lässt leider keine optionalen Parameter zu, sodass C#-Entwickler häufig viele zusätzliche oder irrelevante Werte bereitstellen müssen, um auf eine COM-Komponente zugreifen zu können. Und da für COM-Parameter oft die Verweisübergabe verwendet wird, kann Ihr Code nicht einfach eine null-Referenz übergeben. Er muss stattdessen eine Objektvariable deklarieren und diese übergeben.

Sie können dieses Problem bis zu einem gewissen Grad umgehen, indem Sie immer dann, wenn Sie einen optionalen Parameter nicht angeben möchten, das Feld Type.Missing bereitstellen. Wenn Sie einen Parameter als Verweis übergeben müssen, können Sie, wie nachfolgend gezeigt, einfach eine einzelne Objektvariable deklarieren, auf Type.Missing setzen und für alle optionalen Parameter verwenden:

private static object n = Type.Missing;

Das folgende Beispiel verwendet die Word-COM-Objekte, um ein Dokument programmgesteuert zu erstellen und anzuzeigen. Viele der vom Beispiel verwendeten Methoden benutzen optionale Parameter, die als Verweis übergeben werden. Sie werden feststellen, dass der Einsatz des Feldes Type.Missing diesen Code erheblich vereinfacht. Die entsprechenden Stellen sind hervorgehoben.

using System;

{
    public class OptionalParameters
    {
        private static object n = Type.Missing;

        private static void Main()
        {
            // Word im Hintergrund starten.
            Word.ApplicationClass app = new Word.ApplicationClass();
            app.DisplayAlerts = Word.WdAlertLevel.wdAlertsNone;
            
            // Ein neues Dokument (für den Benutzer nicht sichtbar) erstellen.
            Word.Document doc = app.Documents.Add(ref n, ref n, ref n, ref n);

            Console.WriteLine();
            Console.WriteLine("Erstelle neues Dokument.");
            Console.WriteLine();
            
            // Eine Überschrift und zwei Textzeilen hinzufügen.
            Word.Range range = doc.Paragraphs.Add(ref n).Range;
            range.InsertBefore("Testdokument");
            string style = "Überschrift 1";
            object objStyle = style;
            range.set_Style(ref objStyle);
            
            range = doc.Paragraphs.Add(ref n).Range;
            range.InsertBefore("Zeile 1.\nZeile 2.");
            range.Font.Bold = 1;

            // Word und eine Druckvorschau anzeigen.
            doc.PrintPreview();
            app.Visible = true;

 
            Console.ReadLine();
        }
    }
}

 

Ein ActiveX-Steuerelement in einem .NET-Client verwenden

Aufgabe
Sie möchten ein ActiveX-Steuerelement in einem Fenster einer .NET Framework-Anwendung anordnen.

Lösung
Benutzen Sie einen RCW genauso, wie für eine gewöhnliche COM-Komponente. Um zur Entwurfszeit mit dem ActiveX-Steuerelement zu arbeiten, fügen Sie es der Toolbox von Visual Studio .NET hinzu.

Beschreibung

Das .NET Framework bietet für alle COM-Komponenten dieselbe Unterstützung an. Dazu zählen auch ActiveX-Steuerelemente. Der Unterschied besteht darin, dass die RCW-Klasse eines ActiveX-Steuerelements von dem speziellen .NET-Typ System.Windows.Forms.AxHost abgeleitet wird. Sie fügen Ihrem Formular eigentlich das AxHost-Steuerelement hinzu, das dann im Hintergrund mit dem ActiveX-Steuerelement kommuniziert. Da ein AxHost-Objekt von System.Windows.Forms.Control abgeleitet ist, bietet es alle Standardeigenschaften, methoden und -ereignisse der .NET-Steuerelemente an, wie zum Beispiel Location, Size, Anchor usw. Bei einem automatisch generierten RCW beginnt der Name der AxHost-Klasse immer mit den Buchstaben Ax.

Sie können einen RCW für ein ActiveX-Steuerelement genauso wie für jede andere COM-Komponente auch erstellen, indem Sie Tlbimp.exe oder in Visual Studio .NET das Dialogfeld Verweis hinzufügen verwenden. Danach erstellen Sie das Steuerelement programmgesteuert. In Visual Studio .NET ist es jedoch einfacher, das ActiveX-Steuerelement der Toolbox hinzuzufügen.

Solange Sie der Toolbox ein ActiveX-Steuerelement lediglich hinzufügen, wirkt sich dies nicht auf Ihr Projekt aus. Sie können das entsprechende Toolbox-Symbol jedoch benutzen, um
Ihrem Formular eine Instanz des Steuerelements hinzuzufügen. Wenn Sie dies zum ersten Mal tun, generiert Visual Studio .NET die Interop-Assembly und fügt sie Ihrem Projekt hinzu. Wenn Sie beispielsweise das Microsoft Masked Edit-Steuerelement hinzufügen, zu dem es kein .NET-Äquivalent gibt, erstellt Visual Studio .NET eine RCW-Assembly mit einem Namen wie zum Beispiel AxInterop.MSMask.dll. Nachfolgend ist der Code der versteckten Designer-Region aufgeführt, die eine Steuerelementinstanz erstellt und zum Formular hinzufügt:

this.axMaskEdBox1 = new AxMSMask.AxMaskEdBox();
((System.ComponentModel.ISupportInitialize)(this.axMaskEdBox1)).BeginInit();

 
// 
// axMaskEdBox1
// 
this.axMaskEdBox1.Location = new System.Drawing.Point(16, 12);
this.axMaskEdBox1.Name = "axMaskEdBox1";
this.axMaskEdBox1.OcxState = ((System.Windows.Forms.AxHost.State)
  (resources.GetObject("axMaskEdBox1.OcxState")));

this.axMaskEdBox1.Size = new System.Drawing.Size(112, 20);
this.axMaskEdBox1.TabIndex = 0;

this.Controls.Add(this.axMaskEdBox1);

Beachten Sie, dass die spezifischen Eigenschaften des ActiveX-Steuerelements nicht direkt über set-Eigenschaftsanweisungen zugewiesen werden. Sie werden stattdessen als Gruppe wiederhergestellt, wenn das Steuerelement seine gespeicherte Eigenschaft OcxState setzt. Ihr Code verwendet die Eigenschaften des Steuerelements aber direkt.

 

Eine .NET-Komponente über COM zur Verfügung stellen

Aufgabe
Sie möchten eine .NET-Komponente erstellen, die von einem COM-Client aufgerufen werden kann.

Lösung
Erstellen Sie eine Assembly, die bestimmte in diesem Rezept definierte Beschränkungen einhält. Exportieren Sie eine Typbibliothek für diese Assembly. Verwenden Sie dazu das Befehlszeilenhilfsmittel Tlbexp.exe (Type Library Exporter).

Beschreibung

Das .NET Framework unterstützt COM-Clients dabei, .NET-Komponenten zu benutzen. Wenn ein COM-Client ein .NET-Objekt erstellt, generiert die CLR das verwaltete Objekt und einen CCW (COM Callable Wrapper; ein Wrapper, der von COM aufgerufen werden kann), der das Objekt ummantelt. Der COM-Client interagiert über den CCW mit dem verwalteten Objekt. Die Laufzeit erstellt nur ein CCW pro verwaltetem Objekt. Dies geschieht unabhängig davon, wie viele COM-Clients auf das Objekt zugreifen.

Typen, auf die der Zugriff durch COM-Clients erfolgen soll, müssen bestimmte Anforderungen erfüllen:

  • Der verwaltete Typ (Klasse, Schnittstelle, Struktur oder Enumeration) muss öffentlich (public) sein.

  • Wenn der COM-Client das Objekt erstellen soll, muss es einen öffentlichen Standardkonstruktor besitzen. COM unterstützt keine parametrisierten Konstruktoren.

  • Die Member des Typs, auf den der Zugriff erfolgen soll, müssen öffentliche (public) Instanzenmember sein. COM-Clients können nicht auf Member vom Typ private, protected, internal und static zugreifen.

  • Darüber hinaus sollten Sie die folgenden Empfehlungen berücksichtigen:

  • Sie sollten keine Vererbungsbeziehungen zwischen den Klassen erstellen, da diese Beziehungen für COM-Clients nicht sichtbar sind (obwohl .NET versucht, eine Klassenbeziehung zu simulieren, indem es eine gemeinsam genutzte Basisklassenschnittstelle deklariert).

  • Die Klassen, die Sie offen legen, sollten eine Schnittstelle implementieren. Wünschen Sie eine Versionsverwaltung, können Sie das Attribut System.Runtime.InteropServices.GuidAttribute verwenden, um den GUID festzulegen, der einer Schnittstelle zugewiesen werden soll.

  • Im Idealfall geben Sie der verwalteten Assembly einen starken Namen, sodass sie im GAC installiert und von mehreren Clients gemeinsam genutzt werden kann.

Damit ein COM-Client das .NET-Objekt erstellen kann, ist eine Typbibliothek (eine .tlb-Datei) erforderlich. Die Typbibliothek kann mit dem Befehlszeilenhilfsmittel Tlbexp.exe aus einer Assembly generiert werden. Nachfolgend ist ein Beispiel dafür aufgeführt, wie Sie die Syntax dieses Befehls verwenden:

tlbexp ManagedLibrary.dll

Nachdem Sie die Typbibliothek erstellt haben, können Sie sie vom nicht verwalteten Entwicklungswerkzeug aus referenzieren. Wenn Sie mit Visual Basic 6 arbeiten, referenzieren Sie die .tlb-Datei mithilfe des Dialogfeldes Verweise, das Sie über das Menü Projekt öffnen. In Visual C++ 6 können Sie die Anweisung #import verwenden, um die Typdefinitionen der Typbibliothek zu importieren.