Zeigen Sie Metadaten mit unserem Metadaten-Viewer an

Veröffentlicht: 07. Sep 2001 | Aktualisiert: 19. Jun 2004

Von Matt Pietrek

Die Entwicklung eines Metadaten-Viewers für .NET führt nicht nur zu einem interessanten kleinen Tool, sondern ist auch eine schöne Übung für den Umgang mit Windows Forms und dem .NET Framework.

Auf dieser Seite

Ein neues Tool für .NET Ein neues Tool für .NET
Überblick über die Metadaten Überblick über die Metadaten
Windows Forms - der Sprung ins kalte Wasser Windows Forms - der Sprung ins kalte Wasser
Das MetaViewer-Programm Das MetaViewer-Programm
Interessante Probleme gelöst Interessante Probleme gelöst

Ein neues Tool für .NET

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

sys12

Während ich diese Zeilen schreibe, ist Microsofts .NET-Initiative immer noch relativ neu (die Beta 1 ist gerade draußen) und die mutigen Entdecker finden immer noch neue Ecken und Winkel, in denen noch nie zuvor ein Mensch gewesen ist...

Was mich betrifft, halte ich viele Facetten der Metadaten für besonders interessant, ja geradezu für spannend. Die Metadaten stellen einen nahe liegenden Einstiegspunkt dar, von dem aus sich viele andere Bereiche vom .NET erschließen lassen. Metadaten sind die Informationen, mit denen im .NET die Klassen, Funktionen, Properties, Ressourcen und andere Dinge aus den ausführbaren Dateien beschrieben werden. In diesem Monat möchte ich Ihnen meinen MetaViewer vorstellen - ein kleines Beispielprogramm, das die Metadaten aus einer ausführbaren .NET-Datei anzeigt.

Der MetaViewer soll natürlich nicht die maßgebliche Autorität oder das umfassendste Programm zur Untersuchung von Metadaten werden. Wenn Ihnen etwas in dieser Richtung vorschwebt, sind Sie mit dem ILDASM von Microsoft besser bedient. Sie finden dieses Programm im .NET SDK. Statt dessen nutze ich den MetaViewer eigentlich nur als Gelegenheit, ein wenig nichttrivialen .NET-Code zu schreiben, der etwas Nützliches tut. Außerdem ist der MetaViewer relativ einfach und lässt sich bei Bedarf leicht so erweitern, dass er mehr von den Einzelheiten anzeigt, an denen Sie interessiert sind.

Warum das Rad neu erfinden und einen eigenen Metadaten-Anzeiger schreiben, wo doch Microsoft bereits einen viel besseren entwickelt hat? Nun, meine bisherigen Programmierversuche im .NET beschränken sich alle auf C++ mit den Managed Extensions und es war für mich einfach an der Zeit, C# zu lernen, und zwar mit einem nichttrivialen Programm. Zweitens handelte es sich bei allen meinen bisherigen .NET-Experimenten um Konsolenprogramme. Höchste Zeit, sich hinter die GUI-Programmierung mit Windows Forms zu klemmen. Langjährige Stammleser meiner Kolumne werden sich an meine unglaublich durchgestylten Schnittstellen zum Anwender erinnern (nun ja, über simple Dialogfenster bin ich ja kaum hinaus gekommen). Und ich freue mich, Ihnen berichten zu können, dass Windows Forms mir die Fortsetzung dieser Tradition des auserlesen schlechten Geschmacks erlaubt.

Der letzte Grund, der mich zur Entwicklung des MetaViewers bewegt hat, war meine allgemeine Unzufriedenheit mit den Metadatenanzeigern von Microsoft. Der ILDASM ist derzeit zwar nicht zu schlagen, was den Umfang der Informationen anbetrifft, aber seine Quellen sind nicht verfügbar. Außerdem zeigt der ILDASM die Informationen in einem Format an, das wohl nur eine liebende Mutter von ihrem Schützling akzeptieren würde. Ich fange wohl besser gar nicht erst an, mich über die Symbole aufzuregen, die der ILDASM benutzt und die praktisch nicht zu verstehen sind. Allerdings darf man wohl nicht vergessen, dass es sich immer noch um Beta-Software handelt.

Neben dem ILDASM bietet Microsoft mit dem ClsView einen zweiten Metadatenanzeiger an, der im Verzeichnis \FrameworkSDK\Samples\ClsView zu finden ist. Der Code vom ClsView macht zwar einen ordentlichen Eindruck, aber leider handelt es sich um eine ASP.NET-Anwendung. Solange Sie nicht auch die Internet Information Services (IIS) installieren und starten, ist der ClsView einfach keine Alternative. Nennen Sie mich einen Spielverderber, wenn Sie möchten, aber ich werde jetzt sämtliche Programme sofort zu Webanwendungen umbauen.

Überblick über die Metadaten

Wenn Sie sich mit der COM-Programmierung auskennen, sind Ihnen IDL und Typbibliotheken sicher nicht neu. Damit lassen sich Schnittstellen und deren Methoden beschreiben. Außerdem werden Informationen über alle Parameter der Methoden erfasst, darunter auch deren Typen. Diese Angaben sind für die Automation im COM und für andere Aufgaben erforderlich, zum Beispiel für die Weiterleitung der Argumente an die aufgerufenen COM-Komponenten, die in anderen Prozessen liegen.

Sie können sich .NET-Metadaten als Erweiterung von IDL und Typbibliotheken vorstellen. Metadaten sind viel umfangreicher und genauer als IDL. Wichtiger noch, die Metadaten sind vorgeschrieben. Man kann sie nicht einfach abschalten oder weglassen. Die gemeinsame Laufzeitschicht für alle Sprachen in .NET ist auf die Metadaten angewiesen. Von ihnen erfährt sie, mit welchen Baugruppen Ihr Code arbeitet, wie Ihre Methoden aussehen, wie Ihre Klassen im Speicher aufgebaut sind, welche Ressourcen in Ihrer ausführbaren Datei verfügbar sind und derlei Dinge mehr. Bevor ich nun meinen MetaViewer näher beschreibe, möchte ich Ihnen einen groben Überblick über die Metadaten geben, wie sie sich mit Hilfe der "Reflektionsklassen" vom .NET darstellen.
An der Spitze der Informationen, die in den Metadaten verfügbar sind, steht die Klasse System.Reflection.Assembly. Ein Assembly-Objekt entspricht einer oder mehreren DLLs, aus denen sich die betreffende .NET-Baugruppe zusammensetzt.

Die Assembly-Klasse enthält eine Menge Informationen, darunter eine Liste mit den Modulen (DLLs), aus denen die Baugruppe besteht, Versionsangaben zur Baugruppe sowie Angaben darüber, von welchen anderen Baugruppen die aktuelle Baugruppe abhängt und welche Ressourcen in der Baugruppe zu finden sind (Bitmaps und so weiter).

Unter der Assembly-Klasse folgt die Klasse System.Reflection.Module. Ein Modul repräsentiert eine einzelne DLL. Derzeit bestehen die meisten Baugruppen nur aus einem Modul. Trotzdem sollte man nicht dem Irrtum verfallen, es gäbe eine eins-zu-eins-Beziehung zwischen einer Baugruppe und einem Modul. Neben den Informationen über eine bestimmte DLL enthält die Module-Klasse auch Angaben über die Typen, die in diesem Modul zu finden sind. Für den Augenblick mag es ausreichen, wenn man sich einen Typ als eine .NET-Klasse vorstellt, die in der jeweils benutzten Sprache definiert wurde.
Die Klasse System.Type repräsentiert einen .NET-Typ. Jede Type-Instanz stellt eine von drei möglichen Definitionen dar: eine Klassendefinition, eine Schnittstellendefinition oder eine Wertklasse (normalerweise eine Struktur).

Steigt man die Metadatenhierarchie weiter hinab, so findet sich in jeder Type-Klasse eine Sammlung mit Datenelementen. Eine Instanz der Klasse System.Reflection.MemberInfo stellt jeweils eines dieser Datenelemente dar. Solch ein Datenelement beschreibt eines der Elemente, die in einer Klasse zu finden sind:

Methode(System.Reflection.MethodInfo) 
Konstruktor(System.Reflection.ConstructorInfo) 
Property(System.Reflection.PropertyInfo) 
Feld (Field)(System.Reflection.FieldInfo) 
Ereignis (Event)(System.Reflection.EventInfo)

Mit der MemberInfo-Klasse erhalten Sie Zugriff auf alle Elemente eines Typs. Allerdings ist MemberInfo nur die Basisklasse für eine der abgeleiteten Klassen aus der obigen Liste. Wenn eine bestimmte MemberInfo-Instanz zum Beispiel ein Feld darstellt, können Sie die Instanz also in ein FieldInfo konvertieren und haben dann Zugang zu den Informationen, die für Felder typisch sind.

Was die Klassen MethodInfo und ConstructorInfo anbetrifft, gibt es in den Metadaten noch eine weitere Ebene. Methoden und Konstruktoren können Parameter haben und diese Parameter werden durch die Klasse System.Reflection.ParameterInfo dargestellt. Sie können den Typ eines gegebenen Parameters ermitteln (als ein System.Type) und in den meisten Fällen auch seinen Namen.

Lassen Sie uns der Vollständigkeit halber einmal kurz zur Klasse System.Reflection.Module zurückgehen. Ein Module-Objekt enthält nicht nur die System.Type-Instanzen, sondern auch Angaben über die Methoden und Felder, die auf Modulebene deklariert werden, also außerhalb einer Klassendefinition. Diese Informationen fallen an, wenn man C++ mit den Managed Extensions einsetzt und globale Funktionen und Variablen definiert (also außerhalb einer Klassendefinition).

Bild B1 zeigt die Metadatenhierarchie, die ich gerade beschrieben habe, wie sie sich im .NET in Form der "Reflektionsklassen" darstellt. Wenn Sie mehr darüber wissen möchten, so möchte ich Sie auf meinen Artikel "Metadaten in der .NET-Umgebung" aus Heft 1/2001 verweisen.

09Meta011

B1 Die Metadatenhierarchie

Windows Forms - der Sprung ins kalte Wasser

Meine erste Begegnung mit Windows Forms war das Programm WinDes aus dem .NET SDK. Nach der Entscheidung, mit C# ein neues Win32-Formular zu entwickeln, setzte ich ein TreeView-Steuerelement ins Formular, ein Eingabefeld und ein paar Schaltflächen. Nach dem Abspeichern meines Formulars stellte ich zu meinem Erstaunen fest, dass WinDes eine einzelne .CS-Quelltextdatei geschrieben hatte und keine zusätzliche separate Datei mit einer Beschreibung meines Formulars.

Wie eine nähere Untersuchung des Codes ergab, der für das Formular generiert wurde, werden die Steuerelemente im C#-Code bei der Initialisierung des Formulars alle dynamisch angelegt. Jedes Steuerelement wird durch eine .NET-Objektinstanz repräsentiert und das Formular enthält entsprechende Datenelemente mit Referenzen auf die dazugehörigen Steuerelemente. So legen die folgenden beiden Codezeilen zum Beispiel ein Eingabefeld und einen TreeView an und initialisieren die entsprechenden Datenelemente der Formularklasse:

this.text_details = new System.WinForms.TextBox(); 
this.treeView1 = new System.WinForms.TreeView();

Und was ist mit den Properties der Attribute, die ich in WinDes geändert hatte? Nun, jedes Property wird durch eine einzelne C#-Anweisung gesetzt, die im Anschluss an die Erstellung des Steuerelements folgt. So sorgt die folgende Zeile zum Beispiel im Eingabefeld text_details für ein Schreibverbot:

text_details.ReadOnly = true;

In bestimmten Aspekten unterscheidet sich die Programmierung mit Windows Forms beträchtlich von der herkömmlichen UI-Entwicklung für Win32 mit Ressource-Dateien (.RC) oder mit dem alten Visual Basic. Sowohl die .RC-Dateien als auch die älteren Versionen von Visual Basic speicherten die Inhalte und Eigenschaften der Formulare als strukturierte Daten ab, getrennt von dem Steuercode für das Formular. Wenn ein Formular (Dialog) nach dem alten Modell angelegt wird, liest irgendein Stückchen Systemcode die Daten ein und generiert daraus für Sie das gewünschte Formular. Im Gegensatz dazu trennt Windows Forms den Aufbau des Formulars nicht mehr von dem Code ab, der das Formular erzeugt und initialisiert. Das gibt Ihnen die Möglichkeit, Inhalt und Properties bei der Erstellung des Formulars leicht abändern zu können.

Das Schöne an der Programmierung mit Windows Forms ist, dass Sie es bereits zu mindestens 95 Prozent beherrschen, falls Sie sich mit Visual Basic auskennen. Sie arbeiten nicht mehr mit den rohen User32-Fenstern. Die Formulare samt ihrer Steuerelemente werden von .NET-Klassen gekapselt. Im Normalfall brauchen Sie sich auch nicht mehr mit Windows-Nachrichten herumzuschlagen oder mit verschiedenen anderen Mechanismen aus den Tiefen der virtuellen System-Mechanik, mit denen man einen Entwickler eigentlich nicht behelligen sollte.

Der einzige nennenswerte Unterschied, der mir bei der Programmierung mit Windows Forms aufgefallen ist, ist die gewöhnungsbedürftige Einrichtung der Funktionen, die für die Bearbeitung der verschiedenen Ereignisse zuständig sind. Die Ereignisfunktionen werden dynamisch in den Ereignismechanismus eingehängt. Zur Implementierung der Funktion legen Sie zuerst eine Instanz der entsprechenden EventHandler-Klasse an. Der Konstruktor erwartet, das Sie bei seinem Aufruf auch die Funktion angeben, die Sie für die Bearbeitung des Ereignisses vorgesehen haben. Dann wird die EventHandler-Instanz mit dem betreffenden Steuerelement verknüpft. In C# geschieht das - halten Sie sich fest - mit dem Operator +=. So zeigt die folgende Zeile zum Beispiel, wie ich eine Methode namens treeView1_AfterSelect als Ereignisfunktion für das Ereignis TreeView.AfterSelect installiere:

treeView1.AfterSelect 
+= new System.WinForms.TreeViewEventHandler(treeView1_AfterSelect);

Bisher ist alles, was ich beschrieben habe, recht einfach und lässt sich mühelos mit dem regulären Quelltext erledigen. Wie sieht es nun mit Dingen wie Bitmaps, Symbolen und Ähnlichem aus? Nun, die Antwort drängte sich mir auf, als ich eine Bilderliste (ImageList) in mein Formular setzte, damit mein Baum (der TreeView) diese hübschen kleinen Bildchen anzeigen kann, die mit viel Phantasie darauf hindeuten, was ein bestimmter Knoten bedeuten könnte. Das veranlasste den WinDes, eine zweite Datei mit der Namensendung .resX anzulegen.

Übrigens macht die Einbindung einer binären "Ressource" im Initialisierungscode meines Formulars die Anlage einer System.Resources.ResourceManager-Instanz erforderlich, damit die kompilierten Ressourcen zugänglich werden.

Ein Blick in die .resX-Datei enthüllte schnell, dass es sich um eine XML-Datei handelt. Um sie in eine handhabbare Form zu bringen, müssen Sie die Datei an das ResGen-Programm verfüttern. Es liest die XML-Datei ein und wirft eine Binärdatei mit der Namensendung .resource aus. Diese .resource-Datei können Sie dann in die resultierende ausführbare Datei einbinden oder sie in der Baugruppe als separate Datei führen. Wenn Sie mit dem C#-Compiler arbeiten, sorgt der Schalter /res: für die Einbindung der .resource-Datei in die ausführbare Datei, während sie mit /linkres: als separate Datei in die Baugruppe aufgenommen wird.

Das MetaViewer-Programm

Bei der Planung des MetaViewers nahm ich mir vor, ihm die folgenden Eigenschaften zu geben:

  • Ruft man ihn mit dem Namen einer ausführbaren .NET-Datei auf der Kommandozeile auf, soll er deren Typen und Funktionen anzeigen.

  • Für jeden Typ und jede Funktion soll er die relevanten Details auf einer separaten Fläche anzeigen.

  • Mit einem vollständig oder teilweise angegebenen Namen soll die Suche nach Typen möglich sein.

  • In einem separaten Fenster sollen Informationen auf Baugruppenebene angezeigt werden, zum Beispiel die importierten Baugruppen.

Bild B2 zeigt das Hauptformular vom MetaViewer. In diesem Bild zeigt der MetaViewer übrigens seine eigenen Metadaten an. Der Code für dieses Formular ist leider zu lang, um ihn an dieser Stelle abzudrucken. Sie finden ihn auf der Begleit-CD in der Datei MetaViewer.cs. Ich habe den Code, den der WinDes generiert hat, etwas überarbeitet, damit er übersichtlicher wird. Wenn Sie sich den Code genauer anschauen, werden Sie feststellen, dass es sich um eine Art Codemischung handelt. Ein Teil des Codes steuert wie erwartet das Formular und ein anderer Teil greift über die Reflektionsklassen auf die Metadaten zu.

09Meta022

B2 Das Hauptformular vom MetaViewer.

Der Hauptteil des MetaViewer-Formulars wird von einem TreeView überdeckt, der die vielen Typen anzeigt. Jeder Hauptknoten lässt sich aufklappen, so dass man sich auch die Elemente eines Typs anschauen kann. Auf der rechten Seite gibt es eine Anzeigefläche für die Anzeige von Einzelheiten über den gerade angewählten Knoten. Wird ein bestimmter Typ ausgewählt, so zeigt die Anzeigefläche den Namensraum, aus dem der Typ kommt, sowie eine vollständige Ableitungshierarchie.

Wenn Sie einen Typknoten aufschlagen und eines seiner Elemente markieren, sind auf der Anzeigefläche die entsprechenden Daten zu sehen. Bei einer Methode geht aus den Informationen hervor, ob die Methode die Attribute virtual, static, public, private oder PInvoke (Platform Invoke) hat. Außerdem werden die Parameter und der Ergebnistyp der Methode angezeigt. Bei einem Feld wird der Feldtyp angezeigt und die Frage beantwortet, ob das Feld static, public oder private ist. Die Methode ShowMemberInfoDetails in der Datei MetaViewer.cs zeigt, wie ich diese Daten ermittle und anzeige.

Am unteren Rand des Hauptformulars gibt es zwei Schaltflächen und ein Eingabefeld. Wenn Sie einen bestimmten Typ suchen, geben Sie einfach einen beliebigen Teil seines Namens ein (es wird nicht zwischen Groß- und Kleinbuchstaben unterschieden) und drücken dann auf die Search-Taste. Sollte der MetaViewer fündig werden, wird der entsprechende Typ hervorgehoben. Die Suche beginnt beim aktuell markierten Knoten, so dass Sie weiter auf die Search-Schaltfläche drücken können, um andere Typen mit ähnlichen Namen ausfindig zu machen.

Nun, ich bin sicherlich der erste, der zugibt, dass diese Art der Suche nicht so intuitiv wie in einem ausgefeilten kommerziellen Programm ist - aber für die paar Codezeilen, die ich zur Implementierung gebraucht habe, ist das Ergebnis gar nicht mal so schlecht.

Die Schaltfläche "Assembly Info" schließlich öffnet ein weiteres Formular (Bild B3), dessen Quelltext in der Datei AssemblyInfo.cs zu finden ist (Listing L1). In den Metadaten werden zwar tonnenweise Informationen über eine Baugruppe gespeichert, aber ich habe mich der Übersichtlichkeit halber auf ein paar wichtige Angaben beschränkt, nämlich auf den Modulnamen, die importierten Baugruppen und schließlich die Liste der Dateien, die zur aktuellen Baugruppe gehören (im Normalfall werden das Ressourcen sein).

09Meta03.gif

B3 Informationen über die Baugruppe.

L1 AssemblyInfoForm.cs

//========================================== 
// Matt Pietrek 
// System Journal 
// Datei: AssemblyInfoForm.cs 
//========================================== 
namespace MetaViewerFormNamespace 
{   
  using System; 
  using System.WinForms; 
  using System.Reflection; 
  public class AssemblyInfoForm : System.WinForms.Form 
  { 
    private System.ComponentModel.Container components; 
    private System.WinForms.Button button_ok; 
    private System.WinForms.TextBox text_assemblyInfo; 
    private Assembly m_assembly; 
    public AssemblyInfoForm(Assembly assembly) 
    { 
      m_assembly = assembly; 
      // Wird für den Win Form Designer gebraucht. 
      InitializeComponent(); 
      DisplayAssemblyInfo(); 
    } 
    public override void Dispose() { 
      base.Dispose(); 
      components.Dispose(); 
    } 
    private void InitializeComponent() 
    { 
      this.components = new System.ComponentModel.Container(); 
      this.button_ok = new System.WinForms.Button(); 
      this.text_assemblyInfo = new System.WinForms.TextBox(); 
      button_ok.Location = new System.Drawing.Point(256, 320); 
      button_ok.Size = new System.Drawing.Size(64, 32); 
      button_ok.TabIndex = 0; 
      button_ok.Text = "OK"; 
      button_ok.Click += new System.EventHandler(button_ok_Click); 
      text_assemblyInfo.Location = new System.Drawing.Point(8, 8); 
      text_assemblyInfo.ReadOnly = true; 
      text_assemblyInfo.Multiline = true; 
      text_assemblyInfo.TabIndex = 0; 
      text_assemblyInfo.TabStop = false; 
      text_assemblyInfo.Size = new System.Drawing.Size(560, 296); 
      text_assemblyInfo.ScrollBars = ScrollBars.Both; 
      this.Text = "Assembly Info"; 
      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); 
      this.ClientSize = new System.Drawing.Size(576, 356); 
      this.Controls.Add(button_ok); 
      this.Controls.Add(text_assemblyInfo); 
    } 
    protected void button_ok_Click(object sender, System.EventArgs e) 
    { 
      this.Close(); 
    } 
    //=================================================== 
    protected void DisplayAssemblyInfo() 
    { 
      // Zeige Liste der Module an, die zur Baugruppe  
      // gehören. 
      foreach ( Module module in m_assembly.GetModules() ) 
      { 
        text_assemblyInfo.AppendText( 
          String.Format("Module: {0}\r\n", module.Name) ); 
      } 
      //================================================= 
      // Zeige eine Liste der Baugruppen an, die von  
      // dieser Baugruppe benutzt werden. 
      text_assemblyInfo.AppendText( "\r\n" ); 
      text_assemblyInfo.AppendText( "Referenced Assemblies:\r\n" ); 
      foreach( AssemblyName assemblyName in 
            m_assembly.GetReferencedAssemblies() ) 
      { 
        text_assemblyInfo.AppendText( String.Format("\t{0}\r\n", 
                        assemblyName.Name) ); 
      } 
      //================================================= 
      // Zeige die Dateien aus dieser Baugruppe an. 
      text_assemblyInfo.AppendText( "\r\n" ); 
      text_assemblyInfo.AppendText( "Files:\r\n" ); 
      foreach ( String strResourceName in 
            m_assembly.GetManifestResourceNames() ) 
      { 
        text_assemblyInfo.AppendText( String.Format("\t{0}\r\n", 
                        strResourceName) ); 
      } 
    } 
  } 
}

Zurück im Hauptformular gehörte es zu den angenehmsten Erfahrungen, die ich bisher mit Windows Forms und der .NET-Programmierung gemacht habe, wie leicht sich die Metadaten-Infos im Formular darstellen lassen. Im Prinzip habe ich meine Daten einfach den UI-Klassen vor die Füße geworfen. Das Zauberwort heißt dabei Ableitung.

Das TreeView-Steuerelement von Windows Forms erhält seine Einträge, indem man neue TreeNode-Instanzen mit dem TreeView verknüpft. Da ich meine eigene Klasse von TreeNode ableite, kann ich meine Metadaten in derselben Klassen unterbringen, in der auch die TreeNode-Daten liegen. In der Klasse MemberInfoNode aus der Datei MemberNode.cs finden Sie ein Beispiel dafür (Listing L2). Die Klasse MemberInfoNode leitet sich von TreeNode ab und weist ein zusätzliches Feld für eine MemberInfo-Instanz auf.
L2 MemberNode.cs

//========================================== 
// Matt Pietrek 
// System Journal 
// Datei: MemberNode.cs 
//========================================== 
namespace MetaViewerFormNamespace 
{ 
  using System.WinForms; 
  using System.Reflection; 
  public class MemberInfoNode : TreeNode 
  {  
    // Zu diesem Knoten gehört die Klasse 
    // System.Reflection.MemberInfo 
    public MemberInfo m_memberInfo; 
    public MemberInfoNode(MemberInfo memberInfo) 
    { 
      m_memberInfo = memberInfo; 
      Text = m_memberInfo.Name; 
    } 
  } 
}

Außer MemberInfoNode habe ich auch noch eine Klasse namens TypeNode implementiert, die sich ebenfalls von TreeNode ableitet. Der TypeNode stellt einen Metadatentyp dar und hat ein Feld, in dem sich die entsprechende System.Type-Instanz unterbringen lässt. Wenn ich ein Element in den TreeView eintrage, dann übergebe ich entweder einen TypeNode oder einen MemberInfoNode, also nicht einfach nur einen TreeNode.

Wenn ein TreeView-Ereignis eintritt (es wird zum Beispiel ein Knoten ausgewählt), dann erhält die zuständige Ereignisfunktion eine Referenz auf den aktuellen TreeNode. Mein Code konvertiert diesen TreeNode nun wieder in einen MemberInfoNode oder einen TypeNode, damit er die gewünschten Daten auslesen kann. Woher weiß ich, welche Objektart an meine Ereignisfunktion übergeben wurde? Nun, hier erweist sich der is-Operator von C# als sehr nützlich. Die folgenden Zeilen demonstrieren, wie man ihn einsetzt:

if ( e.node is TypeNode ) 
{ 
    ShowTypeDetails( ((TypeNode)e.node).m_type ); 
} 
else if ( e.node is MemberInfoNode ) 
{ 
    ShowMemberInfoDetails( ((MemberInfoNode)e.node).m_memberInfo ); 
}

Interessante Probleme gelöst

Einer der ersten Bugs, auf die ich mit dem MetaViewer stieß, war der Umstand, dass ich nur die öffentlichen Elemente der Typen aus der Baugruppe zu sehen bekam. Ein Vergleich mit meinem ursprünglichen ReflectMeta-Programm aus dem Artikel im Januarheft zeigte schnell, dass es damit genau dasselbe Problem gab. Selbstverständlich suchte ich intensiv nach dem Fehler, aber ich kam einfach nicht auf die richtige Idee. Nur der Gedanke, dass dieses Problem doch wohl viel zu offensichtlich war, um ein Bug im .NET Framework zu sein, hielt mich von einem Bugreport ab.

Irgendwann sah ich mir wieder einmal die Dokumentation der Methode GetMember von System.Type an und bemerkte dann endlich, dass es sich um eine überladene Methode handelte. Ich rief die einfache GetMember-Form ohne Parameter auf. Wie ein genaueres Studium der Dokumentation ergab, liefert diese Methode tatsächlich nur die öffentlichen Elemente eines Typs. Wenn ich eine vollständige Liste der Klassenelemente haben möchte, muss ich eine andere Form der GetMember-Methode aufrufen, nämlich eine mit einem BindingFlags-Parameter.

Was lernt man daraus? Die Klassenbibliothek vom .NET ist riesengroß und überlädt die Methoden anscheinend mit wachsender Begeisterung. Wenn sie nicht das gewünschte Verhalten zeigt oder die geplante Arbeit anscheinend nicht leistet, sollte man ruhig noch einmal einen Blick in die Dokumentation riskieren und überprüfen, ob man es vielleicht mit einer überladenen Methode zu tun hat, von der es noch andere Versionen gibt, die sich vielleicht besser eignen. In dieser Beziehung werden es die MFC-Programmierer sicherlich leichter als die Visual Basic-Programmierer haben, sich im .NET Framework zurechtzufinden.
Ein weiteres interessantes Problem trat bei der Implementierung der Typsuche anhand des Namens auf. In C++ habe ich die Funktion strstr eher als meinen Freund angesehen. Im .NET konnte ich nichts Vergleichbares finden. Trotz mehrfacher gründlicher Überprüfung der String-Klasse und ihrer Methoden wurde ich nicht fündig. Irgendwann stieß ich dann auf die Klasse RegEx (Regular Expression), die im Namensraum System.Text.RegularExpressions zu finden ist.
Ein paar Minuten in der Online-Dokumentation und ich hatte den Code fertig, der die einzelnen Typknoten aufzählt und mit der IsMatch-Methode von RegEx überprüft, ob der Name mit dem Suchbegriff übereinstimmt. Leider wurde bei diesem Vergleich die Schreibung berücksichtigt. Da mir aber noch die Erfahrung mit den überladenen Methoden frisch in Erinnerung war, die ich gerade auf die harte Tour gelernt hatte, sah ich wieder in der Dokumentation nach und entdeckte dort, dass RegEx einen zweiten Konstruktor hat, bei dem man die Suchoptionen festlegen kann. Wie meine Implementierung des Suchcodes nun aussieht, können Sie in der Methode button_search_Click sehen (in MetaViewer.cs).

Das dritte Problem, auf das ich stieß, hängt mit den unverwalteten Werttypen zusammen. Im Normalfall handelt es sich um klassische C++-Strukturen, die nicht auf der verwalteten Halde angelegt werden. Die .NET-Datei corhdr.h besagt, dass unverwaltete Werttypen nicht empfohlen werden. Allerdings finden sich immer noch sehr viele Stellen, an denen sie eingesetzt werden. Letztlich konnte ich mich jedenfalls nicht dazu aufraffen, einen separaten TreeView für unverwaltete Werttypen zu implementieren. Also benutze ich für verwaltete und unverwaltete Typen dieselbe Bitmap.

Mir hat die Entwicklung vom MetaViewer viel Spaß gemacht und ich habe dabei einiges über C# und das .NET Framework gelernt. Obwohl ich die C++-Programmierung noch nicht mit fliegenden Fahnen untergehen lasse, bin ich doch beeindruckt, was sich mit den wenigen Codezeilen erreichen lässt - auch wenn das wohl eher an der umfangreichen Klassenbibliothek liegt als an C#. Vielleicht konnte ich Sie sogar so neugierig machen, dass Sie selbst schon mit der Erkundung der Metadaten begonnen haben. Wie wäre es mit der Entwicklung eines eigenen kleinen Metadaten-Explorers?