Der Einsatz von XML-Providern für den Datenzugriff

Veröffentlicht: 27. Jan 2002 | Aktualisiert: 15. Jun 2004

Von Aaron Skonnard

Wir stehen wieder einmal vor einem Paradigmenwechsel, wobei sich die Datenhaltung von OLE DB in Richtung XML orientiert. Durch selbsterstellte Infoset-Provider können Sie Daten aus nahezu beliebigen Quellen in XML-Form anbieten und mit Hilfe mächtigen XML-Funktionen von .NET auswerten.

Auf dieser Seite

Probleme mit OLE DB
XML ist UDA
XML-Infosets
Die Infoset-Repräsentationen in .NET
XPathNavigator oder XmlReader?
Erweiterung von XPathNavigator
XPathNavigator-Semantik
Beispiele für anwenderdefinierte XPathNavigator
XPath-Unterstützung
XSLT-Unterstützung
Entwicklung von anwenderdefinierten Lesern
Die Welt als XML-Dokument

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

Bild10

Vor ungefähr fünf Jahren führte Microsoft die UDA-Architektur ein (Universal Data Access), die den Datenverkehr im Unternehmen zu vereinfachen versprach. Die UDA-Strategie verfolgte unter anderem das Ziel, möglichst sämtliche Daten durch eine einzige Gruppe von COM-Schnittstellen anzubieten, die inzwischen als OLE DB bekannt geworden ist. Dahinter stand die Überlegung, dass eine einheitliche Schnittstellengruppe so manche Hürde für die Anwender beseitigen und ihnen den Zugriff auf die verschiedenen Datenspeicher erleichtern würde, und zwar unabhängig davon, ob die Daten nun relational strukturiert sind oder nicht. In einer OLE DB-zentrischen Welt brauchen sich die Anwender nicht mehr mit den vielen lästigen Einzelheiten der vielen datenquellenspezifischen Programmierschnittstellen abzuplagen.
Natürlich geht man bei dieser Strategie von der Annahme aus, irgendjemand würde schon für die Softwareschicht sorgen, die für die Umsetzung der universellen OLE DB-Schnittstelle auf die proprietäre Programmierschnittstelle der Datenquelle zuständig ist. Dieses Softwarestückchen, das zwischen dem Verbraucher und der speziellen Programmierschnittstelle der Datenquelle sitzt, wird OLE DB-Anbieter genannt (OLE DB provider). Ein passender Name, denn er bietet den Zugriff auf den Datenspeicher in OLE DB-Form an.
Heutzutage sind viele verschiedene OLE DB-Anbieter im Umlauf, für alle möglichen Arten von Daten, zum Beispiel für den SQL Server, Oracle, Jet (Access), ISAM, AS/400, DB2, ODBC, OLAP, Microsoft Index/Suchdienst und viele, viele andere. OLE DB-Verbraucher können über dieselbe Schnittstellengruppe auf alle diese Datenspeicher zugreifen, solange es nur den passenden OLE DB-Anbieter gibt.
Als OLE DB Fahrt aufnahm, gingen manche Entwickler dazu über, die ganze Welt durch die OLE DB-farbige Brille zu betrachten. Wenn sie Daten brauchten oder abspeichern wollten, gingen diese Entwickler grundsätzlich über OLE DB. Auch wenn das leicht übertrieben erschien, hatten diese Entwickler die Vision einer Zukunft, in der die Standardisierung irgendwann die entsprechende Infrastruktur nach sich ziehen würde, wobei die Arbeit durch universell einsetzbaren Code, entsprechende Werkzeuge und Dienste erleichtert werden sollte. Letztlich hat OLE DB dieses hochgesteckte Ziel nicht erreicht. Aber seine Verfechter waren zweifellos auf dem richtigen Weg.

Probleme mit OLE DB

Im Rückblick scheint es überdeutlich zu sein, dass OLE DB niemals ein wirklich universeller Standard für das Angebot von Informationen und den Zugriff auf Informationen werden konnte, weil der Wettbewerb mit anderen Lösungen von anderen Herstellern das verhinderte und immer noch verhindert. Da es sich bei OLE DB um eine COM-Technik handelt und COM selbst nie über alle wichtigen Plattformen hinweg angeboten oder unterstützt wurde, hatte es nie wirklich eine Chance. Zudem ist es ziemlich schwierig, direkt mit OLE DB zu arbeiten. Das trägt nicht gerade zur höheren Akzeptanz bei. Manche Entwickler wünschen sich auch heute noch die Vorteile von OLE DB, ohne allerdings bereit zu sein, den hohen Preis dafür zu zahlen.
Während OLE DB fleißig damit beschäftigt war, sich möglichst in alle Welt zu verbreiten, wuchs eine andere Lösung heran, die anfangs relativ unbeachtet blieb und OLE DB inzwischen als echte UDA-Lösung für das Angebot von Daten verdrängte. Bei dieser Lösung handelt es sich, wie Sie sicher schon ahnen, um XML und seine stetig wachsende Familie an Spezifikationen und Werkzeugen.

XML ist UDA

Wenn Sie das bisher Gesagte noch einmal lesen und OLE DB dabei durch XML ersetzen, werden Sie feststellen, dass XML dieselben Ziele verfolgt, die auch OLE DB anstrebte, ohne an denselben Problemen zu leiden. Tatsächlich ist XML nun der weltweit verbreitete, überall anzutreffende Industriestandard, der auf allen Plattformen verfügbar ist. Zudem ist es wesentlich einfacher als OLE DB.
Ein relativ einfacher Weg zum universellen Datenzugriff mit XML führt über die Abbildung aller Datenquellen auf XML 1.0-Dateien, wie in Bild B1 skizziert. Dieses Szenario erlaubt es den Verbrauchern, jeden XML-Parser oder jede passende Programmierschnittstelle auf der Plattform ihrer Wahl einzusetzen. In diesem Fall verhält sich der XML-Parser insofern wie der Anbieter, als dass er die XML 1.0-Daten wieder zurück auf eine XML-Standardschnittstelle abbildet, die von einer heterogenen Kundschaft leicht verstanden werden kann. Sobald die Daten als XML angeboten werden, können die Clients zur Vereinfachung der Bearbeitung auf die üblichen XML-Hilfsmittel zurückgreifen, zum Beispiel auf XPath und XSLT.

Bild01

B1 Abbildung der Datenquellen auf XML 1.0-Dateien

Dateigröße und Bearbeitungsgeschwindigkeit sind bei dieser Technik zweifellos kritische Größen, aber die wesentlich verbesserte Interoperabilität wiegt häufig die Nachteile auf. Trotzdem ist es in vielen Fällen einfach nicht möglich, umfangreiche Datenquellen auf XML 1.0-Dateien abzubilden.

XML-Infosets

Der ultimative XML-Anbieter würde die Daten nicht in Form von XML 1.0 anbieten, sondern die XML-Standardschnittstellen direkt auf die speziellen Schnittstellen der Datenquelle abbilden. Wie bei OLE DB erlaubt dieser Lösungsansatz den Verbrauchern, mit Hilfe von XML-Standardschnittstellen auf eine große Vielfalt an Datenquellen zuzugreifen, und zwar ohne nennenswerte Leistungseinbuße.
Bei diesem Lösungsansatz ist es wie bei dem vorigen möglich, über den anwenderdefinierten XML-Anbietern noch weitere Softwareschichten anzuordnen, in denen andere XML-Techniken wie XPath oder XSLT benutzt werden (Bild B2). Auch das kann ohne nennenswerte Leistungseinbuße geschehen. Allerdings hat diese Lösung eine wesentlich bessere Chance, die UDA-Utopie zu realisieren, von der bereits die Rede war, solange alle Beteiligten bereit sind, sich auf das abstrakte Datenmodell von XML zu einigen. Wird dieses Modell nicht standardisiert, gibt es auf lange Sicht keine Hoffnung auf eine reibungslose Zusammenarbeit der verschiedenen Plattformen auf der Anbieter-Ebene. Das W3C hat versucht, das abstrakte XML-Datenmodell in einer Spezifikation zu beschreiben, die XML Information Set genannt wird (Infoset). Das Infoset beschreibt die logische Struktur eines XML-Dokuments in Form von Knoten (auch Informationseinheiten oder Information Items genannt), die Properties haben.

Bild02

B2 Zusätzliche XML-Verfahren lassen sich ebenfalls einsetzen

Bild B3 skizziert den Infoset eines XML 1.0-Dokuments. Jeder Knoten im Baum hat eine wohldefinierte Menge von Properties, die vom Anbieter verfügbar gemacht werden müssen. Ein Elementknoten hat zum Beispiel einen Namensraumnamen, einen lokalen Namen, ein Präfix, eine ungeordnete Menge an Attributen und eine geordnete Liste mit Kindern (weitere Details finden Sie in der Infoset-Spezifikation unter http://w3.org/xml/schema). Diese abstrakte Beschreibung eines XML-Dokuments standardisiert die Information, die durch die XML-Anbieter für die Verbraucher verfügbar gemacht wird.

Bild03

B3 Der Infoset eines XML 1.0-Dokuments

Eine XML-Programmierschnittstelle bildet den Infoset einfach auf programmierbare Typen ab. Heutzutage gibt es schon eine ganze Menge Infoset-kompatibler Schnittstellen, darunter SAX (Simple API für XML), DOM (Document Object Model) und die neueren .NET-Schnittstellen von Microsoft. Und es werden sicher noch mehr werden. Die Entwicklung eines XML-Infoset-Anbieters reduziert sich nun auf die Frage, wie man die Informationen aus der Datenquelle mit einer Infoset-kompatiblen Schnittstelle anbietet. Durch die Umformungen werden die Informationen letztlich als eine Infoset-Repräsentation angeboten und die Verbraucher haben nun den Vorteil, mit einer wohlbekannten XML-Schnittstelle arbeiten zu können.
Die heutigen DOM-Implementierungen setzen zum Beispiel direkt auf Datenbanken auf. SAX-Produzenten führen zur Erstellung der Infosets explizite Aufrufe von ContentHandler-Implementierungen durch (es ist kein Parser im Spiel). Und es gibt bereits für eine ganze Reihe von Datenquellen .NET-konforme XML-Anbieter, zum Beispiel auch für die Registrierdatenbank von Windows und für .NET-Baugruppen (Assemblies).
Ein gewisses Problem dieses Lösungsansatzes besteht darin, dass er potentielle Kunden an eine bestimmte XML-Programmierschnittstelle bindet, so wie OLE DB die Clients an COM gebunden hat. So können zum Beispiel nur SAX-fähige Anwendungen SAX-Anbieter benutzen, wie auch nur .NET-Anwendungen auf die Anbieter in .NET zugreifen können. Um eine gegebene Datenquelle tatsächlich universell nutzbar zu machen, zumindest für XML-Clients, müssen Sie also für alle wichtigen XML-Programmierschnittstellen entsprechende Anbieter implementieren. Die meisten Entwickler werden das zwar als lästig, aber akzeptabel einstufen, sobald es um eine höhere Leistung geht und eine XML 1.0-Lösung nicht mehr in Frage kommt.
Wichtig ist, dass XML-Anbieter (sowohl auf Basis von XML 1.0 als auch auf Basis von Infosets) im Gegensatz zu OLE DB-Anbietern schreibgeschützt sind, also im Normalfall nur Lesezugriffe erlauben. Sie bieten die verfügbaren Informationen einfach über ein standardisiertes, abstraktes Datenmodell an, sagen aber überhaupt nichts darüber aus, wo die Daten eigentlich liegen oder wie sie aktualisiert werden. Die Lösung dieses Problems ist nämlich wesentlich schwieriger als das simple Angebot der verfügbaren Daten, da es in dieser Hinsicht große Unterschiede zwischen den Datenspeichern gibt. Das ist auch einer der Gründe, aus denen OLE DB (und ODBC) auch weiterhin in typischen Datenbankanwendungen zu finden sein werden, wenn es auf solche genau gesteuerten Interaktionen ankommt.

Die Infoset-Repräsentationen in .NET

Das .NET Framework bietet mit XPathNavigator, XmlReader und XmlWriter drei Basisklassen an, die den Infoset jeweils auf andere Weise modellieren. XPathNavigator modelliert den Infoset als eine Art begehbaren Knotenbaum. XmlReader modelliert den Infoset als Knotenstrom, der nur in eine Richtung weitergegeben wird (forward-only, pull), und XmlWriter modelliert den Infoset als Folge von Methodenaufrufen (push). (Alle Bezüge auf .NET in diesem Artikel beruhen auf einer Vorversion der Beta 2 und können sich bis zur endgültigen Veröffentlichung natürlich noch ändern.)
Wie gerade erwähnt, modelliert XPathNavigator einen Infoset als begehbaren Knotenbaum (Bild B3). Die Baumdarstellung entspricht genau der Darstellung, die von der W3C XPath Recommendation vorgegeben wird (daher der Name XPathNavigator). Dabei handelt es sich praktisch um eine 1:1-Abbildung des Infosets. Für Verbraucher ist diese Form noch die am leichtesten zu verstehende Repräsentation, weil sie sich mehr als die anderen wie XML "anfühlt".
Das .NET Framework bietet eine Implementierung von XPathNavigator an, die sich auf solche DOM-Bäume anwenden lässt, die im Speicher liegen. Derzeit wird diese Klasse DocumentXPathNavigator genannt. Da es sich um eine private Klasse handelt, können Sie von ihr aber keine Instanzen anlegen. Jedenfalls nicht direkt. Statt dessen sieht .NET eine Fabrikschnittstelle namens IXPathNavigable vor, die von Datenquellen implementiert werden sollte, wenn sie die XPathNavigator-Funktionalität unterstützen. Im Fall von DOM können Sie zum Beispiel für einen beliebigen Knoten im Baum eine Instanz von einer XPathNavigator-Implementierung anlegen, wie hier gezeigt (node sei eine Referenz auf ein XmlNode).

node.CreateNavigator(); 

Dieses Konzept ermöglicht die Veröffentlichung jeder Datenquelle durch XPathNavigator, wie in Bild B4 skizziert. Sobald ein passender Anbieter für Ihre Datenquelle geschrieben wurde, können Sie ihn wie die eingebauten Implementierungen benutzen.

Bild04

B4 XPathNavigator

XmlReader modelliert das Auslesen eines Infosets als linearen Knotenstrom (forward-only), wie in Bild B5 dargestellt. Anders als SAX erlaubt XmlReader dem Client, die Knoten einzeln anzunehmen, vergleichbar vielleicht mit dem Firehose-Cursormodell in der Datenzugriffstechnik. Diese Repräsentation bietet den Verbrauchern ein relativ einfaches und glattes Programmiermodell. Attribute sind allerdings nicht Bestandteil des Stroms und man muss sich um spezielle Endknoten kümmern. In manchen Fällen ist der Aufwand also größer als bei einem Firehose-Cursor.

Bild05

B5 XmlReader

.NET bietet von XmlReader mehrere eingebaute Implementierungen an, darunter XmlTextReader und XmlNodeReader. XmlTextReader liest Texte, die sich an XML 1.0 halten, während XmlNodeReader einen DOM-Baum liest. Wie bei XPathNavigator lässt auch das XmlReader-Konzept anwenderdefinierte Leserimplementierungen zu, die auf einen beliebigen Datenspeicher aufsetzen (zum Beispiel ein Dateisystemleser).
Während XPathNavigator und XmlReader das Lesen von Infosets modellieren, kümmert sich XmlWriter um das Schreiben von Infosets. Die Schreibzugriffe erfolgen durch eine Sequenz von Methodenaufrufen. Der XmlWriter lässt sich insofern mit SAX vergleichen, als er einen Infoset in eine gegebene Implementierung schiebt (push). Eine anwenderdefinierte Implementierung von XmlWriter würde im Normalfall die Sequenz der Methodenaufrufe zurück auf ein anwenderdefiniertes Datenformat oder einen Datenspeicher abbilden, wie XML 1.0, EDI (electronic data interchange) oder sogar auf eine Datenbank. Die eingebaute XmlTextWriter-Implementierung bildet die Sequenz der Methodenaufrufe einfach wieder auf das XML 1.0-Format ab.
Kurz zusammengefasst kann man sagen, dass es XmlReader und XPathNavigator ermöglichen, Informationen als XML-Infoset anzubieten, während XmlWriter das Umgekehrte macht. Weitere Einzelheiten über die Arbeitsweise von XmlReader und XmlWriter finden Sie im XML-Artikel aus Heft 3/2001.
Im Folgenden möchte ich mich darauf konzentrieren, wie Datenquellen mit XPathNavigator- und XmlReader-Implementierungen angeboten werden.

XPathNavigator oder XmlReader?

Die erste wichtige Frage bei der Entwicklung eines anwenderdefinierten XML-Anbieters ist die Frage, welches dieser beiden Modelle benutzt werden soll. Ob XPathNavigator oder XmlReader besser geeignet ist, hängt von der Struktur der zugrundeliegenden Datenquelle ab, von der Funktionalität ihrer speziellen Programmierschnittstelle und von der Funktionalität, die den Verbrauchern zur Verfügung stehen soll.
Wenn der zugrundeliegende Datenspeicher die Daten als Strom liefern kann und seine hauseigene Schnittstelle nur Vorwärtsbewegungen durch den Strom zulässt, passt XmlReader am besten. Der Versuch, solch einen Datenstrom via XPathNavigator anzubieten, würde ein ausgefeiltes Cache-System erfordern, damit sich der Verbraucher in verschiedenen Richtungen durch den zugrundeliegenden Datenstrom bewegen kann.
Ist der zugrundeliegende Datenspeicher dagegen hierarchisch strukturiert und beherrscht seine Schnittstelle Bewegungen in verschiedene Richtungen, wie zum Beispiel die Registrierdatenbank von Windows, so passt XPathNavigator besser. Die Daten liegen bereits in einer Baumstruktur vor und der Anbieter braucht sich nicht mit den speziellen Endknoten herumzuschlagen (Bild B5).
Außerdem ist die Implementierung von XPathNavigator die einzige Option, wenn die Verbraucher von Ihrem Anbieter XPath/XSLT verlangen. Die XPath/XSLT-Implementierungen von .NET sind streng in Form von XPathNavigator-Referenzen implementiert. Das ist durchaus sinnvoll, weil die Implementierung von XPath/XSLT auf der Basis von XmlReader nicht möglich wäre, ohne das gesamte Dokument im Speicher zu halten. Damit wäre der wichtigste Vorteil des Stream-Modells verloren. Und verliert man diesen Vorteil, kann man die Infoset-Repräsentation auch gleich mit XPathNavigator implementieren.
Außerdem kann man XmlReader mit einer beliebigen XPathNavigator-Implementierung als Grundlage implementieren. Das Umgekehrte gilt aber nicht ohne Einschränkungen für die Ausdrücke. Eine generische XPathNavigator-Implementierung, die auf einem beliebigen XmlReader aufsetzt, ist zwar möglich, könnte aber nur solche XPath-Ausdrücke auswerten, die nicht versuchen, im zugrundeliegenden Datenstrom rückwärts zu gehen (daher gibt es gibt keine Eltern- oder Vorfahrenachsen). Im Allgemeinen ist die Wahl von XPathNavigator besser - es sei denn, die zugrundeliegende Datenquelle bietet nur Datenströme an.

Erweiterung von XPathNavigator

Zur Entwicklung eines anwenderdefinierten XPathNavigators wird von XPathNavigator eine neue Klasse abgeleitet. Da es sich bei XPathNavigator um eine abstrakte Klasse handelt, müssen alle ihre abstrakten Bestandteile überschrieben werden, damit die neue Klasse konkret wird und sich Instanzen der neuen Klasse anlegen lassen. Listing L1 zeigt, wie man eine neue Klasse von XPathNavigator ableitet. Außerdem skizziert Listing L1 die erforderlichen Überschreibungen. Der Code in den Methoden fehlt allerdings noch.

L1 Ein anwenderdefinierte Vorlage für XPathNavigator

public class MyNavigator : XPathNavigator 
{ 
  // die zu überschreibenden abstrakten Properties  
  public override string LocalName { get {} } 
  public override string NamespaceURI { get{} } 
  public override string Name { get{} } 
  public override string Prefix { get{} } 
  public override XPathNodeType NodeType { get{} } 
  public override string Value { get{} } 
  public override string BaseURI { get{} } 
  public override string XmlLang { get{} } 
  public override bool IsEmptyElement { get{} } 
  public override XmlNameTable NameTable { get{} } 
  public override bool HasAttributes { get{} } 
  public override bool HasChildren { get{} } 
  // die zu überschreibenden abstrakten Methoden 
  // Methoden für den Gang durch den Baum 
  public override bool MoveToNext() {} 
  public override bool MoveToPrevious() {} 
  public override bool MoveToFirst() {} 
  public override bool MoveToFirstChild() {} 
  public override bool MoveToParent() {} 
  public override void MoveToRoot() {} 
  public override bool MoveTo(XPathNavigator other) {} 
  public override bool MoveToId(string id) {} 
  public override bool IsSamePosition( 
    XPathNavigator other) {} 
  public override XPathNavigator Clone() {} 
  // Methoden für die Zugriffe auf die Attributknoten 
  public override string GetAttribute( 
    string localName, string namespaceURI) {} 
  public override bool MoveToAttribute(  
    string localName, string namespaceURI) {} 
  public override bool MoveToFirstAttribute() {} 
  public override bool MoveToNextAttribute() {} 
  // Methoden für die Zugriffe auf Namensraumknoten 
  public override string GetNamespace(string prefix) {} 
  public override bool MoveToNamespace(string prefix) {} 
  public override bool MoveToFirstNamespace() {} 
  public override bool MoveToNextNamespace() {} 
}

Die Implementierung dieser Bestandteile definiert die Abbildung auf die zugrundeliegende Datenquelle. Wenn Sie zum Beispiel einen Navigator für das Dateisystem bauen möchten, sollte die Implementierung von MoveToFirstChild das erste Kind im aktuellen Verzeichnis ansteuern (sofern vorhanden). Wie Sie diese Bestandteile genau implementieren, hängt völlig vom zugrundeliegenden Datenspeicher ab und von seiner speziellen Programmierschnittstelle.

XPathNavigator-Semantik

XPathNavigator arbeitet mit dem Konzept eines Cursors, der auf den aktuellen Knoten verweist. Erfolgt ein Zugriff auf eines der XPathNavigator-Properties (Listing L1), so liefern diese Properties Informationen über den aktuellen Knoten. So geben zum Beispiel die Properties LocalName, NamespaceURI, Name, Prefix und Value die entsprechenden Informationen über den aktuellen Knoten.Aus den Properties HasAttributes und HasChildren geht hervor, ob der aktuelle Knoten irgendwelche Attribute oder Kindknoten hat. Sind Attribute vorhanden, kann der Zugriff unter Angabe des Namens mit der Methode GetAttribute erfolgen. Die Methode MoveToAttribute ermöglicht es, den Cursor auf einen bestimmten Attributknoten zu setzen (wieder unter Angabe des Namens), während MoveToFirstAttribute und MoveToNextAttribute die Aufzählung der ganzen Attributsammlung ermöglichen. Sobald der Cursor auf einem Attributknoten steht, lassen sich die Informationen aus dem aktuellen Attribut mit den Properties von XPathNavigator auslesen.
Steht der Cursor auf einem Attribut, so führt der einzige Weg zurück zum Element über den Aufruf von MoveToParent. Nun werden Attribute aber nicht als Kinder betrachtet. Wie können Sie dann Eltern haben? Die Antwort ist ganz einfach: Die XPath-Spezifikation legt es so fest. XmlReader benutzt zu diesem Zweck MoveToElement (ähnlich dem ownerElement-Property von DOM), aber letztlich ist beides dasselbe. Diese leicht unterschiedliche Interpretation des abstrakten XML-Datenmodells ist genau der Grund, der die Entwicklung von Infosets ausgelöst hat. Damit solche Probleme verbindlich gelöst werden.
Hat ein gegebener Elementknoten Namensraumknoten, erfolgt der Zugriff darauf so ähnlich wie bei den Attributen über die Methoden GetNamespace, MoveToNamespace, MoveToFirstNamespace und MoveToNextNamespace. Laut XPath-Spezifikation hat jeder Elementknoten eine Gruppe von Namensraumknoten, nämlich einen für jede sichtbare Namensraumdeklaration. Wie bei den Attributen müssen Sie wieder MoveToParent aufrufen, wenn Sie vom Namensraumknoten auf das besitzende Element zurückgehen möchten.
Mit den MoveTo-Methoden ist der Gang durch den Baum in jede Richtung möglich. MoveToFirstChild setzt den Cursor auf den ersten Kindknoten des aktuellen Knotens. MoveToNext verschiebt den Cursor auf den nächsten Geschwisterknoten des aktuellen Knotens. MoveToPrevious setzt den Cursor vom aktuellen Knoten aus wieder auf den vorigen Geschwisterknoten zurück. Und MoveToFirst setzt den Cursor auf den ersten der Geschwisterknoten, in der Reihenfolge des Dokuments gezählt. MoveToParent verschiebt den Cursor auf den Elternknoten des aktuellen Knotens, während MoveToRoot ihn auf den obersten Knoten des Baums setzt, der auch Wurzel oder Dokumentknoten genannt wird.
Es gibt noch mehr praktische Move-Methoden. MoveToId setzt den Cursor auf den Elementknoten, der ein Attribut vom Typ ID mit dem angegebenen Wert hat (das erfordert ein DTD oder Schema). MoveTo setzt den Cursor auf dieselbe Position, die er auch im angegebenen XPathNavigator inne hat.
Die Methode MoveTo ist besonders nützlich, wenn sie zusammen mit Clone benutzt wird. Clone liefert einen genauen Schnappschuss des aktuellen XPathNavigators, wie in den folgenden Zeilen gezeigt:

public void FindFooChild(XPathNavigator nav) 
{ 
  XPathNavigator clone = nav.Clone(); 
  // arbeitet man mit clone weiter, bleibt nav unverändert 
  bool found = clone.MoveToFirstChild(); 
  while (found) 
  { 
    if (clone.LocalName.Equals("foo")) 
    { 
      // Erfolg. Setze nav auf die neue Position. 
      nav.MoveTo(clone); 
      break; 
    } 
    else 
      found = clone.MoveToNext(); 
  } 
}

Das ermöglicht es den Verbrauchern, mit temporären Kopien des Navigators zu arbeiten, bevor sie den Cursor endgültig verschieben. Mit der Methode IsSamePosition lässt sich überprüfen, ob der aktuelle Navigator an derselben Position wie der angegebene Navigator steht.

Beispiele für anwenderdefinierte XPathNavigator

Zur passenden Implementierung dieser Bestandteile ist im Anbieter normalerweise ein kleiner endlicher Automat erforderlich (eine Zustandsmaschine). Dieser Automat kümmert sich einfach um den aktuellen Koten im zugrundeliegenden Datenspeicher und um dessen Verschiebung auf Eltern-, Geschwister- und Kindknoten. Und natürlich um die Attribut- und Namensraumknoten, sofern vorhanden. Ist der Automat erst einmal fertig, gestaltet sich die Implementierung der XPathNavigator-Methoden und Properties ziemlich simpel.
Ich möchte Ihnen an einigen XPathNavigator-Beispielen zeigen, wie die Implementierung erfolgt. Die vollständigen Quelltexte der Beispiele finden Sie auf der Begleit-CD dieses Hefts.
Das einfachste Beispiel ist ZipNavigator, ein Navigator für ZIP-Dateien. Er bietet eine ZIP-Datei als XML-Dokument an. Die interne Struktur einer ZIP-Datei ist einfach eine lineare Liste der komprimierten Dateien, über die detaillierte Informationen vorliegen. Ich habe diese Struktur als XML-Dokument mit einem contents-Knoten als Hauptelement modelliert. Im Element contents gibt es für jede enthaltene komprimierte Datei ein Kindelement. Jedes dieser Elemente wird mit mehreren Attributen versehen, die zur genaueren Beschreibung dienen und zum Beispiel Angaben über den Pfad aufnehmen oder die Größe der komprimierten Datei nennen. Bild B6 zeigt eine ZIP-Datei, die mit WinZip geöffnet wurde, und Listing L2 stellt das entsprechende XML-Format dar, wie es der ZipNavigator anbietet.

Bild06

B6 So stellt WinZip eine geöffnete ZIP-Datei dar.

L2 Eine ZIP-Datei als XML

<contents> 
  <fsnav_x005C_bin path="fsnav\bin\"  
    compressedSize="0" uncompressedSize="0"  
    lastModified="05/30/01 14:22:40" /> 
  <aaronsziputilslib.dll  
    path="fsnav\bin\AARONSZIPUTILSLib.dll"  
    compressedSize="1492" uncompressedSize="4096"  
    lastModified="05/30/01 14:22:34" /> 
  <developmentor.xml.dll  
    path="fsnav\bin\Developmentor.Xml.dll"  
    compressedSize="18595" uncompressedSize="57344"  
    lastModified="05/30/01 14:22:38" /> 
  <fsnav.exe path="fsnav\bin\fsnav.exe"  
    compressedSize="2749" uncompressedSize="8704"  
    lastModified="05/30/01 14:22:40" /> 
  <fsnav.pdb path="fsnav\bin\fsnav.pdb"  
    compressedSize="1461" uncompressedSize="13824"  
    lastModified="05/30/01 14:22:40" /> 
  <fsnav.cs path="fsnav\fsnav.cs"  
    compressedSize="1362" uncompressedSize="5284"  
    lastModified="05/23/01 14:18:40" /> 
  <makefile path="fsnav\makefile"  
    compressedSize="417" uncompressedSize="929"  
    lastModified="05/23/01 14:37:32" /> 
  <fsnav path="fsnav\"  
    compressedSize="0" uncompressedSize="0"  
    lastModified="05/30/01 14:22:38" /> 
</contents>

Listing L3 zeigt einen Auszug aus der Klasse ZipState, die sich um die aktuelle Position in der ZIP-Datei kümmert und diese Position wie einen Cursor auf Eltern- und Kindeinträge verschieben kann. Die Klasse erledigt das in Zusammenarbeit mit der speziellen ZIP-Programmierschnittstelle, die ich in diesem Beispiel benutze (AaronsZipUtils.ZipReader). Das Beispiel bleibt deswegen sehr übersichtlich und einfach, weil die Einträge in der ZIP-Datei keine Kinder haben können.

L3 Ein endlicher Automat für den ZipNavigator

namespace Developmentor.Xml 
{ 
  using System; 
  using System.IO; 
  using System.Xml;     
  using System.Xml.XPath; 
  using System.Collections; 
  using AARONSZIPUTILSLib; 
  internal class ZipState 
  { 
    public Object currentObject; 
    public ZipState parent; 
    public int indexOfCurrentInParent; 
    public int indexOfAttribute; 
    public ZipNavigator owner; 
    // Für andere Knotentypen: #document, #text 
    public string nonEntryName;  
    // Attributnamen 
    public static string[] atts =  
    {  
      "path",  
      "compressedSize",  
      "uncompressedSize"  
    }; 
    internal ZipState() 
    { 
      this.indexOfCurrentInParent = -1; 
      this.indexOfAttribute = -1; 
      this.nonEntryName = ""; 
    } 
    internal ZipState(Object current, ZipState p,  
      int index, string neName, ZipNavigator nav) 
    { 
      this.currentObject = current; 
      this.parent = p;     
      this.nonEntryName = neName; 
      this.indexOfCurrentInParent = index; 
      this.indexOfAttribute = -1; 
      this.owner = nav; 
    } 
    public ZipState Clone() 
    { 
      ZipState astate = new ZipState(); 
      astate.currentObject = this.currentObject; 
      astate.nonEntryName = this.nonEntryName; 
      astate.parent = this.parent; 
      astate.indexOfCurrentInParent =  
        this.indexOfCurrentInParent; 
      astate.indexOfAttribute = this.indexOfAttribute; 
      astate.owner = this.owner; 
      return astate; 
    } 
    public ZipState OpenChild(int childIndex) 
    { 
      ZipState ast = null; 
      if (IsDocument) 
      { 
        if (childIndex > 0) 
          return null; 
        ast = new ZipState(owner.zip, this, 0,  
          "contents", owner); 
      } 
      else if (IsAttribute) 
      { 
        if (childIndex > 0) 
          return null; 
        ast =  
          new ZipState(null, this, 0, "#text", owner); 
      } 
      else if (childIndex >= 0 && childIndex<ChildCount) 
        ast =  
          new ZipState(null, this, childIndex,"",owner); 
      else return null; 
      return ast; 
    }             
    public string Name  
    {  
      get  
      {  
        if (IsAttribute) 
          return AttributeNames[indexOfAttribute]; 
        else if (IsZipItem) 
        { 
          string name =  
            ((AARONSZIPUTILSLib.ZipReader) 
               owner.zip).GetFileName( 
                 indexOfCurrentInParent).ToLower(); 
          int index = name.LastIndexOf("\\"); 
          if (index >= 0) 
          { 
            string encName =  
              XmlConvert.EncodeLocalName( 
                name.Substring(index+1)); 
            if (encName.Length == 0) 
              return XmlConvert.EncodeLocalName( 
                name.Substring(0, index)); 
            else 
              return encName; 
          } 
          else 
            return XmlConvert.EncodeLocalName(name); 
        } 
        else 
          return nonEntryName; 
      } 
    } 
    public int ChildCount  
    {  
      get  
      { 
        if (IsDocument) 
          return 1; 
        else if (IsDocumentElement) 
          return ((AARONSZIPUTILSLib.ZipReader) 
            owner.zip).GetCount(); 
        else if (IsAttribute) 
          return 1;  
        else if (IsTextNode) 
          return 0; 
        else 
          return 0; 
      } 
    } 
    public string GetAttribute(string name)  
    { 
      if (IsZipItem) 
      { 
        AARONSZIPUTILSLib.ZipReader myzip =  
          (AARONSZIPUTILSLib.ZipReader)owner.zip; 
        switch(name) 
        { 
        case "path": 
          return myzip.GetFileName( 
            indexOfCurrentInParent); 
        case "compressedSize": 
          return myzip.GetCompressedSize( 
            indexOfCurrentInParent).ToString(); 
        case "uncompressedSize": 
          return myzip.GetUncompressedSize( 
            indexOfCurrentInParent).ToString(); 
        default: 
          break; 
        } 
      } 
      return ""; 
    }         
    ...    // Rest der Klasse der Übersichtlichkeit 
           // halber weggelassen. 
}

Listing L4 zeigt einen Teil der ZipNavigator-Implementierung und des Zusammenspiels mit der ZipState-Klasse. Den vollständigen Quelltext finden Sie auf der Begleit-CD.

L4 Die Klasse ZipNavigator

namespace Developmentor.Xml 
{ 
  using System; 
  using System.IO; 
  using System.Xml;     
  using System.Xml.XPath; 
  using System.Collections; 
  using AARONSZIPUTILSLib; 
  public class ZipNavigator : XPathNavigator 
  { 
    private ZipState state; 
    private string zipFileName; 
    private XmlNameTable nt = new NameTable(); 
    public AARONSZIPUTILSLib.ZipReader zip; 
    public ZipNavigator(string zipFileName) 
    { 
      this.zip = new AARONSZIPUTILSLib.ZipReader(); 
      this.zipFileName = zipFileName; 
      this.state =  
        new ZipState(null, null, -1, "#document", this); 
      zip.Open(zipFileName); 
    } 
    private ZipNavigator(ZipState s,  
      AARONSZIPUTILSLib.ZipReader zr) 
    { 
      this.zip = zr; 
      this.state = s; 
    } 
    public override XPathNavigator Clone() 
    { 
      return  
        new ZipNavigator(this.state.Clone(), this.zip); 
    } 
    public override XPathNodeType NodeType 
    {  
      get  
      { 
        if (state.IsDocument) 
          return XPathNodeType.Root; 
        else if (state.IsAttribute) 
          return XPathNodeType.Attribute; 
        else if (state.IsTextNode) 
          return XPathNodeType.Text; 
        else  
          return XPathNodeType.Element; 
      } 
    }    
    public override string LocalName  
    {  
      get { return nt.Add(state.Name); } 
    } 
    public override string NamespaceURI  
    {  
      get { return nt.Add(string.Empty); }  
    } 
    public override string Name  
    {  
      get { return nt.Add(state.Name); } 
    } 
    public override string Prefix  
    {  
      get { return nt.Add(string.Empty); } 
    } 
    public override bool IsEmptyElement 
    {  
      get 
      {  
        if (state.IsAttribute || state.IsTextNode) 
          return false; 
        return !HasChildren; 
      } 
    } 
    public override bool HasAttributes 
    { 
      get { return AttributeCount > 0; } 
    } 
    public override bool HasChildren  
    {  
      get { return (state.ChildCount > 0); } 
    } 
    public override string GetAttribute( 
      string localName, string namespaceURI ) 
    { 
      if (namespaceURI.Equals("")) 
        return state.GetAttribute(localName); 
      else 
        return ""; 
    } 
    private bool UpdateState(ZipState s) 
    { 
      if (s == null) 
        return false; 
      else 
      { 
        state = s; 
        return true; 
      } 
    } 
    public override bool MoveToNext() 
    { 
      if (state.IsAttribute) 
        return false; 
      ZipState p = state.parent; 
      if (p!=null && (IndexInParent+1 < p.ChildCount)) 
      { 
        ZipState newState = p.OpenChild(IndexInParent+1); 
        return UpdateState(newState); 
      } 
      return false; 
    } 
    public override bool MoveToPrevious() 
    { 
      if (state.IsAttribute) 
        return false; 
      ZipState p = state.parent; 
      if (p!=null && (IndexInParent-1 >= 0)) 
      { 
        ZipState newState = p.OpenChild(IndexInParent-1); 
        return UpdateState(newState); 
      } 
      return false; 
    } 
    public override bool MoveToFirstChild() 
    { 
      ZipState newState = state.OpenChild(0); 
      return UpdateState(newState); 
    } 
    public override bool MoveToParent() 
    { 
      if (state.IsAttribute) 
      { 
        state.indexOfAttribute = -1; 
        return true; 
      } 
      if (state.parent != null) 
      { 
        state = state.parent; 
        return true; 
      } 
      return false; 
    } 
    public override void MoveToRoot() 
    { 
      state = new ZipState(null, null, -1, "#document", 
        this); 
    } 
    public override bool MoveTo(XPathNavigator other) 
    { 
      if (other is ZipNavigator) 
      { 
        ZipNavigator asn = (ZipNavigator)other; 
        state = asn.state.Clone(); 
        return true; 
      } 
      return false; 
    } 
    ...    // Rest der Klasse der Übersichtlichkeit 
           // halber weggelassen. 
  } 
}

Etwas aufwendiger ist der Baugruppennavigator namens AssemblyNavigator, der eine .NET-Baugruppe als XML-Dokument anbietet. In diesem Fall richtet sich die XML-Struktur sehr stark nach den Typinformationen, die in der Baugruppe zu finden sind. Das Hauptelement ist der Name der Baugruppe. Es enthält für jedes Modul ein Kindelement. Modulelemente haben für jedes Typkonstrukt ein Kindelement. Typelemente haben für jeden Bestandteil (Member) ein Kindelement, und so weiter. Jedes Element wird durch mehrere Attribute beschrieben. Jedes Element hat ein isa-Attribut, aus dem die Art des Konstrukts hervorgeht, das es darstellt. Bild B7 zeigt zum Beispiel eine einfache Baugruppe, wie sie sich im ILDASM präsentiert, dem Disassembler für die Zwischensprache (intermediate language disassembler). Und Listing L5 zeigt das entsprechende XML-Format, das der AssemblyNavigator anbietet.

Bild07

B7 So erscheint eine Baugruppe im ILDASM.

L5 Eine Baugruppendatei als XML

<person isa="assembly" codebase="file:///C:/Temp/person.dll" 
   location="c:/temp/person.dll"> 
  <person.dll isa="module" qname="c:\temp\person.dll"  
    scopename="person.dll"> 
    <Person isa="type" qname="Example.Person"  
      typetype="class" abstract="False" visibility="public"  
      sealed="False" serializable="False" basetype="Object"  
      underlying="Person"  
      guid="2e3aa5c8-7104-3a9a-ba6f-6aa433101d35"> 
      <ctor isa="constructor" abstract="False"  
        visibility="public" final="False" static="False"  
        virtual="False" callconv="Standard, HasThis" /> 
      <name isa="field" type="String" visibility="public"  
        literal="False" initonly="False"  
        notserialized="False" static="False" /> 
      <age isa="field" type="Double" visibility="public"  
        literal="False" initonly="False"  
        notserialized="False" static="False" /> 
      <GetHashCode isa="method" rettype="Int32"  
        abstract="False" visibility="public" final="False"  
        static="False" virtual="True"  
        callconv="Standard, HasThis" /> 
      <Equals isa="method" rettype="Boolean"  
        abstract="False" visibility="public" final="False"  
        static="False" virtual="True"  
        callconv="Standard, HasThis"> 
        <obj isa="parameter" type="Object" default=""  
          optional="False" in="False" out="False"  
          retval="False" /> 
      </Equals> 
      <ToString isa="method" rettype="String"  
        abstract="False" visibility="public" final="False" 
        static="False" virtual="True"  
        callconv="Standard, HasThis" /> 
      <GetType isa="method" rettype="Type" abstract="False"  
        visibility="public" final="False" static="False"  
        virtual="False" callconv="Standard, HasThis" /> 
    </Person> 
  </person.dll> 
</person>

Das nächste Beispiel ist der Dateisystemnavigator FileSystemNavigator, der das gesamte Dateisystem in Form eines einzigen großen XML-Dokuments anbietet. Die Abbildung des Dateisystems auf XML ist relativ einfach, da die Daten bereits in einer hierarchischen Struktur vorliegen. Das Wurzelelement heißt mycomputer und hat für jedes logische Laufwerk ein Kindelement. Ein logisches Laufwerkselement wiederum hat für jedes Verzeichnis und jede Datei ein Kindelement. Ein Verzeichniselement hat für jedes Unterverzeichnis und jede Datei ein Kindelement, und so weiter. Dateielemente haben keine Kinder. Verzeichnis- und Dateielemente werden durch Attribute ergänzt, mit denen die Dateien und Verzeichnisse näher beschrieben werden. Wie das in XML-Form aussieht, geht aus Listing L6 hervor.

L6 Das Dateisystem als XML.

<mycomputer> 
  <c type="dir" creationTime="5/14/2001 6:45:29 PM"  
    lastAccessTime="5/30/2001 3:28:59 PM"  
    lastWriteTime="5/23/2001 3:03:52 PM" fullName="C:\"> 
    <data type="dir" creationTime="5/15/2001 9:24:54 PM"  
      lastAccessTime="5/30/2001 3:33:16 PM"  
      lastWriteTime="5/30/2001 3:33:02 PM"  
      fullName="C:\data"> 
      <sync.bat type="file"  
        creationTime="5/19/2001 12:22:44 PM"  
        lastAccessTime="5/19/2001 12:22:44 PM"  
        lastWriteTime="9/10/2000 11:08:25 AM"  
        fullName="C:\data\sync.bat" length="19" /> 
      <!-- weitere Verzeichnis/Dateielemente --> 
      ... 
    </data> 
    <!-- weitere Verzeichnis/Dateielemente --> 
    ... 
  </c> 
  <!-- hier folgen weitere logische Laufwerke --> 
</mycomputer>

Neben diesen Beispielen finden Sie auf der Begleit-CD auch einen kleinen Navigator für die Registrierdatenbank von Windows, der ursprünglich von Chris Lovett geschrieben wurde, einem Produktmanager für Web Services bei Microsoft. Aus Platzgründen ist es leider bei keinem dieser Beispiele möglich, den vollständigen Quelltext abzudrucken. Wenn Sie mehr über die Quellen wissen möchten, schauen Sie einfach auf der Begleit-CD nach. Die genannten Beispiele sollten für den Anfang eigentlich ausreichen.
Die praktische Arbeit mit diesen anwenderdefinierten Navigatoren ist auch nicht schwieriger als der Umgang mit dem eingebauten Navigator für DOM-Dokumente. Der Code aus Listing L7 zeigt, wie man einen XPathNavigator durchläuft und ihn auf eine XmlWriter-Implementierung hinausschreibt.

L7 Durchlaufen eines Navigators

public static void SerializeNode(XmlWriter w,  
  XPathNavigator nav) 
{ 
  switch (nav.NodeType) 
  { 
  case XPathNodeType.Element: 
    w.WriteStartElement(nav.Prefix, nav.LocalName,  
      nav.NamespaceURI); 
    if (nav.MoveToFirstAttribute()) 
    { 
      w.WriteStartAttribute(nav.Prefix, nav.LocalName,  
        nav.NamespaceURI); 
      w.WriteString(nav.Value); 
      w.WriteEndAttribute(); 
      while (nav.MoveToNextAttribute()) 
      { 
        w.WriteStartAttribute(nav.Prefix, nav.LocalName,  
          nav.NamespaceURI); 
        w.WriteString(nav.Value); 
        w.WriteEndAttribute(); 
      } 
      nav.MoveToParent(); 
    } 
    if (nav.HasChildren) 
    { 
      bool more = nav.MoveToFirstChild(); 
      while (more) 
      { 
        SerializeNode(w, nav, descendants, attributes); 
        more = nav.MoveToNext(); 
      } 
      nav.MoveToParent(); 
    } 
    w.WriteEndElement(); 
    break; 
  case XPathNodeType.Text: 
    w.WriteString(nav.Value); 
    break; 
  case XPathNodeType.ProcessingInstruction: 
    w.WriteProcessingInstruction(nav.Name, nav.Value); 
    break; 
  case XPathNodeType.Comment: 
    w.WriteComment(nav.Value); 
    break; 
  case XPathNodeType.Whitespace: 
  case XPathNodeType.SignificantWhitespace: 
    // weiße Leerzeichen ignorieren 
    break; 
  } 
}

Wie Listing L8 demonstriert, ist die Methode SerializeNode unabhängig von der speziellen XPathNavigator-Implementierung. Das wiederum ist einer der größten Vorteile, den man durch die Umwandlung der Daten in die XML-Form erreicht. Nun können die Verbraucher so mit den Daten arbeiten, als handele es sich um XML, ohne sich zuvor intensiv um die spezielle Datenstruktur oder um eine mehr oder weniger esoterische Programmierschnittstelle kümmern zu müssen.

L8 Hier werden anwenderdefinierte Navigatoren eingesetzt

public void TraverseAndSerializeNavigators() 
{ 
  // Durchlaufe ein XML-Dokument 
  XmlDocument doc = new XmlDocument(); 
  doc.Load("foo.xml"); 
  XPathNavigator xn =  
    ((IXPathNavigable)doc).CreateNavigator(); 
  SerializeNode(new XmlTextWriter(Console.Out), xn); 
  // Durchlaufe eine ZIP-Datei als XML 
  ZipNavigator zn = new ZipNavigator("fsnav.zip"); 
  SerializeNode(new XmlTextWriter(Console.Out), zn); 
  // Durchlaufe eine .NET-Baugruppe als XML 
  AssemblyNavigator an = new  
    AssemblyNavigator("person.dll"); 
  SerializeNode(new XmlTextWriter(Console.Out), an); 
  // Durchlaufe das Dateisystem als XML 
  FileSystemNavigator fsn = new FileSystemNavigator(); 
  SerializeNode(new XmlTextWriter(Console.Out), fsn); 
  // Durchlaufe die Windows-Registrierdatenbank als XML 
  RegistryNavigator rn = new RegistryNavigator(); 
  SerializeNode(new XmlTextWriter(Console.Out), rn); 
}

XPath-Unterstützung

Die gute Nachricht ist, dass Sie die XPath-Unterstützung ohne zusätzliche Arbeit erhalten, sobald Sie Ihren anwenderdefinierten XPathNavigator implementiert haben. Die XPathNavigator-Basisklasse bietet eine Implementierung der Methode Select an, die einen übergebenen XPath-Ausdruck kompiliert und eine XPathNodeIterator-Referenz zurückgibt. Bei jedem XPathNavigator::MoveNext-Aufruf seitens des Clients ruft die Implementierung zur Bewegung im Baum und zur Suche nach Übereinstimmungen die am weitesten abgeleitete Klasse auf (die Klasse, die Sie von XPathNavigator abgeleitet haben). Bild B8 skizziert diesen Vorgang. Solange Sie die in Listing L1 genannten Bestandteile der Klasse entsprechend implementieren, sollte die eingebaute XPath-Maschine klaglos arbeiten. Die Namensraumachse wurde in der Beta 2 nicht implementiert. Daher werden diese Methoden derzeit nicht von der Select-Standardimplementierung aufgerufen.

Bild08

B8 MoveNext

Falls Ihnen die XPath-Standardmaschine aus irgendeinem Grund nicht gefällt, können Sie eine eigene Maschine zur Bewertung von XPath-Ausdrücken implementieren. Zu diesem Zweck überschreiben Sie die Select-Methode und nehmen Ihre eigene Implementierung von XPathNodeIterator vor. Allerdings ist das keine triviale Arbeit.
Der folgende Code zeigt, wie man einen XPath-Ausdruck auf den anwenderdefinierten FileSystemNavigator anwendet. Dieses Beispiel listet alle abhängigen Elemente des Verzeichnisses C:\temp auf, in deren Namen der Bestandteil "xml" zu finden ist.

public void EvaluateXPathAndDisplayResults() 
{ 
  XmlTextWriter tw = new XmlTextWriter(Console.Out); 
  FileSystemNavigator fsn = new FileSystemNavigator(); 
  XPathNodeIterator sel =  
    fsn.Select("/*/c/temp//*[contains(name(),'xml')]"); 
  while (sel.MoveNext()) 
    SerializeNode(tw, sel.Current); 
}

Die Klasse FileSystemNavigator ermöglicht zudem die Verknüpfung von anwenderdefinierten Navigatoren mit bestimmten Dateinamensendungen (Listing L9). Die Methode RegisterFileHandler erwartet eine Namensendung, den Namen der XPathNavigator-Klasse und den Namen der Baugruppe, in der die Klasse lebt. Wird der FileSystemNavigator dann auf eine Datei positioniert, deren Namensendung mit der registrierten Endung übereinstimmt, kann er eine Instanz der registrierten XPathNavigator-Implementierung anlegen und mit ihrer Hilfe in diese spezielle Datei hineingehen. Dadurch wird auch die Formulierung von XPath-Ausdrücken möglich, mit denen sich die Liste der Dateitypen auswerten lässt, die auf dem System registriert sind.

L9 Anmeldung der Dateihandler

public FileSystemNavigator CreateAndInitialize() 
{ 
  FileSystemNavigator nav = new FileSystemNavigator(); 
  // melde den Standardbearbeiter für XML-Dateien an 
  nav.RegisterFileHandler(".xml",  
    "Developmentor.Xml.XmlDocumentNavigatorFactory",  
    "Developmentor.Xml"); 
  // melde den Standardbearbeiter für ZIP-Dateien an 
  nav.RegisterFileHandler(".zip",  
    "Developmentor.Xml.ZipNavigatorFactory",  
    "Developmentor.Xml"); 
  // melde den Standardbearbeiter für Baugruppen an 
  nav.RegisterFileHandler(".dll",  
    "Developmentor.Xml.AssemblyNavigatorFactory",  
    "Developmentor.Xml"); 
  return nav; 
}

Ich habe ein kleines Beispielprogramm namens fsnav.exe geschrieben, das die Endung .xml mit dem DOM-Standardnavigator aus dem .NET verknüpft, die Endung .dll mit AssemblyNavigator und die Endung .zip mit ZipNavigator. Bild B9 illustriert, wie man fsnav.exe mit XPath-Ausdrücken aufrufen und in die verschiedenen angemeldeten Dateitypen hineingehen kann.

Bild09

B9 So wird das Beispielprogramm fsnav.exe eingesetzt.

XSLT-Unterstützung

Neben der XPath-Unterstützung erhalten Sie mit einem anwenderdefinierten XPathNavigator auch noch die XSLT-Unterstützung frei Haus. Die Klasse XslTransform aus dem .NET Framework erwartet ein XPathNavigator als Quelldokument, das umgewandelt werden soll. Daher können Sie XSLT-Dokumente für das XML-Format Ihres Anbieters schreiben. Aus den folgenden Zeilen geht hervor, wie man mit XslTransform und einer Instanz der AssemblyNavigator-Klasse eine Umwandlung durchführt:

  public void TransformAssembly(string assemblyFile) 
  { 
    // Gib den Namen der zu transformierenden Datei an 
    AssemblyNavigator nav =  
      new AssemblyNavigator(assemblyFile); 
    // XSLT-Dokument 
    XslTransform tx = new XslTransform(); 
    tx.Load("txassembly.xsl"); 
    // Ergebnis in einen Datenstrom schreiben 
    Stream str = new FileStream(@"temp-assembly.htm",  
                                FileMode.Create); 
    tx.Transform(nav, null, str); 
    str.Close(); 
  }

Entwicklung von anwenderdefinierten Lesern

Die Entwicklung eines XmlReaders hat große Ähnlichkeit mit der Entwicklung eines anwenderdefinierten XPathNavigators, beschränkt sich aber auf eine Bewegungsrichtung, nämlich vorwärts durch den Baum, und zwar in der Reihenfolge, die das Dokument vorgibt. (Bild B3 nummeriert die Knoten in Dokumentreihenfolge.) Aus Listing L10 geht hervor, wie man von XmlReader eine neue Klasse ableitet und welche Überschreibungen erforderlich sind.

L10 Eine Vorlage für einen anwenderdefinierten XmlReader

public class MyReader : XmlReader  
{ 
  // dieselben Properties wie bei XPathNavigator 
  public override string LocalName { get{} } 
  public override string NamespaceURI { get{} } 
  public override string Name { get{} } 
  public override string Prefix { get{} }  
  public override XmlNodeType NodeType { get{} } 
  public override string Value { get{} } 
  public override string BaseURI { get{} } 
  public override string XmlLang { get{} } 
  public override bool IsEmptyElement { get{} }  
  public override XmlNameTable NameTable { get{} } 
  public override bool HasAttributes { get{} } 
  // Methoden 
  public override string GetAttribute(string name,  
    string namespaceURI) {} 
  public override bool MoveToFirstAttribute() {} 
  public override bool MoveToNextAttribute() {} 
  public override bool MoveToAttribute(string name,  
    string ns) {} 
  // spezifische XmlReader-Properties 
  public override int AttributeCount { get{} } 
  public override string this[int index] { get{} } 
  public override string this[string name] { get{} } 
  public override string this[string localName,  
    string nsURI] { get{} } 
  public override int Depth { get{} } 
  public override bool HasValue { get{} } 
  public override bool IsDefault { get{} } 
  public override bool EOF { get{} } 
  public override ReadState ReadState { get{} }  
  public override Char QuoteChar { get{} } 
  public override XmlSpace XmlSpace { get{} } 
  // Methoden 
  public override string GetAttribute(int i) {} 
  public override string GetAttribute(string name) {} 
  public override void MoveToAttribute(int index) {} 
  public override bool MoveToAttribute(string name) {} 
  public override bool ReadAttributeValue() {} 
  public override bool MoveToElement() {} 
  public override string LookupNamespace(string prefix){} 
  public override bool Read() {} 
  public override string ReadInnerXml() {} 
  public override string ReadOuterXml() {} 
  public override string ReadString() {} 
  public override void ResolveEntity() {} 
  public override void Close() {} 
}

Ein großer Teil der Bestandteile wurden auch in XPathNavigator definiert. Die zusätzlichen Bestandteile sollen die Arbeit mit dem Stream-Modell und den Attributknoten erleichtern. Es gibt einige zusätzliche Überschreibungen für die Zugriffe auf Attribute, die mit dem qualifizierten Namen, dem unqualifizierten Namen oder einem Index erfolgen. Dadurch hat der Implementierer des Anbieters zwar mehr Arbeit, aber die Kunden von MyReader haben es einfacher. Außerdem gibt es noch zusätzliche Properties, mit denen sich der Zustand des Datenstroms ermitteln lässt (ReadState, EOF und so weiter). Um solche Dinge braucht man sich in XPathNavigator-Implementierungen nicht zu kümmern.
Wie beim XPathNavigator kennt auch der XmlReader das Konzept des Cursors, wobei dieser aber auf die Vorwärtsbewegung beschränkt bleibt. Wird der Cursor auf einen bestimmten Knoten aus dem Datenstrom gesetzt, so lässt sich der neue aktuelle Knoten mit Hilfe der verschiedenen Properties untersuchen.
ReadInnerXml liefert die Knoten, die im aktuellen Knoten liegen, als XML 1.0-String. Handelt es sich beim aktuellen Knoten um einen Elementknoten, verschiebt ReadInnerXml den Cursor auf den entsprechenden EndElement-Knoten. Handelt es sich beim aktuellen Knoten um ein Attribut, verbleibt er auch nach dem Aufruf auf dem Attribut. ReadOuterXml ähnelt ReadInnerXml, verbraucht zudem aber den aktuellen Knoten und setzt den Cursor hinter den dazugehörigen Endknoten (sofern vorhanden). ReadString schließlich liefert den Textinhalt des aktuellen Elements und setzt den Cursor auf den nächsten Nicht-Text-Knoten. Diese Read-Methoden sind die einzigen, die man implementieren muss, damit sich der Cursor durch den Datenstrom bewegen lässt.
Damit sich die Funktionsweise leichter nachvollziehen lässt, habe ich ein kleines XmlReader-Beispiel implementiert, das ich NavigatorReader nenne. Diese Klasse stellt eine generische XmlReader-Implementierung dar, die auf einer XPathNavigator-Implementierung aufsetzt. Es ist eine sehr simple Implementierung, denn sie kann den zugrundeliegenden Navigator nur in Dokumentreihenfolge bewegen. Allerdings muss sie an den entsprechenden Stellen spezielle EndElement-Knoten einfügen. Und so wird sie benutzt:

public void TraverseReader() 
{ 
  // durchlaufe die .NET-Baugruppe als XML 
  AssemblyNavigator an =  
    new AssemblyNavigator("person.dll"); 
  NavigatorReader reader = new NavigatorReader(an); 
  while (reader.Read()) 
    Console.WriteLine("{0}: {1}",  
      reader.NodeType, reader.Name); 
}

NavigatorReader erweist sich als nützlich, wenn die Integration mit dem DOM gefordert ist. Dessen .NET-Implementierung bietet Ladevorgänge nur mit Hilfe einer XmlReader-Referenz an und nicht für einen XPathNavigator. Wenn Sie sich aber für einen XPathNavigator entscheiden, können Sie diese Integration immer mit NavigatorReader erreichen, wie hier gezeigt:

public void LoadDOM() 
{ 
  // durchlaufe die .NET-Baugruppe als XML 
  AssemblyNavigator an =  
    new AssemblyNavigator("person.dll"); 
  NavigatorReader reader = new NavigatorReader(an); 
  XmlDocument doc = new XmlDocument(); 
  doc.Load(reader); 
  // tue etwas mit dem Dokument 
}

Wie schon erwähnt, kann man auch einen XPathNavigator mit einem XmlReader als Grundlage implementieren, solange die XPath-Ausdrücke keine Eltern- oder Vorfahrenachsen benutzen. Zur Vereinfachung ist diese Annahme durchaus akzeptabel, weil sie nun die Bearbeitung von XML-Datenströmen mit Hilfe von XPath ermöglicht, wie in diesem Beispiel:

ReaderNavigator reader = new ReaderNavigator("foo.xml"); 
XPathNodeIterator iterator =  
  reader.Select("//foo[@bar='xyz']"); 
while (iterator.MoveNext()) { 
  // bearbeite den aktuellen Knoten 
}

Mark Fussel, Programmmanager des .NET XML Frameworks, hat solch ein Beispiel zusammengestellt. Es heißt XPathReader. Sie finden es auf der Begleit-CD.

Die Welt als XML-Dokument

Wie etliche begeisterte OLE DB-Entwickler beginnen nun auch viele XML-Entwickler, die Welt durch eine Art Infoset-farbiger Brille zu betrachten. Der Infoset macht den Zugriff auf die verschiedenen proprietären Datenquellen für die Verbraucher nicht nur geradezu trivial, sondern ermöglicht auch die Integration mit einer ausgefeilten XML-bezogenen Infrastruktur, die sich immer noch mit einer erstaunlichen Geschwindigkeit weiterentwickelt. Kommt es auf eine möglichst hohe Systemleistung an, lassen sich die Probleme mit anwenderdefinierten XML-Infosetanbietern lösen, die einen geradlinigeren Zugang zu den Schnittstellen ermöglichen.
Die XML-Architektur von .NET vereinfacht die Entwicklung von schreibgeschützten Infosetanbietern (read-only), die sich in den Rest des Grundgerüsts einfügen lassen. Außerdem bietet sie mit XPathNavigator und XmlReader zwei Infoset-Repräsentationen an, deren unterschiedliche Stärken sich je nach der Art der zugrundeliegenden Datenquellen zeigen. Die Anbieterbeispiele, die in diesem Artikel besprochen wurden, sollten Ihnen den Weg zur Entwicklung eigener XML-Anbieter für .NET ebnen.