Aktivieren von Anwendungen mit Kurztasten

Veröffentlicht: 15. Dez 2001 | Aktualisiert : 09. Nov 2004

Von Aaron Skonnard

Microsoft .NET führt eine ganze Reihe neuer XML-Schnittstellen ein. Am interessantesten sind die Klassen, die auf XMLReader und XMLWriter basieren. Sie stellen Funktionen zur Verfügung, die man früher nur mit Mühe selbst programmiert hat.

Auf dieser Seite

XML als Technologiesubstrat von .NET XML als Technologiesubstrat von .NET
Abstrakte Basisklassen Abstrakte Basisklassen
XmlReader XmlReader
Untersuchung des aktuellen Knotens Untersuchung des aktuellen Knotens
Einlesen der Knoten Einlesen der Knoten
Expansion von Entity-Referenzen Expansion von Entity-Referenzen
Attribute Attribute
XmlTextReader XmlTextReader
Alternative Leser Alternative Leser
XmlWriter XmlWriter
XmlTextWriter XmlTextWriter
DOM-Implementierung DOM-Implementierung
Navigation und XmlNavigator Navigation und XmlNavigator
DocumentNavigator DocumentNavigator
Transformationen und Xsl Transform Transformationen und Xsl Transform
Wo stehen wir? Wo stehen wir?

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

sys

In diesem Artikel möchte ich Ihnen eine neue Gruppe der Schnittstellenfunktionen vom .NET vorstellen, die man .NET-XML-Klassen nennt. Im folgenden gehe ich davon aus, dass Sie bereits über .NET-Grundkenntnisse verfügen und auch C#-Code lesen können, denn die Beispiele wurden in C# geschrieben. Außerdem komme ich wohl nicht um das übliche Cave Canem herum: dieser Artikel beruht auf der .NET Beta 1. Bis zur endgültigen Version kann sich im Prinzip also noch alles ändern.

Die derzeit wichtigsten Anwendungsbereiche von .NET haben immer in der einen oder anderen Weise mit XML zu tun. Daher wurde ein beträchtlicher Teil der Zeit in die Entwicklung von brauchbaren XML-Funktionen gesteckt. Die .NET-XML-Klassen beruhen auf wichtigen Industriestandards wie DOM Level 2 Core, XPath 1.0, XSLT 1.0, XML Schemas (XSD) und dem Simple Object Access Protocol (SOAP). Außerdem führen die Klassen einige neue Erweiterungen ein, mit denen das Programmiermodell insgesamt verbessert werden soll. Neben dem klassischen DOM-Modell führen die .NET-XML-Klassen auch eine innovative Schnittstelle auf Stream-Basis ein, die einem Pull-Modell folgt, im Gegensatz zum traditionellen Push-Modell von SAX. Neben dieser wohl wichtigsten Änderung am MSXML 3.0-Modell gibt es noch verschiedene andere Erweiterungen, die zur Leistungssteigerung und zur Verbesserung der Bedienbarkeit dienen.

Im wesentlichen stellen die .NET-XML-Klassen keinen revolutionären Quantensprung dar, sondern so etwas wie die natürliche Weiterentwicklung von MSXML 3.0 für die .NET-Architektur. Das neue Grundgerüst wurde zwar nach MSXML 3.0 modelliert, hat aber einige Verbesserungen vorzuweisen, darunter eine bessere Übereinstimmung mit den Standards, erweiterbare Schnittstellen und ein einfacheres Programmiermodell. Und das alles ohne Einbußen bei der Geschwindigkeit.

MSXML 3.0 ist heute verfügbar und wird weiterhin ein Eckstein der Entwicklung von COM-Anwendungen bleiben, in denen mit XML gearbeitet wird (zum Beispiel für den Internet Explorer, Anwendungen mit dynamischem HTML, Office, Visual Basic 6.0 und so weiter). Bis das .NET fertig ausgeliefert wird, bleibt das für Produktionscode sowieso die bessere Alternative.

Die Entwickler können sogar in der .NET-Welt des "verwalteten Codes" mit MSXML 3.0 weiterarbeiten, wenn sie auf die Interop-Dienste aus dem .NET zurückgreifen. Um im .NET mit MSXML 3.0 arbeiten zu können, müssen Sie zuerst eine Baugruppe (assembly) für MSXML 3.0-Typen anlegen. Zu diesem Zweck gibt es im SDK ein Hilfsprogramm namens tlbimp. Anschließend importieren Sie den MSXML-Namensraum und verweisen beim Kompilieren auf die erzeugte Baugruppe (mit /reference oder kurz mit /r). Das folgende Codefragment soll verdeutlichen, wie man die MSXML 3.0-Klasse DOMDocument in einer C#-Klasse einsetzen kann:

using MSXML; 
DOMDocument30 doc = new DOMDocument30(); 
doc.async = false; 
if (doc.load("somefile.xml")) 
{ 
  // bearbeite hier das Dokument 
}

Der wichtigste Grund für den Rückgriff auf solche Konstruktionen dürfte wohl die Portierung von vorhandenem Code auf .NET bei minimalen Codeänderungen sein. Vielleicht haben Sie auch größere Investitionen in die ältere XSL-Sprache vorgenommen, die nur von MSXML angeboten wird.

XML als Technologiesubstrat von .NET

XML ist ein entscheidender Bestandteil von .NET. Alle anderen Teile des .NET-Grundgerüsts (ASP.NET, Webdienste und so weiter) benutzen XML als Datenformat. Die .NET-XML-Klassen sind ebenso eng mit dem Managed Data Access verkoppelt (ADO.NET). .NET bietet damit für alle Datentypen ein gemeinsames Programmiermodell an.

Dieses Programmiermodell beruht vollständig auf den .NET- XML-Klassen. Dadurch wird es zum Beispiel möglich, eine relationale ADO.NET-Datenmenge anzufordern (wie einen ADO-Recordset) und sie via DOM auszuwerten, mit XPath in den Daten zu navigieren oder die Daten abzufragen und sie mit XSLT zu konvertieren. Und mit XML Schemas lassen sich Datasets auch in Dokumente oder Datenströme serialisieren. Während XML in ADO 2.x wie ein nachträglicher Einfall wirkt, stellt es in ADO.NET den zentralen Kern dar. Weitere Informationen über ADO.NET (bisher bekannt als ADO+) und seiner XML-Unterstützung finden Sie in Omri Gazitts Artikel "Eine Einführung in ADO+" [1].

Im folgenden möchte ich mich nun auf die Komponenten beschränken, aus denen sich die .NET-XML-Klassen zusammensetzen. (Für Beschreibungen der Technologien XPath, XSLT und XML Schemas siehe die Literaturliste am Ende des Artikels).

Die .NET-XML-Klassen erstrecken sich über mehrere Namensräume. Die Kerntypen liegen im Namensraum System.Xml. Die XPath-/XSLT-spezifischen Typen sind in den Namensräumen System.Xml.Xpath und System.Xml.Xsl zu finden (Bild B1). Diese drei Namensräume wurden alle der Baugruppe System.Xml.dll zugeordnet. Um also in C# mit den .NET-XML-Klassen arbeiten zu können, müssen Sie mit der using-Direktive die richtigen Namensräume importieren:

using System.Xml; // nun können Sie diese Typen benutzen

Wenn Sie das Projekt kompilieren, müssen Sie die entsprechende Baugruppendatei einbinden, in der die Metadaten für diese Typen zu finden sind. In diesem Fall handelt es sich um die Datei System.Xml.dll. Der Befehl für die Kommandozeilenversion des Compilers könnte zum Beispiel so aussehen:

csc /debug+ /r:System.Xml.dll helloxml.cs

Abstrakte Basisklassen

Im Zentrum der .NET-XML-Klassen liegen zwei abstrakte Klassen, nämlich XmlReader und XmlWriter. XmlReader benutzt zur Bearbeitung eines XML-Dokumentenstroms einen schnellen Vorwärts-Cursor mit Lesezugriff. XmlWriter bietet eine Schnittstelle für die Erzeugung von XML-Dokumentenströmen an, die den XML 1.0 + Namespaces Recommendations des W3C entsprechen.

Anwendungen, die XML-Dokumente bearbeiten möchten, treten als Verbraucher von XmlReader auf, während Anwendungen, die XML-Dokumente erzeugen möchten, Verbraucher von XmlWriter sind. Beide Klassen implizieren ein Datenstrommodell, das ohne teuren speicherinternen Cache auskommt. Das macht beide Klassen zu attraktiven Alternativen zur klassischen DOM-Lösung.

XmlReader und XmlWriter sind beides abstrakte Basisklassen, in denen die Funktionalität definiert wird, die in allen abgeleiteten Klassen verfügbar sein muss. Von XmlReader gibt es drei konkrete Implementierungen, nämlich XmlTextReader, XmlNodeReader und XslReader. Von XmlWriter gibt es die beiden konkreten Implementierungen XmlTextWriter und XmlNodeWriter. XmlNodeWriter gibt es noch nicht in der Beta 1, sollte aber in einer zukünftigen Version zu finden sein.

XmlTextReader und XmlTextWriter sind für Schreib- und Lesezugriffe auf Textdatenströme gedacht, während XmlNodeReader und XmlNodeWriter für die Arbeit an speicherinternen DOM-Bäumen vorgesehen sind (Bild B2). Zu den größten Vorteilen dieses neuen Designs gehört der Umstand, dass man nun zur Erweiterung der vorgefertigten Funktionalität anwenderdefinierte Leser und Schreiber entwickeln kann.

Bevor ich auf die Einzelheiten eingehe, möchte ich Ihnen ein kleines Beispiel dafür zeigen, wie man XML-Dokumente mit XmlReader und XmlWriter bearbeitet. Die folgenden Zeilen durchlaufen den Dokumentenstrom mit XmlReader und zeigen den Namen jedes Knotens an, der auf dem Weg liegt (ohne Attributknoten):

public void ReadDocument(XmlReader reader) 
{ 
  // lies den nächsten Knoten in Dokumentreihenfolge 
  while (reader.Read()) 
  { 
    // Ermittle den Namespace und Namen des aktuellen Knotens 
    String sNodeName = (reader.NodeType == XmlNodeType.Element  
         || reader.NodeType == XmlNodeType.EndElement)  
      ? reader.NamespaceURI + "#" + reader.Name  
      : reader.Name; 
    Console.WriteLine(sNodeName); 
  } 
}

Der folgende Code demonstriert, wie man mit der XmlWriter-Klasse einen XML-Dokumentenstrom generieren kann:

public void WriteDocument(XmlWriter writer) 
{ 
  writer.WriteStartDocument(); 
  writer.WriteComment("generated by SampleWriter"); 
  writer.WriteProcessingInstruction("hack", "on person"); 
  writer.WriteStartElement("p", "person", "urn:person"); 
  writer.WriteStartElement("name", ""); 
  writer.WriteString("joebob"); 
  writer.WriteEndElement(); 
  writer.WriteElementInt16("age", "", 28); 
  writer.WriteEndElement(); 
  writer.WriteEndDocument(); 
}

Dieses spezielle Codefragment generiert ein XML-Dokument, in folgendem Format:

<?xml version="1.0"?> 
<!--sample person document-->  
<?hack on person?> 
<p:person xmlns:p="urn:person"> 
  <name>joebob</name> 
  <age unit="year">28</age> 
</p:person>

Beginnen wir unsere Untersuchung mit XmlReader, gefolgt vom XmlWriter. Anschließend möchte ich noch auf die Programmierschnittstellen für DOM Level 2 Core, XPath und XSLT eingehen.

XmlReader

Die Klasse XmlReader benutzt für die Lesezugriffe auf XML-Dokumente einen schnellen Vorwärts-Cursor. Sie verbirgt die Komplexität im Umgang mit den zugrundeliegenden Daten, indem sie den Infoset des Dokuments über wohldefinierte Methoden anbietet. Auf den ersten Blick sieht XmlReader ziemlich nach SAX aus (besonders ContentHandler), das ebenfalls auf Dokumentströmen basiert, aber die beiden Programmiermodelle unterscheiden sich sehr stark.

Im Jahre 1998, als XML zur W3C-Empfehlung (Recommendation) wurde, etwas später im selbem Jahr gefolgt von DOM Level 1, merkten die Entwickler sehr schnell, dass DOM nicht die Ansprüche jeder Anwendung erfüllen kann, vor allem beim Umgang mit sehr großen Dokumenten. Also wurde SAX entwickelt, damit ein ressourcenschonenderes Programmiermodell zur Verfügung steht. SAX-Leser sind für das Einlesen der Zeichenströme zuständig und für die Weitergabe des Infosets vom Dokument an die Verbraucheranwendung.

Die Übergabe erfolgt über Schnittstellen, die von der Verbraucheranwendung bei SAX registriert werden. Dieses Übergabemodell (Push-Modell) hat gegenüber DOM einige Vorteile, aber auch seine eigenen Komplexitäten, von denen sich die meisten um die Zustandsverwaltung drehen.

Die XmlReader-Klasse stellt einen Kompromiss zwischen den DOM- und den SAX-Funktionen dar. Sie bietet wie SAX ein Strommodell an, das aber mit einem einfacheren Programmiermodell, ähnlich wie DOM, verbunden ist. XmlReader ließe sich mit einem Vorwärts-/nur Lese-Cursor vergleichen, mit dem sich die Anwender die Datensätze einzeln abholen und uninteressante Datensätze überspringen können.

Das Pull-Modell vom XmlReader hat gegenüber dem Push-Modell von SAX einige Vorteile (Bild B3). Zuerst und vor allem ist es leichter zu benutzen. Es ist für Entwickler nun mal wesentlich einfacher, in while-Schleifen zu denken, als sich mit komplexen Zustandsmaschinen herumzuschlagen. Obwohl die Zustandsverwaltung auch im Pull-Modell so etwas wie eine Herausforderung bleibt, können die Entwickler nun prozedurale Methoden dafür einsetzen, die ihnen einfach geläufiger sind.

Das Pull-Modell kann zudem auf verschiedene Weisen noch die Leistung optimieren. XmlReader können zum Beispiel Hinweise auswerten, die sie von den Clients erhalten, um die Zeichenpuffer effizienter auszunutzen und eventuell überflüssige String-Kopien zu vermeiden. Außerdem können Verbraucher selektive Arbeiten durchführen (nicht erforderliche Elemente überspringen, bestimmte Entitäten nicht expandieren und so weiter). Beim Push-Modell muss alles durch die Anwendung durchgeschleust werden, weil der Leser einfach nicht wissen kann, was wichtig ist und was nicht.

Letztlich bietet das Pull-Modell nicht nur ein bekannteres Programmiermodell an, sondern hat auch einige Leistungsvorteile zu bieten. Wenn Sie trotzdem noch begeisterter Anhänger des Push-Modells sind, können Sie das Pull-Modell vom XmlReader durch eine zusätzliche Softwareschicht mit Push-Schnittstellen überdecken. Umgekehrt geht es aber nicht. Vielleicht wird es im .NET Framework SDK eine SAX2-Beispielimplementierung geben, die auf dem XmlReader aufsetzt.

Untersuchung des aktuellen Knotens

Der Cursor aus dem Pull-Modell vom XmlReader kennt so etwas wie das Konzept des aktuellen Knotens im Dokument-Datenstrom. Der aktuelle Knoten ändert sich, wenn man eine der vielen anderen Methoden aufruft (darauf komme ich gleich noch zurück). Der XmlReader bietet eine ganze Reihe von Properties an, mit denen man den Zustand des aktuellen Knotens untersuchen kann. Das Namensproperty liefert zum Beispiel den QName des Knotens, sofern vorhanden, während LocalName und NamespaceURI die mit Namensraum qualifizierte Identität des Knotens nennen. NodeType liefert eine Typkennung ähnlich denen, die im DOM benutzt werden. Wenn der aktuelle Knoten einen Wert hat (Textknoten, Attribute und so weiter), können Sie mit dem Value-Property darauf zugreifen. Und wenn es sich beim aktuellen Knoten um ein Element handelt, können Sie mit den Properties HasAttributes oder AttributesCount überprüfen, ob es Attribute hat. Das folgende Codefragment gibt ein Beispiel dafür, wie diese Properties benutzt werden:

public static void InspectCurrentNode(XmlReader reader) 
{ 
  String sQName = reader.Name; 
  String sLocalName = reader.LocalName; 
  String sNSURI = reader.NamespaceURI; 
  XmlNodeType type = reader.NodeType; 
  String sValue = reader.Value; 
  boolean bAtts = reader.HasAttributes; 
  int nAtts = reader.AttributeCount; 
  ... 
  // tu etwas mit den Daten 
}

Einlesen der Knoten

Für die Bewegung des Cursors durch den Dokument-Datenstrom gibt es eine ganze Reihe von Methoden, von denen Read die wichtigste ist. Die Read-Methode geht bei jedem Aufruf um einen Knoten im Strom weiter, und zwar in Dokumentreihenfolge. Sollte es Probleme mit der Wohlgeformtheit des Dokuments geben, wird eine XmlException gemeldet.

public void TraverseInDocumentOrder(XmlReader reader) 
{ 
  try { 
    while (reader.Read()) 
        Console.WriteLine("{0}", reader.NodeType); 
  } 
  catch(XmlException e) { 
    Console.WriteLine("###error: " + e.Message); 
  } 
}

Als Dokumentreihenfolge wird die Reihenfolge definiert, in welcher die Strukturelemente im serialisierten Dokument auftauchen. Anders gesagt, der Gang durch die logische Struktur des Dokuments erfolgt zuerst in die Tiefe (Bild B4).

Setzt man das gerade gezeigte Codefragment auf das folgende XML-Dokument an, produziert der Code die Ergebnisse, die im Listing L1 erscheinen.

<?xml version="1.0"?> 
<!--sample person document-->  
<?hack on person?> 
  <name>joebob</name> 
  <age unit="year">28</age> 
</p:person>

Nach der Initialisierung vom XmlReader gibt es noch keinen aktuellen Knoten. Der erste Aufruf von Read legt die aktuelle Position also auf den ersten Knoten im Dokument. Wenn der XmlReader das Ende des Dokuments erreicht, verlässt er den letzten Knoten nicht, was ihn in einen undefinierte Zustand versetzen würde. Statt dessen gibt er einfach false zurück, sobald es keine Knoten mehr zu bearbeiten gibt.

L1 Die Ausgabe des Beispielcodes auf der Konsole

[XmlDeclaration] 
[Whitespace]  
[Comment] 
[Whitespace] 
[ProcessingInstruction] 
[Whitespace] 
[Element] 
[Whitespace]  
[Element]  
[Text] 
[EndTag]   
[Whitespace]  
[Element]  
[Text] 
[EndTag]  
[Whitespace]  
[EndTag] 

Der XmlReader bietet noch einige andere Read-Methoden an, mit denen sich auch Kontextüberprüfungen durchführen lassen. Wenn Sie zum Beispiel sicherstellen möchten, dass es sich beim aktuellen Knoten um ein Element mit einem bestimmten Namen handelt, bevor Sie fortfahren, können Sie ReadStartElement einsetzen:

try 
{ 
  // wenn der aktuelle Knoten nicht den Namen "foo" trägt 
  // und nicht den Namensraum-URI "urn:bar" hat, wird eine 
  // Ausnahme gemeldet 
  reader.ReadStartElement("foo", "urn:bar"); 
} 
catch(XmlException e) 
{ 
  Console.WriteLine(e.Message); 
}

Zum Einlesen von typisierten Textwerten gibt es im XmlReader eine ganze Reihe von Methoden (ReadInt16, ReadInt32, ReadDouble, ReadString und so weiter). Diese Methoden geben dem Leser einen Hinweis darauf, welcher Typ als nächstes erwartet wird (dadurch werden einige interne Optimierungen im Parser möglich). Statt eines Strings liefern diese Methoden natürlich einen typisierten Wert. So zeigt der folgende Code zum Beispiel, wie man alle Price-Elemente eines Dokuments aufsummieren kann:

// addiere alle Price-Elemente 
double dTotal = 0.0; 
while (reader.Read()) 
{ 
  if (reader.NodeType == XmlNodeType.Element) 
  { 
    if (reader.LocalName.Equals("Price")) 
      dTotal += reader.ReadDouble(); 
  } 
} 
Console.WriteLine(dTotal);  

Read trifft nicht auf Attributknoten, weil diese nicht als Bestandteil der hierarchischen Dokumentstruktur betrachtet werden. Attribute werden als Metadaten angesehen, die mit den Strukturelementen verknüpft sind. Wenn es sich beim aktuellen Knoten um ein Element handelt, sind dessen Attribute mit GetAttribute zugänglich, sei es nun über den Namen oder über den Index. Das folgende Beispiel zählt die Attribute eines Elements anhand ihrer Indizes auf:

public static void ReadAttributes(XmlReader reader) 
{ 
  while (reader.Read()) 
  { 
    if (reader.NodeType == XmlNodeType.Element) 
      // bearbeite alle Attribute eines Elements 
      for (int i=0; i<reader.AttributeCount; i++) 
        Console.WriteLine(reader.GetAttribute(i)); 
  } 
}

Listing L2 verdeutlicht, wie sich der gesamte Dokument-Datenstrom mit Read verarbeiten lässt. Es ist das unvermeidliche Beispiel, das jeden Knoten einliest und wieder als XML 1.0 auf die Konsole serialisiert.

L2 Einlesen oder Serialisieren eines Dokuments.

using System; 
using System.Xml; 
// (Rest der Klasse der Übersichtlichkeit halber weggelassen) 
public void ReadDocumentAndSerialize(XmlReader reader) 
{ 
    // lies jeden Knoten aus dem Baum ein 
    while (reader.Read()) 
    { 
    // serialisiere die Knoten auf die Konsole  
    // entsprechend XML 1.0 + Names 
    switch (reader.NodeType) 
    { 
      case XmlNodeType.Element: 
        Console.Write("<" + reader.Name); 
        while (reader.MoveToNextAttribute()) 
         Console.Write(" " + reader.Name + "='" + reader.Value + "'"); 
        Console.Write(">"); 
        break; 
      case XmlNodeType.Text: 
        Console.Write(reader.Value); 
        break; 
      case XmlNodeType.CDATA: 
        Console.Write(reader.Value); 
        break; 
      case XmlNodeType.ProcessingInstruction: 
        Console.Write("<?" + reader.Name + " " + reader.Value + "?>"); 
        break; 
      case XmlNodeType.Comment: 
        Console.Write(">!--" + reader.Value + "-->"); 
        break; 
      case XmlNodeType.Document: 
        Console.Write("<?xml version='1.0'?>"); 
        break; 
      case XmlNodeType.Whitespace: 
        Console.Write(reader.Value); 
        break; 
      case XmlNodeType.SignificantWhitespace: 
        Console.Write(reader.Value); 
        break; 
      case XmlNodeType.EndTag: 
        Console.Write("</" + reader.Name + ">"); 
        break; 
    } 
    } 
}

Expansion von Entity-Referenzen

Beim Einlesen eines Dokumentstroms ist es möglich, die Expandierung der Entitätsknoten zu optimieren. Sobald der XmlReader auf einen Knoten des Typs EntityReference trifft, ist es Sache des Verbrauchers, mit dem expliziten Aufruf der Methode ResolveEntity zu entscheiden, ob eine Expandierung erforderlich ist. Sofern der Verbraucher auf den ResolveEntity-Aufruf verzichtet und mit dem nächsten Read-Aufruf fortfährt, wird der Ersatzinhalt der Entität einfach übersprungen. Ruft der Verbraucher aber ResolveEntity auf, so wird der Ersatzinhalt wie alles andere verarbeitet.

Außerdem fügt der XmlReader an passender Stelle einen EndEntity-Knoten in den Strom ein, damit der Verbraucher das Ende des Ersatzinhaltes erkennen kann. Das folgende Beispiel setzt diese Details in Code um (unter der Annahme, dass EntityHandling nur auf ExpandCharEntitites gesetzt wurde):

while (reader.Read()) 
{ 
  if (reader.NodeType == XmlNodeType.Element) 
    Console.WriteLine(reader.Name); 
  if (reader.NodeType == XmlNodeType.EntityReference) 
  { 
    Console.WriteLine("*** resolve entity called ***"); 
    // Im Anschluss an diesen Aufruf geht der Leser über 
    // den Inhalt der Entität 
    reader.ResolveEntity(); 
  } 
  if (reader.NodeType == XmlNodeType.EndEntity) 
    Console.WriteLine("*** end of current entity ***"); 
}

Dieser Code soll nun auf folgendes XML-Dokument angesetzt werden:

  <!DOCTYPE person [ 
    <!ENTITY fl "<first>Bob</first><last>Smith</last>"> 
  ]> 
  <person> 
    <name>&fl;</name> 
    <age>35</age> 
  </person>

Das Ergebnis sieht auf der Konsole dann so aus:

  person 
  name 
  *** resolve entity called *** 
  first 
  last 
  *** end of entity *** 
  age

Attribute

Neben den sequentiellen Lesevorgängen bietet der XmlReader auch verschiedene Methoden an, mit denen sich bestimmte Knoten aus dem Dokumentstrom ansprechen lassen. Sofern Sie gerade auf einem Elementknoten positioniert sind, können Sie mit MoveToAttribute, MoveToFirstAttribute und MoveToNextAttribute seitwärts durch die anhängenden Attributknoten gehen. (Natürlich können Sie auch zur bereits erwähnten Methode GetAttribute greifen, aber diese liefert nur Attributwerte.) Umgekehrt können Sie mit MoveToElement zurück zum Besitzer gehen, falls Sie gerade auf einem Attribut positioniert sind, also zum dazugehörigen Elementknoten.

Falls Sie auf einem Knoten positioniert sind, der keinen Inhalt anbietet (wie Leerzeichen, Kommentare und Bearbeitungsbefehle) und nun sozusagen mit der Strömung zum nächsten Inhaltsknoten schwimmen möchten (Element, Text und so weiter), können Sie das mit der praktischen Methode MoveToContent tun. Die folgenden Zeilen demonstrieren, wie das geht:

public static void MoveToFirstContentNode(XmlReader reader) 
{ 
  if (reader.Read()) 
  { 
    Console.WriteLine(reader.Name); 
    // überspringt alle nicht-Inhalt-Knoten (Kommentare, Leerzeichen) 
    reader.MoveToContent();  
    Console.WriteLine(reader.Name); 
  } 
}

Schauen Sie sich das folgende Dokument an. Es enthält einige Knoten, die nichts zum Inhalt beitragen:

  <?skip me?> 
  <!--skip me too--> 
  <?and me too...?> 
  <foo> 
    <bar/> 
  </foo>

Die Bearbeitung dieses Dokuments mit dem obigen Codebeispiel führt zu folgendem Ergebnis:
skip foo

XmlTextReader

Alle bisher gezeigten Beispiele wurden für den XmlReader geschrieben. Aber da es sich beim XmlReader eigentlich um eine abstrakte Klasse handelt, dürfte es sinnvoll sein, einige der konkreten Klassen einzuführen, die von ihr abgeleitet werden. XmlTextReader ist solch eine Klasse. Sie zerlegt einen Dokumentenstrom auf Textbasis in die einzelnen Bestandteile. XmlTextReader hat mehrere überladene Konstruktoren, die verschiedene Arten von Eingabe-Datenströmen akzeptieren, darunter URI-Strings, Stream- und TextReader-Typen. XmlTextReader bietet einige zusätzliche Leistungen an, zum Beispiel für die Validierung und für die anwenderdefinierte Auflösung von externen Entitäten.

XmlTextReader unterstützt die Validierung gegen DTDs, XML Data-Reduced (XDR) und XSD-Schemas (XSD wird in der Beta 1 zwar noch nicht angeboten, soll aber in einer zukünftigen Version unterstützt werden). Die Verbraucher haben über das Validation-Property Einfluss darauf, wie der Leser die Validierung vornimmt. Im Normalzustand erkennt XmlTextReader DTDs und Schemas für die Bearbeitung von Entitäten und Standard-Attributwerten automatisch. Um die Validierung zu ermöglichen, müssen Sie einen ValidationHandler zur Verfügung stellen. Auf Validation.Schema gesetzt, erkennt der Leser automatisch, ob ein XDR oder ein XSD-Schema benutzt wird (es wird als Fehler angesehen, wenn ein Dokument beides benutzt).

Der ValidationHandler ist eine Funktion zur Bearbeitung bestimmter Ereignisse, die der Leser aufruft, sobald er auf Validierungsfehler stößt. Diese Callback-Methode wird über das Ereignis-Property ValidationEventHandler mit dem Leser verknüpft. Listing L3 zeigt eine Konsolenanwendung, die das angegebene Dokument anhand einer DTD oder eines XML-Schemas überprüft.

L3 XmlTextReader und Validierung

using System; 
using System.Xml; 
public class ValidateUtility 
{ 
  static bool m_error = false; 
  public static void Main(String[] args) 
  { 
    if (args.Length < 1) 
    { 
      Console.WriteLine( 
          "usage: validate filename [-dtd, -schema, -none]"); 
      return; 
    } 
    try 
    { 
      // lege eine Instanz von XmlTextReader an 
      XmlTextReader tr = new XmlTextReader(args[0]); 
      // lege die Art der Validierung fest 
      if (args.Length > 1) 
      { 
        if (args[1].Equals("-dtd")) 
          tr.Validation = Validation.DTD; 
        else if (args[1].Equals("-schema")) 
          tr.Validation = Validation.Schema; 
        else if (args[1].Equals("-none")) 
          tr.Validation = Validation.None; 
        else 
          tr.Validation = Validation.Auto; 
      } 
      // Melde die Rückruffunktion an, die für Validierungsfehler 
      // zuständig ist. 
      tr.ValidationEventHandler += new  
          ValidationEventHandler(ValidationCallback); 
      // Lies das gesamte Dokument ein 
      while (tr.Read())  
        ; 
      if (!m_error) 
        Console.WriteLine("success: document is valid"); 
    } 
    catch(XmlException e) 
    { 
      Console.WriteLine("###error: " + e.Message); 
    } 
    return; 
  } 
  public static void ValidationCallback (Object obj,  
      ValidationEventArgs args) 
  { 
     m_error = true; 
     Console.Write("\r\n\t###validation error: " + args.Message); 
  } 
}

Durch die anwenderdefinierte Auflösung von externen Entitäten wird eine Leistungsoptimierung möglich, indem die Anwendung die wiederverwendeten Ressourcen (wie DTDs oder Schemas) in einen Cache stellt und somit weitere Ladeverzögerungen vermeidet. Anwendungen, die solch eine anwenderdefinierte Auflösung durchführen möchten, müssen eine Klasse implementieren, die von XmlResolver abgeleitet wird (siehe GetEntity-Methode). Dann können Sie eine Instanz dieser Klasse über das XmlResolver-Property mit dem XmlTextReader verknüpfen. Die folgenden Zeilen skizzieren eine anwenderdefinierte Implementierung von XmlResolver:

public class MyCustomResolver : XmlUrlResolver 
{ 
  public override object GetEntity(string baseUri, string relativeUri, 
    string role, Type t, out string resolvedUri) 
  { 
    resolvedUri = DoMyCustomResolution(baseUri, relativeUri); 
    return GetResource(resolvedUri); 
  } 
}

Listing L4 zeigt neben einigen anderen XmlTextReader-Properties auch, wie diese Klasse MyCustomerResolver eingesetzt wird.

L4 Konfigurierung von XmlTextReader

using System; 
using System.Xml; 
public static void Main(String[] args) 
{ 
  try 
  { 
    // Dateiname wird in args[0] übergeben 
    XmlTextReader tr = new XmlTextReader(args[0]); 
    // anwenderdefinierter Entity-Auflöser 
    tr.XmlResolver = new MyResolver(); 
    // keine Leerzeichen erwähnen 
    tr.WhitespaceHandling = WhitespaceHandling.None; 
    // Optimiere Entity-Expandierung 
    // muss für Expansion ResolveEntity aufrufen 
    tr.EntityHandling = EntityHandling.AllEntityNodes; 
    // benutzte dtd/schema für Entity-Expansion & Standardattribute 
    // validiere aber nicht 
    tr.Validation = Validation.Auto; 
    // lies das Dokument ein 
    while (tr.Read()) 
    { 
      // bearbeite hier die Knoten 
    } 
  } 
  catch(XmlException e) 
  { 
    Console.WriteLine("###error: " + e.Message); 
  } 
}

Alternative Leser

XmlTextReader ist zwar die übliche XmlReader-Implementierung, aber es sind noch viele andere Arten von Lesern möglich, darunter auch anwenderdefinierte Implementierungen. XmlNodeReader ist ein weiteres Beispiel für eine Implementierung. Diese Klasse liest einen DOM-Baum ein, der bereits im Speicher liegt, also keinen Zeichenstrom (Bild B2). Das Ergebnis von XslTransform.Transform ist ein weiterer XmlReader, der das Ergebnis der XSLT-Transformation Knoten für Knoten liefert. Es ist auch möglich, eigene XmlReader-Implementierungen vorzunehmen und auf diese Weise die Funktionalität der vorhandenen Klassen zu erweitern oder mit anwendungsspezifischen Datentypen zu arbeiten, die nichts mit XML-Datentypen zu tun haben.

Schauen wir uns einmal an einer XInclude-fähigen XmlReader-Klasse an, wie das funktioniert. XInclude definiert ein globales Attribut namens href, das zum Namensraum http://www.w3.org/1999/XML/xinclude gehört. Wann immer ein Element auftaucht, in dem es das XInclude-Attribut href gibt, sollte es durch den Infoset des angesprochenen Dokuments ersetzt werden. Listing L5 zeigt den Anfang solch einer Implementierung.

L5 Anwenderdefinierter XmlReader zur Bearbeitung von XIncludes

// Autor: Aaron Skonnard 
// http://staff.develop.com/aarons 
using System; 
using System.Xml; 
public class XincUtility 
{ 
  public static void Main(String[] args) 
  { 
    try 
    { 
      // Lege eine Instanz unseres anwenderdefinierten XmlReaders 
      // (XincReader) an und konfiguriere sie 
      XincReader r = new XincReader(args[0]); 
      r.WhitespaceHandling = WhitespaceHandling.None; 
      XmlTextWriter tw = new XmlTextWriter(Console.Out); 
      tw.Formatting = Formatting.Indented; 
      tw.WriteStartDocument(); 
      while (r.Read()) 
        SerializeNode(r, tw); 
      tw.WriteEndDocument(); 
    } 
    catch(XmlException e) 
    { 
      Console.WriteLine("###error: " + e.Message); 
    } 
    return; 
  } 
  public static void SerializeNode(XmlReader reader, XmlWriter w) 
  { 
    switch (reader.NodeType) 
    { 
      case XmlNodeType.Element: 
        w.WriteStartElement(reader.Prefix, reader.LocalName,  
            reader.NamespaceURI); 
        while (reader.MoveToNextAttribute()) 
        { 
          w.WriteStartAttribute(reader.Prefix, reader.LocalName,  
              reader.NamespaceURI); 
          w.WriteString(reader.Value); 
          w.WriteEndAttribute(); 
        } 
        break; 
      case XmlNodeType.Text: 
        w.WriteString(reader.Value); 
        break; 
      case XmlNodeType.CDATA: 
        w.WriteCData(reader.Value); 
        break; 
      case XmlNodeType.ProcessingInstruction: 
        w.WriteProcessingInstruction(reader.Name, reader.Value); 
        break; 
      case XmlNodeType.Comment: 
        w.WriteComment(reader.Value); 
        break; 
      case XmlNodeType.EndTag: 
        w.WriteEndElement(); 
        break; 
    } 
  } 
} 
public class XincReader : XmlTextReader 
{ 
  // geschachtelter Leser zur Bearbeitung der XInclude-Elemente 
  XincReader m_NestedReader; 
  public XincReader(String sURI) : base(sURI) {m_NestedReader=null;} 
  // anwenderdefinierte Implementierung von Read: delegiere, falls es 
  // einen geschachtelten Leser gibt. Lies andernfalls den nächsten  
  // Knoten und prüfe auf xinc:href. Ist es vorhanden, lege einen 
  // weiteren geschachtelten Leser an und fahre fort. 
  public override bool Read() 
  { 
    bool bMore; 
    String strHref = null; 
    // delegiere, falls es einen geschachtelten Leser gibt 
    if (m_NestedReader != null) 
    { 
      bMore = m_NestedReader.Read(); 
      // gib die Ressourcen wieder ab und setze den Zustand zurück, 
      // falls der geschachtelte Leser nicht mehr gebraucht wird 
      if (!bMore) 
      { 
        m_NestedReader.Close(); 
        m_NestedReader = null; 
        return base.Read(); 
      } 
      else return true; 
    } 
    else 
    { 
      // lies den nächsten Knoten ein und teste auf xinc:href 
      bMore = base.Read(); 
      strHref = base.GetAttribute("href",  
          "http://www.w3.org/1999/XML/xinclude"); 
      // gefunden? Dann lege einen neuen geschachtelten Leser an und 
      // gehe auf den ersten Knoten 
      if (strHref != null) 
      { 
        m_NestedReader = new XincReader(strHref); 
        m_NestedReader.Read(); 
        // gehe hinter XmlDeclaration, sofern vorhanden 
        if (m_NestedReader.NodeType == XmlNodeType.XmlDeclaration) 
          m_NestedReader.Read(); 
        return true; 
      } 
      else 
        return bMore; 
    } 
  } 
  // delegiere an den geschachtelten Leser, sofern vorhanden 
  // (überschreibe alle XmlTextReader-Methoden) 
  public string Name 
  { 
    override get 
    { 
      if (m_NestedReader != null) 
        return m_NestedReader.Name; 
      else 
        return base.Name; 
    } 
  } 
  public XmlNodeType NodeType 
  { 
    override get 
    { 
      if (m_NestedReader != null) 
        return m_NestedReader.NodeType; 
      else 
        return base.NodeType; 
    } 
  } 
  public string NamespaceURI 
  { 
    override get 
    { 
      if (m_NestedReader != null) 
        return m_NestedReader.NamespaceURI; 
      else 
        return base.NamespaceURI; 
    } 
  } 
  public string LocalName 
  { 
    override get 
    { 
      if (m_NestedReader != null) 
        return m_NestedReader.LocalName; 
      else 
        return base.LocalName; 
    } 
  } 
  public string Prefix 
  { 
    override get 
    { 
      if (m_NestedReader != null) 
        return m_NestedReader.Prefix; 
      else 
        return base.Prefix; 
    } 
  } 
  public string Value 
  { 
    override get 
    { 
      if (m_NestedReader != null) 
        return m_NestedReader.Value; 
      else 
        return base.Value; 
    } 
  } 
  public int AttributeCount 
  { 
    override get 
    { 
      if (m_NestedReader != null) 
        return m_NestedReader.AttributeCount; 
      else 
        return base.AttributeCount; 
    } 
  } 
  public override bool MoveToNextAttribute() 
  { 
    if (m_NestedReader != null) 
      return m_NestedReader.MoveToNextAttribute(); 
    else 
      return base.MoveToNextAttribute(); 
  } 
  // Rest der Übersichtlichkeit halber gestrichen... 
}

Diese Implementierung führt die Infosets zusammen, die von den XInclude-Elementen angesprochen werden (rekursiv über alle eingefügten Dokumente). Die folgenden Dokumente enthalten zum Bespiel eine XInclude-Beziehung:

<!-- person-xinc.xml --> 
<person xmlns:xinc="http://www.w3.org/1999/XML/xinclude"> 
  <fullname-ref xinc:href="fullname.xml"/> 
</person> 
>!-- fullname.xml --> 
<name> 
  <first>Joe</first> 
  <last>Bob</last> 
</name>

Wird nun person-xinc.xml von dieser XmlReader-Implementierung bearbeitet, erzeugt sie einen Dokumentstrom, der sich folgendermaßen serialisieren ließe:

<person> 
  <name> 
    <first>Joe</first> 
    <last>Bob</last> 
  </name> 
</person>

Es gibt für anwenderdefinierte Leser noch viele andere Anwendungsbereiche, besonders dann, wenn man anwendungsspezifische (binäre) Daten einlesen und anderen Anwendungen als XML-Daten anbieten muss.

XmlWriter

Wie XmlReader ist auch XmlWriter eine abstrakte Klasse. Sie definiert die Basisfunktionalität, die zur Erzeugung von Dokument-Datenströmen nach den XML 1.0 + Namespaces Recommendations vom W3C erforderlich ist. Wie schon gezeigt, schirmt XmlWriter die Anwendungen vollständig von der Komplexität ab, die mit der Erzeugung von XML-Dokumentenströmen verbunden ist, und ermöglicht den Anwendungen die Arbeit mit einer Schnittstelle auf Infoset-Basis. Die Erzeugung eines Dokuments mit XmlWriter lässt sich mit der Dokumenterzeugung via SAX vergleichen (schauen Sie sich einmal die SAX XmlWriter-Hilfsklasse an). Derzeit gibt es zwei XmlWriter-Implementierungen, nämlich XmlTextWriter und XmlNodeWriter (nicht in der Beta 1; er wird in einer zukünftigen Version verfügbar sein). Diese Implementierungen ähneln den Leserversionen, arbeiten aber in die entgegengesetzte Richtung.

XmlWriter ermöglicht es, die ganzen Standardkonstrukte eines Infosets (wie Elemente, Attribute und Bearbeitungsbefehle) durch entsprechende WriteXxx-Methoden herauszuschreiben, wobei Xxx für den Namen des betreffenden Konstrukts steht. Wenn Sie sich das XmlWriter-Beispiel aus dem zweiten Abschnitt dieses Artikels anschauen, werden Sie darin einige dieser Methoden in Aktion sehen (WriteStartDocument, WriteStartElement, WriteProcessingInstruction und so weiter).

Der XmlWriter bietet mit den WriteElementXxx-Methoden und den WriteAttributeXxx-Methoden auch Methoden zur Ausgabe von typisierten Elementen und Attributen an. Außerdem gibt es mit den entsprechenden WriteXxx-Methoden ein vergleichbares Bündel von Methoden für die Ausgabe von typisierten Textwerten. Mit WriteNode können Sie sogar den aktuellen Knoten in einem XmlReader herausschreiben (mit oder ohne Attribute). Das folgende Beispiel zeigt, wie sich das Serialisierungsbeispiel aus Listing L2 mit WriteNode vereinfachen ließe:

public static void Serialize(XmlReader r, XmlWriter w) 
{ 
  while (r.Read()) 
    w.WriteNode(r, true); 
}

XmlTextWriter

XmlTextWriter ist eine konkrete Klasse, die von XmlWriter abgeleitet wird und ihre Texte in Zeichenströme herauschreibt. Sie kann mit verschienen Stream-Typen umgehen (Datei-URI, Stream und TextWriter) und ist weitgehend konfigurierbar. Sie können zum Beispiel angeben, ob Sie mit Namensräumen arbeiten möchten, welche Optionen es zur Einrückung gibt oder welches Anführungszeichen für Attributwerte benutzt wird. Sie können sogar festlegen, welche lexikalische Repräsentation für typisierte Werte benutzt werden soll (WriteElementBool und so weiter), abhängig vom benutzten Schema-Namensraum (siehe Property DataTypeNamespace). Der folgende Code zeigt die Konfiguration mit einigen dieser Properties:

public static void Main(String[] args) 
{ 
  XmlTextWriter w = new XmlTextWriter(Console.Out); 
  // schreibe Namensraumnamen 
  w.Namespaces = true; 
  w.EncodeNames = true; 
  // Ausgabe einrücken ("Schöndruck") 
  w.Formatting = Formatting.Indented; 
  w.Indentation = 4; 
  // Benutze ' für Attributwerte 
  w.QuoteChar = "'"; 
  WriteDocument(w); 
}

Obwohl XmlTextWriter der gebräuchlichste XmlWriter ist, sind auch noch andere XmlWriter-Implementierungen möglich, zum Beispiel XmlNodeWriter. Wie XmlNodeReader schreibt dieser Schreiber einen XML-Dokumentenstrom in einen speicherinternen DOM-Baum (Bild B2).

DOM-Implementierung

Die DOM-Implementierung im .NET (System.Xml.XmlDocument) bietet DOM Level 1 und DOM Level 2 Core, allerdings mit einigen kleineren Namensänderungen. DOM-Ladevorgänge bauen auf dem XmlReader auf, während die DOM-Serialisierung über XmlWriter abgewickelt wird. Daraus ergeben sich zahlreiche Wege, auf denen man das Zusammenspiel zwischen DOM und Ihren Anwendungen erweitern könnte.

Die DOM-Schnittstelle vom .NET ähnelt dem, was man vom MSXML 3.0 her kennt, wenn auch mit einigen kleinen Änderungen. Bild B5 skizziert die DOM-Klassenhierarchie, die im .NET gilt. Die erste kleine Änderung zeigt sich in der Einführung der beiden neuen Klassen XmlWhitespace und XmlSignificantWhitespace, mit denen sich die Leerzeichen in einem XML-Dokument modellieren lassen. Leerzeichen werden als unbedeutend erachtet, solange sie nur der Lesbarkeit halber in einem Inhaltsmodell auftauchen (der Parser erkennt das bei der Untersuchung des DTD/Schemas). XmlLinkedNode wird von XmlNode abgeleitet und stellt die Basisklasse für alle Strukturelemente des Dokuments dar.

Die Klasse XmlDocument entspricht der MSXML-Coklasse DOMDocument, mit der die Informationseinheiten des Dokuments modelliert werden. XmlDocument hat einige überladene Load-Methoden, mit denen sich der DOM-Baum aus einem Eingabe-Datenstrom ableiten lässt. Load erwartet einen Dokumenten-URI, einen TextReader oder eine XmlReader-Referenz. Aus den folgenden Zeilen geht hervor, wie sich ein Dokument per URI laden lässt:

XmlDocument doc = new XmlDocument(); 
doc.Load("http://staff.develop.com/aarons/xmlstuff.xml");

Die Methode LoadXML erwartet keine Stream-Kennung, sondern einen XML-String. Im MSXML 3.0 gibt es zwar auch noch einen asynchronen Lademechanismus, aber der ist im .NET nicht mehr erforderlich, weil die Verbraucher im .NET via XmlReader oder XmlWriter die vollständige Kontrolle darüber haben, wie die DOM-Bäume aufgebaut werden.

Die programmgesteuerte Erstellung von DOM-Dokumenten funktioniert wie im MSXML 3.0. XmlDocument bietet für jede DOM-Knotenart eine Erzeugermethode an (CreateElement, CreateAttribute und so weiter). Listing L6 demonstriert, wie sich das bereits gezeigte Personen-Beispieldokument generieren lässt.

L6 Erstellung eines Dokuments mit dem DOM

using System; 
using System.Xml; 
public class GenerateDocument 
{ 
  public static void Main(String[] args) 
  { 
    // lege eine Dokumentinstanz an 
    XmlDocument doc = new XmlDocument(); 
    XmlNode node; 
    // lege Kommentar und Befehl an, anhängen 
    node = doc.CreateComment("sample person document"); 
    doc.AppendChild(node); 
    node = doc.CreateProcessingInstruction("hack", "on person"); 
    doc.AppendChild(node); 
    // lege das Element person im Namensraum 'urn:person' an 
    node = doc.CreateElement("p", "person", "urn:person"); 
    doc.AppendChild(node); 
    // lege die Elemente name und age außerhalb der Namensräume an 
    node = doc.CreateElement("name"); 
    node.InnerText = "joebob"; 
    doc.DocumentElement.AppendChild(node); 
    node = doc.CreateElement("age"); 
    node.InnerText = "28"; 
    doc.DocumentElement.AppendChild(node); 
    // serialisiere das Dokument auf die Konsole 
    XmlTextWriter tw = new XmlTextWriter(Console.Out); 
    tw.Formatting = Formatting.Indented; 
    doc.Save(tw); 
  } 
}

Der Code im Listing L6 benutzt auch die Save-Methode von XmlDocument, um den DOM-Baum wieder herauszuserialisieren (natürlich mit Einrückung!), wie es die XML 1.0 + Namespaces-Spezifikation ermöglicht. Es gibt sogar noch mehr überladene Save-Methoden, die einen URI erwarten, einen TextWriter oder eine XmlWriter-Referenz.

Listing L6 benutzt zur Serialisierung auf der Konsole eine Instanz von XmlTextWriter. Es gibt auch noch einige andere Methoden wie WriteTo und WriteContentTo, mit denen sich der aktuelle Knoten (und sein Unterbaum) auf dem betreffenden XmlWriter serialisieren lassen. Außerdem gibt es noch zwei an DHTML angelehnte Properties mit den Namen InnerXml und OuterXml, die einen XML-String für den aktuellen Knoten zurückliefern. Anders als im MSXML lässt sich das Property InnerXml setzen, so dass sich die Unterbäume mit einer einfachen Stringzuweisung bilden lassen.

Die wichtigsten Methoden für die Navigation im DOM-Baum sind in XmlNode zu finden, darunter ChildNodes, FirstChild, LastChild, ParentNode, NextSibling und PreviousSibling. Die Navigation im Dokument kann auch mit einer separaten Klasse namens XmlNavigator erfolgen, die neben einem allgemeinen Navigationsmechanismus auch eine Implementierung der XPath 1.0-Empfehlung aufzuweisen hat.

Wie XmlReader und XmlWriter ist auch XmlNavigator eine abstrakte Basisklasse, in der die Funktionalität definiert wird, die in allen Navigatoren erforderlich ist. XmlNavigator bietet allgemeine Navigationsroutinen, die Auswahl von Knoten, die Aufzählung einer Auswahl und auch komplexere Arbeiten mit der Auswahl (kopieren, verschieben, löschen und so weiter).

Der XmlNavigator ist gegenüber der üblichen DOM-Schnittstelle vorzuziehen. Typische XmlNavigator-Implementierungen brauchen nicht die gesamte Dokumentstruktur in den Speicher zu laden, wie es die meisten DOM-Implementierungen tun. Folglich ist der XmlNavigator die ressourcenschonendere und auch leistungsfähigere Alternative. Er scheint auch leichter zu durchschauende Schnittstellen zu haben, besonders bei der Arbeit mit XPath-Ausdrücken.

In der Klasse XmlNavigator gibt es zwei Methoden zur Auswahl von Knotenmengen, nämlich Select und SelectSingle, die den Methoden selectNodes und selectSingleNode von MSXML 3.0 entsprechen. Beide Methoden akzeptieren einen XPath-Ausdruck in Form eines Strings oder eines vorkompilierten Ausdrucksobjekts. Der Ausdruck wird zur Identifizierung einer Knotenmenge benutzt:

public static void EvaluateXPath(XmlNavigator nav) 
{ 
  // gibt eine Knotenmenge zurück 
  nav.Select("//Price/text()"); 
  while (nav.MoveToNextSelected()) 
  { 
    // bearbeite hier die ausgewählten Knoten 
  } 
}

Falls ein Ausdruck wiederholt benutzt werden soll, können Sie die Geschwindigkeit verbessern, indem Sie den Ausdruck in ein Objekt kompilieren, das in einem Cache aufbewahrt wird und wiederverwendet werden kann:

Object cachedExpr = null; 
cachedExpr = nav.Compile("//Invoice[@id>1000]//Price[.>10]"); 
// der Ausdruck lässt sich ohne erneute Kosten für die  
// Kompilierung wiederverwenden 
nav.Select(cachedExpr); 
while (nav.MoveToNextSelected()) 
{ 
  // bearbeite hier die ausgewählten Knoten 
}

Nach dem Select-Aufruf können Sie die ausgewählten Knoten mit der Methode MoveToNextSelected durchlaufen.

Zum Kopieren, Verschieben und Löschen der gewählten Knoten gibt es weitere Methoden. So zeigt das folgende Codefragment zum Beispiel, wie man die ausgewählten Knoten von XmlNavigator zu XmlNavigator kopieren kann:

public static void EvaluateXPath(XmlNavigator in, XmlNavigator out) 
{ 
  // liefert eine Knotenmenge 
  in.Select("//Price"); 
  out.CopySelected(TreePosition.FirstChild, in); 
  // nun enthält der out-Navigator die ausgewählten Knoten 
}

Mir gefällt die neue Methode Evaluate ausgesprochen gut. Sie bewertet XPath-Ausdrücke, die andere XPath-Typen als node-set liefern (string, number oder Boolean). So summieren die folgenden Zeilen zum Beispiel mit Hilfe eines XPath-Ausdrucks alle Price-Elemente eines Dokuments auf:

public static void SumPriceNodes(XmlNavigator nav) { 
  // in diesem Fall liefert evaluate eine number 
  Console.WriteLine("sum=" + nav.Evaluate("sum(//Price)")); 
}

Ohne diese Methode müsste man alle Price-Elemente auswählen, die Elemente der Ergebnismenge aufzählen und die Berechnungen von Hand ausführen. Listing L7 zeigt die Implementierung eines kleinen Konsolenprogramms, mit dem man XPath-Ausdrücke bewerten kann.

L7 Ein Konsolenprogramm zur Bewertung von XPath-Ausdrücken

// Autor: Aaron Skonnard 
// http://staff.develop.com/aarons 
using System; 
using System.Xml; 
class XPathUtility 
{ 
  public static void Main(String[] args) 
  { 
    if (args.Length < 2) 
    { 
      System.Console.WriteLine 
          ("usage: xpath.exe filename xpath-expr [-eval]"); 
    } 
    else 
    { 
      try 
      { 
        // lade XML-Dokument mit Eingabefeld 
        XmlDocument doc = new XmlDocument(); 
        doc.Load(args[0]); 
        // verpacke es in einem XmlNavigator 
        DocumentNavigator nav = new DocumentNavigator(doc); 
        // Wenn -eval, rufe evaluate auf und liefer das  
        // Ergebnis ab 
        if (args.Length == 3 && args[2].Equals("-eval")) 
        { 
          Console.WriteLine(nav.Evaluate(args[1])); 
          return; 
        } 
        // andernfalls rufe Select auf 
        nav.Select(args[1]); 
        // lege einen XmlTextWriter für die Konsole an 
        XmlTextWriter xw = new XmlTextWriter(Console.Out); 
        xw.Formatting = Formatting.Indented; 
        // lege ein neues Dokument für die gewählten Knoten an 
        XmlDocument docoutput = new XmlDocument(); 
        XmlElement e = docoutput.CreateElement("selection"); 
        docoutput.AppendChild(e); 
        DocumentNavigator navoutput = new DocumentNavigator( 
                                                  docoutput); 
        navoutput.MoveToDocumentElement(); 
        // kopiere die ausgewählten Knoten ins Ausgabedokument 
        navoutput.CopySelected(TreePosition.FirstChild, nav); 
        // serialisiere auf die Konsole 
        docoutput.Save(xw); 
      } 
      catch(Exception e) 
      { 
        System.Console.WriteLine("###error: " + e.Message); 
      } 
    } 
  } 
}

DocumentNavigator

Von XmlNavigator leitet sich die konkrete Klasse DocumentNavigator ab, die speziell zur Navigation in DOM-Bäumen entwickelt wurde. Wenn Sie eine Instanz von DocumentNavigator anlegen, übergeben Sie einfach eine Referenz auf das XmlDocument, in dem Sie navigieren möchten.

XmlDocument doc = new XmlDocument(); 
doc.Load("person.xml"); 
DocumentNavigator nav = new DocumentNavigator(doc); 
nav.Select("/person/name"); 
while (nav.MoveToNextSelected()) 
{ 
  // bearbeite hier die Auswahl... 
}

Während Sie mit XmlNavigator im Dokument herumklettern, haben Sie über die Properties des jeweils aktuellen Knotens Zugriff auf die entsprechenden Informationen. Falls Sie eine vollständige DOM-Ansicht des aktuellen Knotens brauchen, beschaffen Sie sich einfach mit der Methode GetNode eine Referenz auf das aktuelle XmlNode-Objekt.

Dank dieser erweiterbaren Architektur lassen sich auch noch andere Navigatoren implementieren. So gibt es zum Beispiel eine Klasse DataDocumentNavigator, mit der die Navigation in einem ADO.NET-Dataset möglich ist, als sei es ein XML-Dokument. Außerdem kursieren Gerüchte über einen Navigator für die Registrierdatenbank. (Anwenderdefinierte Implementierungen von XmlNavigator erhalten die XPath-Unterstützung praktisch geschenkt.)

Transformationen und Xsl Transform

Die Klasse XslTransform ist im neuen Grundgerüst für die Verwaltung der XSLT-Transformationen zuständig. XslTransform lebt im Namensraum System.Xml.Xsl und greift während der Transformation auf XmlNavigator zurück. Wie alle XSLT-Prozessoren erwartet XslTransform ein XML-Dokument, ein XSLT-Dokument und einige optionale Angaben. Sie kann jede Art von Textausgabe erzeugen und ja, man kann das Umwandlungsergebnis mit einem anwenderdefinierten XmlReader einlesen.

Zur Durchführung einer Umwandlung mit XslTransform legen Sie zuerst ein XslTransform-Objekt an und laden es durch den Aufruf von Load mit dem gewünschten XSLT-Dokument. Anschließend legen Sie ein XmlNavigator-Objekt an und initialisieren es mit dem XML-Quelldokument, das umgewandelt werden soll. Und dann leiten Sie den Vorgang mit dem Aufruf von Transform ein (Listing L8).

L8 Umwandlung eines Dokuments mit XslTransform

// Autor: Aaron Skonnard 
// http://staff.develop.com/aarons 
using System; 
using System.Xml; 
using System.Xml.Xsl; 
public class TransformationUtility 
{ 
  public static void transformDocument(String strXML,  
      String strXSLT, XmlWriter xw) 
  { 
    try 
    { 
      // lade das umzuwandelnde Quelldokument 
      XmlDocument docSource = new XmlDocument(); 
      docSource.Load(strXML); 
      // verpacke es in einem XmlNavigator 
      DocumentNavigator navSource = new DocumentNavigator(docSource); 
      // lege das XslTransform-Objekt an 
      XslTransform tr = new XslTransform(); 
      // lade es mit der Formatvorlage (stylesheet) 
      tr.Load(strXSLT); 
      // rufe transform auf - das Ergebnis strömt auf die Konsole... 
      tr.Transform(navSource, null, xw); 
    } 
    catch(XmlException e) 
    { 
      Console.WriteLine("###error: " + e.Message); 
    } 
  } 
  public static void Main(String[] args) 
  { 
    if (args.Length < 2) 
    { 
      Console.WriteLine("usage: xslt sourcedoc xsltdoc [outdoc]"); 
      return; 
    } 
    if (args.Length == 3) 
    { 
      // lege einen XmlTextWriter für die Ausgabe in eine Datei an 
      XmlTextWriter tw = new XmlTextWriter(args[2], null); 
      transformDocument(args[0], args[1], tw); 
      return; 
    } 
    // andernfalls schicke das Ergebnis einfach auf die Konsole 
    transformDocument(args[0], args[1],  
                      new XmlTextWriter(Console.Out)); 
  } 
}

Wo stehen wir?

Ich konnte in diesem Artikel zwar auf die Kerntypen eingehen, die es in den neuen XML-Klassen vom .NET-Grundgerüst gibt, mit XmlReader und XmlWriter als wichtigste Vertreter, aber letztlich war hier nur von der Spitze des Eisbergs die Rede. Die XML-Story von .NET hat noch viele andere erzählenswerte Geschichten zu bieten, darunter die XML-Integration in ADO.NET, die XML-Serialisierung, das XSD-Objektmodell, Sicherheitsaspekte und digitale XML-Signaturen, die XML-Integration mit ASP.NET, die Webdienste und so weiter und so fort. In den nächsten Folgen des System Journals werden Sie noch mehr über diese Themen lesen können.

Literatur
[1] Omri Gazitt: "Eine Einführung in ADO+", System Journal 02/2001, S. 18
[2] Aaron Skonnard: "XML für Ein- und Umsteiger", System Journal 05/2000, S. 33
[3] Don Box, Aaron Skonnard, John Lam: "Eine Einführung in XSLT", System Journal 06/2000, S. 30
[4] Aaron Skonnard: "Infosets auffinden mit Xpath", System Journal 06/2000, S. 60
[5] Aaron Skonnard: "Das ist neu in MSXML 3.0", System Journal 01/2001, S. 86
[6] Aaron Skonnard: "Nutzen Sie das Simple API for XML", System Journal 02/2001, S. 52

Aaron Skonnard ist Trainer und Entwickler bei DevelopMentor, wo er den XML-Kurs entwickelt. Er ist Co-Autor von "Essential XML" (Addison-Wesley) und hat das Buch "Essential WinInet" geschrieben. Er ist unter http://staff.develop.com/aarons erreichbar.