## 16.3 Die Klasse »XmlReader« ### 16.3.1 XML-Dokumente mit einem »XmlReader«-Objekt lesen Das .NET Framework bietet mehrere Möglichkeiten, um die von einem XML-Dokument beschriebenen Daten zu lesen. In diesem Abschnitt wollen wir uns mit der einfachsten Variante beschäftigen, nämlich dem Einlesen über ein XmlReader-Objekt. Ein Objekt vom Typ XmlReader liest die Elemente eines XML-Dokuments einzeln und der Reihe nach einmal ein. Die gelesenen Daten sind schreibgeschützt und bleiben nicht im Arbeitsspeicher. Das mag ein Nachteil sein, hat aber auch zur Folge, dass die Speicherressourcen geschont werden und der Lesevorgang schnell ist. Damit bietet sich die Klasse XmlReader besonders an, wenn es ausreicht, ein XML-Dokument nur sequenziell zu lesen, und Sie nicht innerhalb des Dokuments nach bestimmten Daten suchen wollen. Die Klasse ist als abstract definiert und kann demnach nicht instanziiert werden. Allerdings bietet die Klasse mit der statischen Methode Create die Möglichkeit, sich eine Instanz zu besorgen.
    XmlReader reader = XmlReader.Create(@"MeineDaten.xml");
Beachten Sie bitte, dass Sie den Namespace System.Xml mit using bekannt geben sollten.
Anmerkung
Im .NET Framework 1.x übernahm die Klasse XmlTextReader die Aufgabe, ein XML-Dokument einzulesen. Seit der Veröffentlichung von .NET 2.0 wird empfohlen, dafür die Klasse XmlReader und deren Methode Create einzusetzen, da XmlReader mehr Möglichkeiten bietet als XmlTextReader.
Nachdem Sie das XmlReader-Objekt erzeugt haben, rufen Sie in einer Schleife die Methode Read auf, die alle Knoten im XML-Dokument nacheinander abruft. Der Rückgabewert der Methode ist true, wenn noch mindestens ein weiterer Knoten eingelesen werden kann.
    while(reader.Read())
    {
      // weitere Anweisungen
    }
In der Schleife muss zuerst untersucht werden, welcher Knoten aktuell gelesen wird. Hier interessieren vorrangig Typ und Name des Knotens. Den Typ des Knotens rufen Sie mit der Eigenschaft NodeType ab, die durch einen Wert der Enumeration XmlNodeType beschrieben wird. Tabelle 16.2 beschreibt einen Auszug aus dieser Enumeration.

Tabelle 16.2 Auszug aus der Enumeration »XmlNodeType«

Bezeichner | Beschreibung
Element | Beschreibt ein Element, z. B. <Artikel>.
Attribute | Beschreibt ein Attribut.
Text | Beschreibt den Textinhalt eines Knotens.
CDATA | Beschreibt einen CDATA-Abschnitt.
ProcessingInstruction | Beschreibt eine Verarbeitungsanweisung, z. B. <?pi Testanweisung?>.
Comment | Beschreibt einen Kommentar, z. B. <!-- Ich bin ein Kommentar -->.
Whitespace | Beschreibt Leerraum zwischen Markup.
EndElement | Beschreibt ein Endelement-Tag, z. B. </Artikel>.
XmlDeclaration | Beschreibt die XML-Deklaration, z. B. <?xml version='1.0'?>.
Die Struktur einer Schleife könnte also wie folgt codiert sein:
    while (reader.Read()) {
      switch (reader.NodeType) {
        case XmlNodeType.XmlDeclaration:
          // Anweisungen
          break;
        case XmlNodeType.CDATA:
          // Anweisungen
          break;
        ...
      }
    }
Der Untersuchung des Knotentyps hat eine besondere Bedeutung hinsichtlich der Auswertung, denn je nach Typ möchten Sie entweder den Knotenbezeichner oder den von einem Knoten beschriebenen Inhalt auswerten. Dazu dienen die beiden Eigenschaften Name und Value des XmlReader-Objekts. Mit Value rufen Sie den Inhalt des aktuellen Knotens ab. Von den in Tabelle 16.2 angeführten Knotentypen können nur Attribute, CDATA, Comment, ProcessingInstruction, Text, Whitespace und XmlDeclaration einen Wert zurückgeben. Alle anderen Knoten liefern String.Empty. Die Eigenschaft Name hingegen liefert den Bezeichner des aktuellen Knotens. Auch diese Eigenschaft ist auf bestimmte Knotentypen beschränkt. Dazu gehören Attribute, Element, EndElement, ProcessingInstruction und XmlDeclaration. #### Attribute eines Elements auswerten Eine spezielle Behandlung erfahren die Attribute eines Knotens vom Typ XmlNodeType.Element. Eigentlich würde man erwarten, dass ein Attribut als Knoten erkannt wird und dass das XmlReader-Objekt sich bei jedem Aufruf von Read auch von Attribut zu Attribut hangelt. Tatsächlich aber liest der XmlReader ein XmlNodeType.Element samt dessen Attributen komplett ein, sodass die Attribute beim nächsten Aufruf der Read-Methode nicht mehr im Datenstrom enthalten sind. Sie haben dann zwar den Inhalt des Elements ausgewertet, aber die Attribute unberücksichtigt gelassen. Ist nicht exakt bekannt, ob und wie viele Attribute ein Element hat, hilft Ihnen die Eigenschaft HasAttributes des XmlReader-Objekts weiter, die true oder false zurückliefert. Diese Eigenschaft wird aufgerufen, wenn es sich bei dem aktuellen Knoten um den Typ XmlNodeType.Element handelt. Mit der Eigenschaft AttributeCount lässt sich die Anzahl der Attribute in Erfahrung bringen. Den zurückgelieferten Wert können Sie in einer for-Schleife verwenden. Die Methode GetAttribute unter Angabe entweder des Index oder des Attributbezeichners liefert den Wert des Attributs.
    ...
    case XmlNodeType.Element:
      if (reader.HasAttributes) {
        for (int i = 0; i < reader.AttributeCount; i++) {
          Console.WriteLine(reader.GetAttribute(i));
        }
      }
      break;
    ...
Alternativ können Sie auch mit der Methode MoveToNextAttribute durch die Liste der Attribute navigieren. Ähnlich der Read-Methode des XmlReader-Objekts liefert auch diese Methode true zurück, falls das Element noch ein weiteres Attribut enthält.
    ...
    case XmlNodeType.Element:
      if (reader.HasAttributes) {
        while (reader.MoveToNextAttribute()) {
          // Anweisungen
        }
      }
      break;
    ...
#### Beispielprogramm Wir wollen uns nun die bisherigen Erkenntnisse in einem kompletten Beispielprogramm ansehen. Dazu brauchen wir eine XML-Datei. Um nicht nur die XML-Datei, sondern auch deren Analyse und Ausgabe nicht zu aufwendig zu gestalten, wird mit dem Dokument nur eine Person mit den wichtigsten Eckdaten beschrieben.
    <?xml version="1.0" encoding="utf-8" ?>
    <!-- Liste von Personen-->
    <Personen>
      <Person>
        <Befehl><![CDATA[Ich stehe unter CDATA]]></Befehl>
        <Vorname>Manfred</Vorname>
        <Telefon/>
        <Zuname>Fischer</Zuname>
        <Alter>45</Alter>
        <Adresse Ort="Bonn" Strasse="Neuestr.34"></Adresse>
      </Person>
    </Personen>
Auf eine genaue Erklärung des folgenden Codings werde ich an dieser Stelle verzichten. Er erhält ausschließlich Passagen, die bereits zuvor erläutert worden sind. Sie sollten den Code jedoch einmal mit der Konsolenausgabe vergleichen, die Sie in Abbildung 16.5 sehen.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 16\XmlReaderSample
    // ---------------------------------------------------------
    XmlReader reader = XmlReader.Create(@"..\..\Personen.xml");
    while (reader.Read()) {
      switch (reader.NodeType) {
       case XmlNodeType.XmlDeclaration:
         Console.WriteLine("{0,-20}<{1}>", "DEKLARATION", reader.Value);
         break;
       case XmlNodeType.CDATA:
         Console.WriteLine("{0,-20}{1}", "CDATA", reader.Value);
         break;
       case XmlNodeType.Whitespace:
         Console.WriteLine("{0,-20}","WHITESPACE");
         break;
       case XmlNodeType.Comment:
         Console.WriteLine("{0,-20}<!--{1}-->","COMMENT", reader.Value);
         break;
       case XmlNodeType.Element:
         if (reader.IsEmptyElement)
           Console.WriteLine("{0,-20}<{1} />","EMPTY_ELEMENT", reader.Name);
         else {
           Console.WriteLine("{0,-20}<{1}>", "ELEMENT", reader.Name);
           // prüfen, ob der Knoten Attribute hat
           if (reader.HasAttributes) {
             // Durch die Attribute navigieren
             while (reader.MoveToNextAttribute()) {
               Console.WriteLine("{0,-20}{1}",
                      "ATTRIBUT", reader.Name + "=" + reader.Value);
             }
           }
         }
         break;
       case XmlNodeType.EndElement:
         Console.WriteLine("{0,-20}</{1}>", "END_ELEMENT", reader.Name);
         break;
       case XmlNodeType.Text:
         Console.WriteLine("{0,-20}{1}", "TEXT", reader.Value);
         break;
      }
    }
Hier klicken, um das Bild zu vergrößern Abbildung 16.5 Ausgabe des Beispielprogramms »XmlReaderSample« #### Daten eines XML-Dokuments verarbeiten Das folgende Beispiel zeigt, wie die Daten dazu benutzt werden können, Objekte zu erstellen, die die gelesenen Daten in einem sinnvollen Kontext speichern. Ausgangspunkt sei wieder eine XML-Datei, die in diesem Fall aber nicht nur eine, sondern mehrere Personen beschreibt. Eine Person sei durch Vorname, Zuname, Alter sowie eine Adresse beschrieben.
    <Person>
      <Vorname>Manfred</Vorname>
      <Zuname>Fischer</Zuname>
      <Alter>45</Alter>
      <Adresse Ort="Bonn" Strasse="Neuestr.34"></Adresse>
    </Person>
Die Daten zu einer Person sollen in einem Objekt vom Typ Person gespeichert werden. Für die Angaben der Adresse einer Person steht eine weitere Klasse Adresse zur Verfügung, die mit Ort und Strasse zwei Eigenschaften hat.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 16\UsingXmlData
    // ---------------------------------------------------------
    static void Main(string[] args) {
      XmlReader reader;
      reader = XmlReader.Create(@"..\..\Personen.xml");
      List<Person> liste = new List<Person>();
      Person person = null;
      Adresse adresse = null;
      while (reader.Read()) {
        // prüfen, ob es sich aktuell um ein Element handelt
        if (reader.NodeType == XmlNodeType.Element) {
          // alle relevanten Elemente untersuchen
          switch(reader.Name) {
            case "Person":
              // neue Person erzeugen und in Liste eintragen
              person = new Person();
              liste.Add(person);
              break;
            case "Vorname":
              person.Vorname = reader.ReadString();
              break;
            case "Zuname":
              person.Zuname = reader.ReadString();
              break;
            case "Alter":
              person.Alter = reader.ReadElementContentAsInt();
              break;
            case "Adresse":
              // neue Adresse erzeugen und der Person zuordnen
              adresse = new Adresse();
              person.Adresse = adresse;
              if (reader.HasAttributes) {
                // Attributsliste durchlaufen
                while (reader.MoveToNextAttribute())
                {
                  if (reader.Name == "Ort")
                    adresse.Ort = reader.Value;
                  else if (reader.Name == "Strasse")
                    adresse.Strasse = reader.Value;
                }
              }
              break;
          }
        }
      }
      // Liste an der Konsole ausgeben
      GetList(liste);
      reader.Close();
      Console.ReadLine();
    }
    // Ausgabe der Listeneinträge
    static void GetList(List<Person> liste) {
      foreach (Person temp in liste)
      {
        Console.WriteLine("Vorname: {0}\nZuname: {1}\nAlter: {2}",
                temp.Vorname, temp.Zuname, temp.Alter);
        Console.WriteLine("Ort: {0}\nStrasse: {1}\n",
                temp.Adresse.Ort, temp.Adresse.Strasse);
      }
    }
In der while-Schleife interessieren im Zusammenhang mit der Aufgabenstellung nur die Elemente, die Daten beschreiben. Trifft der Reader auf einen Knoten vom Typ XmlNodeType.Element, wird mit einer switch-Anweisung der Elementbezeichner ausgewertet. In den case-Zweigen wird der beschriebene Wert des Elements ermittelt und der entsprechenden Eigenschaft eines Person-Objekts zugewiesen, das beim Erreichen des Elements Person erzeugt und einer generischen Liste hinzugefügt wird. Trifft die Laufzeit auf das Element Adresse, wird ein Objekt der gleichnamigen Klasse erzeugt und die Liste der Attribute des Elements ausgewertet. Den vollständigen Code einschließlich der Klassen Person und Adresse finden Sie auf der Buch-DVD. ### 16.3.2 Validieren eines XML-Dokuments Die XML-Dokumente in den beiden zuvor gezeigten Beispielprogrammen sind wohlgeformt. Sie erkennen das daran, dass die gesamte Datei eingelesen wird, ohne dass es zu einer Ausnahme vom Typ XmlException kommt. Das bedeutet aber noch nicht, dass das Dokument valide, also gültig ist. Wie Sie wissen, ist dazu eine XML-Schema-Datei erforderlich. Mit dem Tool xsd.exe oder Visual Studio ist eine solche Datei sehr einfach zu erzeugen. Um ein XML-Dokument, das mit einem XmlReader-Objekt sequenziell eingelesen wird, zu validieren, müssen wir auf eine Überladung der Methode Create der Klasse XmlReader zurückgreifen, die in einem zweiten Parameter nach einem Objekt vom Typ XmlReaderSettings verlangt.
    public static XmlReader Create(string, XmlReaderSettings)
Ein XmlReaderSettings-Objekt dient nicht nur dazu, die Validierung vorzuschreiben. Es kann darüber hinaus das XmlReader-Objekt mit spezifischen Eigenschaften ausstatten. Beispielsweise könnte das XmlReaderSettings-Objekt das Einlesen der Kommentare oder die Berücksichtigung der Whitespaces unterbinden. Doch widmen wir uns hier der Validierung. Zunächst müssen wir die Klasse XmlReaderSettings instanziieren und anschließend die Eigenschaft ValidationType einstellen. Wollen wir das Dokument gegen ein XML Schema auf Gültigkeit prüfen, müssen wir ValidationType.Schema angeben, z. B.:
    XmlReaderSettings readerSettings = new XmlReaderSettings();
    readerSettings.ValidationType = ValidationType.Schema;
Im nächsten Schritt ist das XML Schema zu benennen. Da es sich auch um mehrere Schemas handeln könnte, kommt eine Auflistung ins Spiel, der alle XML Schemas hinzugefügt werden. Die Referenz auf diese Auflistung erhalten wir über die Eigenschaft Schemas des XmlReaderSettings-Objekts. Darauf rufen wir die Methode Add auf, die im ersten Parameter den im Schema angegebenen targetNamespace erwartet. Enthält das XML Schema kein Attribut targetNamespace oder legen Sie auf den Namespace der im XML Schema beschriebenen Elemente keinen Wert, können Sie dem ersten Parameter auch null übergeben. Dem zweiten Parameter teilen Sie den URI der XML-Schema-Datei mit.
    readerSettings.Schemas.Add(null, @"..\..\Personen.xsd");
Sollte bei der Validierung des XML-Dokuments ein Fehler auftreten, wird das XmlReaderSettings-Objekt das Ereignis ValidationEventHandler auslösen. Wir müssen bei dem Objekt jetzt nur noch einen passenden Ereignishandler registrieren. readerSettings.ValidationEventHandler += ValidationCallback; Scheitert die Validierung, wird die Überprüfung des XML-Dokuments abgebrochen. Der Rest des Dokuments wird zwar weiterhin eingelesen, bleibt aber ungeprüft, und folgende Validierungsfehler werden nicht mehr erkannt. Besser ist es, im Fall des Scheiterns der Validierung eine Ausnahme auszulösen. Dies ermöglicht uns der Parameter vom Typ ValidationEventArgs des Ereignishandlers, der drei spezifische Eigenschaften hat, die Sie der Tabelle 16.3 entnehmen können.

Tabelle 16.3 Die Eigenschaften des »ValidationEventArgs«-Objekts

Eigenschaft | Beschreibung
Exception | Ruft die dem Validierungsfehler zugeordnete Excpetion vom Typ XmlSchemaException ab.
Message | Liefert eine textuelle Beschreibung des aufgetretenen Validierungsfehlers.
Severity | Ruft den Schweregrad des Validierungsfehlers ab. Dabei werden zwei Schweregrade unterschieden: Warning, falls kein XML Schema vorhanden ist, anhand dessen validiert werden kann, und Error, wenn ein Validierungsfehler auftritt.
Bitte beachten Sie, dass die Klasse ValidationEventArgs im Namespace System.Xml.Schema enthalten ist, der über using bekannt gegeben werden sollte. Nachdem nun alle vorbereitenden Maßnahmen ergriffen worden sind, müssen wir zum Schluss noch dem XmlReader-Objekt das konfigurierte XmlReaderSettings-Objekt übergeben.
    XmlReader reader =
        XmlReader.Create(@"..\..\Personen.xml", readerSettings);
#### Beispielprogramm zur Validierung Die XML-Datei aus dem Beispiel XmlReaderSample soll nun anhand eines XML- Schemas validiert werden. Tritt ein Validierungsfehler auf, soll eine Ausnahme ausgelöst werden, die wir dem zweiten Parameter des Ereignishandlers entnehmen. Darüber hinaus interessieren uns in der Ausgabe weder die Kommentare noch die Whitespaces. Diese werden durch die Einstellungen IgnoreWhitespace und IgnoreComments des XmlReaderSettings-Objekts ignoriert.
    // ---------------------------------------------------------
    // Beispiel: ... \Kapitel 16\ValidationSample
    // ---------------------------------------------------------
    static void Main(string[] args) {
      XmlReaderSettings readerSettings = new XmlReaderSettings();
      readerSettings.IgnoreWhitespace = true;
      readerSettings.IgnoreComments = true;
      readerSettings.ValidationType = ValidationType.Schema;
      readerSettings.Schemas.Add(null, @"..\..\Personen.xsd");
      readerSettings.ValidationEventHandler += ValidationCallback;
      XmlReader reader = XmlReader.Create(@"..\..\Personen.xml", readerSettings);
      try {
        while (reader.Read()) {
          // Code wie im Beispielprogramm "XmlReaderSample"
        }
      }
      catch(Exception ex) {
        Console.WriteLine("Validierung fehlgeschlagen. \n{0}", ex.Message);
      }
      reader.Close();
      Console.ReadLine();
    }
    // Ereignishandler
    static void ValidationCallback(object sender, ValidationEventArgs e)
    {
      throw e.Exception;
    }