Von XML-Dokumenten zu VB-Klassen

Veröffentlicht: 15. Dez 2001 | Aktualisiert: 19. Jun 2004

Von Dave Grundgeiger und Patrick Escarcega

Durch die in diesem Artikel vorgestellte Klassenvorlage können Sie in Ihrer Anwendung Klassen erzeugen, die auf XML-Datenhaltung und -abbildung aufsetzen. Diese Klassen helfen Ihnen, den Datenbestand zu manipulieren, ohne auf die XML-Schnittstellen zurückgreifen zu müssen.

Auf dieser Seite

 Mit Hüllklassen-Templates
 Die XML-Verpackungsklassen
 Entwicklung einer Verpackungsklasse
 Sich wiederholende Kindelemente
 Das Beispielprogramm
 Raum für Erweiterungen
 Ein Wort zum Schluss

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

Bild05

Mit Hüllklassen-Templates

XML ist eine leistungsfähige plattformunabhängige Formatierungssprache, die oft gewählt wird, wenn Daten darzustellen sind. Wenn man sie mit XML-Schemas kombiniert, ist es möglich, eine Dokumentart zu definieren und dafür zu sorgen, dass sich ein gegebenes Dokument an die gewünschte Definition hält. In der Industrie haben sich viele Gruppen gebildet, die nach ihren eigenen Vorstellungen XML-Schemas definiert und veröffentlicht haben. Diese Formatierungsvorschriften legen fest, wie die XML-Daten aus den entsprechenden Arbeitsgebieten dargestellt werden sollen. Anwendungen, die ihre Daten anhand dieser Schemata in XML-Form erwarten und anbieten, sind untereinander zur Zusammenarbeit in der Lage, selbst wenn sie in verschiedenen Programmiersprachen implementiert worden sind und auf verschiedenen Plattformen von verschiedenen Herstellern laufen.

Werkzeugbauer haben schon längst mit der Entwicklung von wiederverwendbaren Komponenten begonnen, mit denen Entwickler XML-Dokumente einschließlich der Schemata erzeugen und analysieren können. Die XML-Komponente von Microsoft (MSXML) ist ein schönes Beispiel dafür. Sie kann XML-Daten erzeugen oder analysieren und bietet mit dem DOM (document object model) ein flexibles hierarchisches Objektmodell an, dessen Hierarchie dem zu bearbeitenden Dokument entspricht.

Allerdings bringt eine Komponente wie MSXML den Nachteil mit sich, dass sich der Entwickler nicht nur mit XML auskennen muss, sondern auch mit der Komponente selbst. Da MSXML immer noch relativ neu ist, könnten die Anforderungen dadurch vielleicht zu hoch gesteckt sein. Für manche Projekte könnten die Kosten für die Aneignung des neuen Objektmodells (und die Stabilisierung des resultierenden Codes) so hoch sein, dass sich ein Entwurf auf XML-Basis vielleicht von selbst verbietet.

Unsere Lösung besteht in der Entwicklung von passenden XML-Verpackungsklassen - nämlich von Visual Basic-Klassen, die in der Lage sind, sich selbst in ein XML-Dokument auf Schema-Basis zu serialisieren. Wir versuchen, dieses Ziel durch die Aufnahme der entsprechenden Funktionalität für Erzeugung und Analyse von XML-Dokumenten in jede Klasse zu erreichen, deren Daten von mehreren Konsumenten gemeinsam benutzt werden sollen.

In diesem Artikel möchten wir unsere Methode anhand einiger Klassen aus einer Beispielanwendung für die Erfassung von Bestellungen und dem dazugehörigen Schema vorstellen. Eine kleine improvisierte Anwenderschnittstelle soll verdeutlichen, wie diese Objekte benutzt werden. Beginnen möchten wir mit der Beschreibung des XML-Schemas, auf dem die Anwendungsdaten beruhen. Anschließend werden wir beschreiben, wie man die erforderlichen Klassen mit Hilfe unserer XML-Verpackungsklasse entwickelt. Außerdem möchten wir Ihnen demonstrieren, wie wir die Klassen für die sich wiederholenden Kindelemente angepasst haben, die zum selben Schematyp gehören.

L1 Schema für das Customer-Element

<AttributeType name="ID" content="textOnly" dt:type="i4"/> 
<ElementType name="Name" content="textOnly" dt:type="string"/> 
<ElementType name="Address" content="textOnly" dt:type="string"/> 
<ElementType name="City" content="textOnly" dt:type="string"/> 
<ElementType name="State" content="textOnly" dt:type="string"/> 
<ElementType name="ZIP" content="textOnly" dt:type="string"/> 
<ElementType name="Telephone" content="textOnly" dt:type="string"/> 
<ElementType name="Customer" content="eltOnly" model="closed"> 
    <attribute type="ID"/> 
    <element type="Name" minOccurs="1" maxOccurs="1"/> 
    <element type="Address" minOccurs="1" maxOccurs="1"/> 
    <element type="City" minOccurs="1" maxOccurs="1"/> 
    <element type="State" minOccurs="1" maxOccurs="1"/> 
    <element type="ZIP" minOccurs="1" maxOccurs="1"/> 
    <element type="Telephone" minOccurs="1" maxOccurs="1"/> 
</ElementType>

 

Die XML-Verpackungsklassen

Listing L1 zeigt einen Ausschnitt aus dem XML-Schema, das in unserem Beispielprogramm zum Einsatz kommt. Dieses Fragment definiert ein Element, in dem Informationen liegen, die sich zur Beschreibung eines Kunden eignen. Das folgende XML-Element entspricht diesem Schema:

<Customer ID="1234"> 
    <Name>J. Customer</Name> 
    <Address>123 Easy St.</Address> 
    <City>Paradise</City> 
    <State>HI</State> 
    <ZIP>99999</ZIP> 
    <Telephone>(808)555-1212</Telephone> 
</Customer>

Wir beginnen mit der Entwicklung einer Klasse namens CCustomer in Visual Basic, die in der Anwendung das Customer-Element aus diesem Schema repräsentiert. Sie wird die Properties ID, Name, Address, City, State, ZIP und Telephone haben und sich folgendermaßen einsetzen lassen:

Dim objCustomer As CCustomer 
Set objCustomer = New CCustomer 
With objCustomer 
    .ID = 1234 
    .Name = "J. Customer" 
    .Address = "123 Easy St." 
    .City = "Paradise" 
    .State = "HI" 
    .ZIP = "99999" 
    .Telephone = "(808)555-1212" 
End With

Wer die Programmierung in Visual Basic kennt, wird wohl kaum Probleme mit dem Objekt haben. Etwas ungewöhnlich dürfte allerdings das Property namens XML sein. Liest man dieses XML-Property des Objekts objCustomer nach den obigen Zuweisungen aus, erhält man den entsprechenden XML-Code, der den aktuellen Zustand des Objekts darstellt:

<Customer ID="1234"> 
    <Name>J. Customer</Name> 
    <Address>123 Easy St.</Address> 
    <City>Paradise</City> 
    <State>HI</State> 
    <ZIP>99999</ZIP> 
    <Telephone>(808)555-1212</Telephone> 
</Customer>

Mit dem resultierenden XML-Code kann man zum Beispiel eine XSL-Transformation für die Anzeige in einem Webbrowser vornehmen oder ihn als Serialisierungsmechanismus zwischen Komponenten oder separaten Anwendungen betrachten, mit dessen Hilfe sich Objekte als Werte (by Value) übergeben lassen.

 

Entwicklung einer Verpackungsklasse

Nach diesen Vorbereitungen kann es an die Entwicklung einer XML-Verpackungsklasse gehen. Im Beispielcode für diesen Artikel gibt es eine Datei namens XML Wrapper.cls (den Code finden Sie auf der Begleit-CD dieses Hefts). Diese Datei enthält die Implementierung einer Visual Basic-Klasse namens CXMLWrapper. Sie enthält den gesamten Code, der für die Ausstattung der eigenen Anwendungsklassen mit der Funktionalität für die XML-Erzeugung und Analyse erforderlich ist.

Wir benutzen zum Lesen und Schreiben von XML einfach den MSXML-Parser. Daher muss diese Komponente natürlich auf Ihrem System installiert sein. Die neuste Version des MSXML-Parsers finden Sie unter http://msdn.microsoft.com/xml. Folgen Sie einfach den entsprechenden Verknüpfungen. Starten Sie die heruntergeladene ausführbare Datei auf Ihrer Maschine, damit die MSXML-Komponente installiert und in der Registratur angemeldet wird. Sorgen Sie auch dafür, dass die Komponente im Verweise-Dialog Ihres Visual Basic-Projekts genannt wird.

Da wir diese XML Wrapper-Klasse als Vorlage benutzen möchten, haben wir sie in den Ordner für die Klassenvorlagen kopiert, der in Visual Basic normalerweise vom Installationsverzeichnis aus unter \Template\Classes zu erreichen ist. Dadurch wird das Klassenmodul oder die "Template-Klasse" für die Aufnahme in Visual Basic-Projekte zugänglich (Bild B1).

Bild01

B1 XML Wrapper als Klassenvorlage

L2 Das Beispiel-Schema

<?xml version="1.0"?> 
<Schema 
  name="Order Entry Test" 
  xmlns="urn:schemas-microsoft-com:xml-data"  
  xmlns:dt="urn:schemas-microsoft-com:datatypes"> 
  <AttributeType name="ID" content="textOnly" dt:type="i4"/> 
  <ElementType name="Quantity" content="textOnly" dt:type="i4"/> 
  <ElementType name="Name" content="textOnly" dt:type="string"/> 
  <ElementType name="Description" content="textOnly"  
               dt:type="string"/> 
  <ElementType name="Location" content="textOnly" dt:type="string"/> 
  <ElementType name="Telephone" content="textOnly" dt:type="string"/> 
  <ElementType name="CreatedDate" content="textOnly"  
               dt:type="dateTime"/> 
  <ElementType name="DueDate" content="textOnly" dt:type="dateTime"/> 
  <ElementType name="Contact" content="textOnly" dt:type="string"/> 
  <ElementType name="Address" content="textOnly" dt:type="string"/> 
  <ElementType name="City" content="textOnly" dt:type="string"/> 
  <ElementType name="State" content="textOnly" dt:type="string"/> 
  <ElementType name="ZIP" content="textOnly" dt:type="string"/> 
  <ElementType name="Customer" content="eltOnly" model="closed"> 
      <attribute type="ID"/> 
      <element type="Name" minOccurs="1" maxOccurs="1"/> 
      <element type="Address" minOccurs="1" maxOccurs="1"/> 
      <element type="City" minOccurs="1" maxOccurs="1"/> 
      <element type="State" minOccurs="1" maxOccurs="1"/> 
      <element type="ZIP" minOccurs="1" maxOccurs="1"/> 
      <element type="Telephone" minOccurs="1" maxOccurs="1"/> 
  </ElementType> 
  <ElementType name="OrderItem" content="eltOnly" model="closed"> 
      <element type="Name" minOccurs="1" maxOccurs="1"/> 
      <element type="Description" minOccurs="1" maxOccurs="1"/> 
      <element type="Quantity" minOccurs="1" maxOccurs="1"/> 
      <element type="Location" minOccurs="1" maxOccurs="1"/> 
      <attribute type="ID"/> 
  </ElementType> 
  <ElementType name="OrderItems" content="eltOnly" model="closed"> 
      <element type="OrderItem" minOccurs="0" maxOccurs="*"/> 
  </ElementType> 
  <ElementType name="Order" content="eltOnly" model="closed"> 
      <element type="OrderItems" minOccurs="1" maxOccurs="1"/> 
      <element type="Customer" minOccurs="1" maxOccurs="1"/> 
      <element type="DueDate" minOccurs="1" maxOccurs="1"/> 
      <element type="CreatedDate" minOccurs="1" maxOccurs="1"/> 
      <element type="Contact" minOccurs="1" maxOccurs="1"/> 
      <attribute type="ID"/> 
  </ElementType> 
</Schema>

Nachdem wir nun die Klasse CXMLWrapper als Vorlage zur Verfügung hatten, nahmen wir eine neue Klasse in unser Projekt auf und nannten sie CCustomer. Mit den folgenden sechs Arbeitsschritten änderten wir die Klasse CCustomer so ab, dass sie zum XML-Element Customer aus Listing L2 passt.

  1. Ändere die Konstante NODE_NAME so ab, dass sie den Namen des zu verpackenden Elements wiedergibt.

  2. Füge die erforderlichen Properties zur Repräsentation der Attribute und Kindelemente des Elements in die Klasse ein.

  3. Füge für jedes Attribut und Kindelement den erforderlichen Code in die CreateNode-Methode der Klasse ein, der den XML DOM-Knoten aufbaut.

  4. Füge für jedes Attribut und Kindelement den erforderlichen Code in die LoadNode-Methode ein, der einen XML DOM-Knoten einliest.

  5. Füge den Code in die Class_Initialize-Routine ein, der die Instanzen der Kindobjekte anlegt, sofern vorhanden.

  6. Füge den Code in die Class_Terminate-Routine ein, der die Kindobjekte wieder entsorgt, die in Class_Initialize angelegt wurden.

Zuerst änderten wir die Definition der Konstanten NODE_NAME im Code der neuen Klasse so ab, dass sie den Namen des einzupackenden Elements angibt. Ist die Klasse CCustomer in unserem XML-Code zum Beispiel für das Customer-Element zuständig, so lautet die Definition der NODE_NAME-Konstanten folgendermaßen:

Private Const NODE_NAME = "Customer"

Anschließend fügten wir die Properties in die Klasse ein, die zur Repräsentation der Attribute und Kindelemente vom einzuhüllenden Element erforderlich sind. Jedes Attribut und Kindelement wird durch ein Property dargestellt, das denselben Namen wie das Attribut oder Kindelement hat. So geht aus unserem XML-Beispielschema hervor, dass es im Customer-Element ein Attribut namens ID mit dem Datentyp i4 gibt (4-Byte-Integer). Zur Repräsentation dieses Attributs fügten wir ein Property namens ID in die Klasse CCustomer ein und gaben ihm den Datentyp Long. In unserer Klassenvorlage gibt es dafür eine kleine Anleitung mit Beispielcode:

'******* Attribut-Beispieldefinition ******* 
' Kopieren Sie zur Definition eines Properties, das ein Attribut  
' repräsentiert, die folgenden beiden Zeilen. Ersetzen Sie "MyAttri- 
' bute" durch den Namen des Attributs und "MyDataType" durch einen 
' passenden Datentyp. 
'Public MyAttribute As MyDataType 
'Private Const ATTRIBUTE_MyAttribute = "MyAttribute"

Also kopierten wir die letzten beiden Zeilen, löschten die Kommentarzeichen an den Zeilenanfängen und änderten MyDataType in den Typ des abzubildenden Attributs, in diesem Fall also in Long. Anschließend änderten wir die drei Vorkommen der Bezeichnung MyAttribute jeweils in den Namen des abzubildenden Attributs. Das Ergebnis für das ID-Attribut sieht so aus:

Public ID As Long 
Private Const ATTRIBUTE_ID = "ID"

Zusätzlich zum ID-Property wird auch eine Konstante für den Attributnamen definiert. Im Code dient diese Konstante später für Zugriffe auf das Attribut in der XML DOM-Hierarchie.

Die Kindelemente werden in ähnlicher Weise berücksichtigt. Allerdings machen Sie etwas mehr Arbeit. Wir unterscheiden zwischen Kindelementen, die Daten mit irgendeinem Basistyp wie String oder Integer enthalten, und Kindelementen, die weitere geschachtelte Kindelemente enthalten. Im Rest dieses Artikels ist mit der Bezeichnung "Datenelement" ein Kindelement gemeint, das Daten von irgendeinem Basistyp enthält, während das "geschachtelte Element" ein Kindelement meint, das weitere Kindelemente enthält. Diese Bezeichnungen werden hier quasi zu privaten Zwecken definiert. Sie werden im Allgemeinen nicht automatisch so verstanden, wenn von XML-Kindelementen die Rede ist.

Unsere Vorlage für Datenelemente sieht so aus:

'******* Beispieldeklaration eines Datenelements ******* 
' Kopieren Sie zur Deklaration eines Properties, das ein Kindelement 
' repräsentiert, in dem es einen Wert mit irgendeinem Basistyp gibt, 
' die folgenden beiden Zeilen (das Kindelement enthält also keine 
' weiteren Kindelemente). Ersetzen Sie "MyDataElement" durch den Namen 
' des Kindelements und "MyDataType" durch den entsprechenden Datentyp, 
' der im Kindelement gespeichert wird. 
'Public MyDataElement As MyDataType 
'Private Const ELEMENT_MyDataElement = "MyDataElement"

Für den Einsatz dieser Vorlage ändern wir MyDataType in den enthaltenen Datentyp ab (in diesem Falle String) und ändern dann alle drei Vorkommen von MyDataElement jeweils auf den Namen des XML-Elements ab, das wir abbilden. Das Name-Element von unserem Customer-Element wird zum Beispiel durch die folgenden Zeilen repräsentiert:

Public Name As String 
Private Const ELEMENT_Name = "Name"

Für die Einbindung geschachtelter Kindelemente müssen unsere Klassen selbst geschachtelt sein. Da unser Customer-Element keine geschachtelten Elemente enthält, möchten wir nun das Order-Element als Beispiel benutzen. Unser Order-Element enthält ein Customer-Element. Da es im Customer-Element wiederum Kindelemente gibt, wird es nicht als Basisdatentyp abgebildet, sondern als eine Klasse namens CCustomer. Unsere COrder-Klasse muss sich also an die folgende Vorlage halten:

'******* Beispieldeklaration für ein geschachteltes Element ******* 
' Zur Deklaration eines Properties, das ein Kindelement repräsentiert, 
' in dem es weitere Kindelemente gibt, kopieren Sie die folgende Zei- 
' le. Ersetzen Sie "MyNestedElement" durch den Namen des Kindelements. 
'Public MyNestedElement As CMyNestedElement

Für den Einsatz dieses Codes ersetzen wir die beiden Vorkommen des Strings "MyNestedElement" durch den Namen des geschachtelten Elements und streichen das Kommentarzeichen am Zeilenanfang. Nun steht die folgende Zeile in unserer COrder-Klasse, die unser geschachteltes Customer-Element repräsentiert:

Public Customer As CCustomer

Im dritten Schritt nahmen wir den Code zur Erstellung der Attribute und Kindelemente in die CreateNode-Methode auf. Zweck der CreateNode-Methode ist die Erstellung eines XML DOM-Knotens und der geschachtelten Kindobjekte anhand der Werte aus den Properties der Klasse. Beginnen wir mit einem Blick auf die Signatur der CreateNode-Methode:

Public Function CreateNode( _ 
    Optional Parent As MSXML2.IXMLDOMNode = Nothing _ 
) As MSXML2.IXMLDOMNode

Die CreateNode-Methode nimmt als optionales Argument eine IXMLDOMNode-Schnittstelle an. Damit können unsere Klassen die CreateNode-Methoden der geschachtelten Klassen aufrufen und dafür sorgen, dass der resultierende DOM-Knoten an einen einzelnen Baum mit DOM-Knoten angebunden wird. Sollte ein XML DOM-Knoten im Parent-Parameter der CreateNode-Methode übergeben werden, so legt unser Code einen neuen Knoten an, der seine Properties repräsentiert, und fügt ihn als Kind in den übergebenen Knoten ein. Fehlt das Argument für den Parent-Parameter oder wird nur Nothing übergeben, legt die Methode einen Haupt-Dokumentknoten an (top-level).

L3 Die CreateNode-Funktion der CCustomer-Klasse

' Liefere einen XML DOM-Knoten, der die Properties dieser Klasse dar- 
' stellt. Falls ein Elternknoten übergeben wird, so wird der zurück- 
' gegebene Knoten außerdem als Kindknoten in den Elternknoten ein- 
' gefügt. 
Public Function CreateNode( _ 
    Optional Parent As MSXML2.IXMLDOMNode = Nothing _ 
) As MSXML2.IXMLDOMNode 
    Dim node As MSXML2.IXMLDOMNode 
    Dim dom As MSXML2.DOMDocument 
    ' Sofern kein Elternknoten übergeben wurde, erzeuge ein  
    ' DOM- und Document-Element. 
    If Parent Is Nothing Then 
        Set dom = New MSXML2.DOMDocument 
        dom.loadXML "<" & NODE_NAME & "/>" 
        Set node = dom.documentElement 
    ' Benutze im anderen Fall den übergebenen Elternknoten. 
    Else 
        Set dom = Parent.ownerDocument 
        Set node = dom.createElement(NODE_NAME) 
        Parent.appendChild node 
    End If 
    '********************************************************************* 
    ' TODO: Fügen Sie hier den Code zum Speichern der Attribute 
    ' und Kindelemente im Knoten ein. Die auskommentierten Code- 
    ' zeilen zeigen einige Beispiele. 
    NodeAppendAttribute dom, node, ATTRIBUTE_ID, ID 
    NodeAppendChildElement dom, node, ELEMENT_Name, Name 
    NodeAppendChildElement dom, node, ELEMENT_Address, Address 
    NodeAppendChildElement dom, node, ELEMENT_City, City 
    NodeAppendChildElement dom, node, ELEMENT_State, State 
    NodeAppendChildElement dom, node, ELEMENT_ZIP, ZIP 
    NodeAppendChildElement dom, node, ELEMENT_Telephone, Telephone 
    '******* Beispielcode zur Speicherung eines Attributs ******* 
    ' Zur Speicherung eines Attributs kopieren Sie die folgende Zeile 
    ' und ändern "MyAttribute" in den Namen des Attributs. 
    'NodeAppendAttribute dom, node, ATTRIBUTE_MyAttribute, MyAttribute 
    '******* Beispielcode zur Speicherung eines Datenelements ******* 
    ' Zur Speicherung eines Kindelements, das einen Wert mit einem  
    ' Basistyp enthält, kopieren Sie die folgende Zeile. Ersetzen Sie 
    ' den String "MyDataElement" durch den Namen des Kindelements. 
    'NodeAppendChildElement dom, node, ELEMENT_MyDataElement, _ 
    '  MyDataElement 
    '** Beispielcode zur Speicherung eines geschachtelten Elements ** 
    ' Zur Speicherung eines Kindelements, das weitere Kindelemente  
    ' enthält, kopieren Sie die folgende Zeile und ersetzen den  
    ' String "MyNestedElement" durch den Namen des Kindelements. 
    'MyNestedElement.CreateNode node 
    ' Gib den erzeugten Knoten zurück 
    Set CreateNode = node 
End Function

Unabhängig davon, ob nun ein Elternknoten übergeben wird oder nicht, werden die Properties der Klasse als Kinder des neuen Knotens abgespeichert. Zur Speicherung von Attributen und Datenelementen im neuen Knoten sind unsere Hilfsfunktionen NodeAppendAttribute und NodeAppendChildElement erforderlich (Dieser Code liegt gegen Ende der Datei in der Klassenvorlage). Zur Speicherung der geschachtelten Elemente wird die CreateNode-Methode der entsprechenden Elementklasse aufgerufen.

Kommen wir in unserem Beispiel noch einmal auf die Klasse CCustomer zurück, die das Customer-Element verpackt. In unserem Schema haben Customer-Elemente ein ID-Attribut. Wie Sie im vorigen Schritte sehen konnten, haben wir in die CCustomer-Klasse den Code für ein ID-Property eingebaut. Nun müssen wir auch die CreateNode-Methode erweitern, um den Wert des ID-Properties in ein Attribut des neu angelegten Knotens speichern zu können. Die Vorlage macht folgendes Angebot zur Speicherung von Attributen:

'******* Beispielcode zur Speicherung eines Attributs ******* 
' Zur Speicherung eines Attributs kopieren Sie die folgende Zeile und 
' ändern "MyAttribute" durch den Namen des Attributs. 
'NodeAppendAttribute dom, node, ATTRIBUTE_MyAttribute, MyAttribute

Für unsere CCustomer-Klasse ersetzten wir "MyAttribute" durch "ID" und löschten dann das Kommentarzeichen. Das Ergebnis sieht so aus:

NodeAppendAttribute dom, node, ATTRIBUTE_ID, ID

Diese Codezeile ruft die NodeAppendAttribute-Prozedur auf. Damit erzeugt sie in der Attributsammlung des Knotens ein neues Attribut, gibt ihr den Namen ID (das ist der Wert der Konstanten ATTRIBUTE_ID) und weist ihr den Wert aus dem ID-Property der Klasse zu.

Der Vorschlag der Klassenvorlage zur Speicherung eines Datenelements lautet ähnlich:

'******* Beispielcode zur Speicherung eines Datenelements ******* 
' Zur Speicherung eines Kindelements, das einen Wert mit einem  
' Basistyp enthält, kopieren Sie die folgende Zeile. Ersetzen Sie 
' den String "MyDataElement" durch den Namen des Kindelements. 
'NodeAppendChildElement dom, node, ELEMENT_MyDataElement, MyDataElement

Wir folgten derselben Verfahrensweise wie bei den Attributen und ersetzten beide Vorkommen von "MyDataElement" durch den tatsächlichen Elementnamen. So geht die CCustomer-Klasse zum Beispiel folgendermaßen mit dem Name-Element um:

NodeAppendChildElement dom, node, ELEMENT_Name, Name

Zum Schluss möchten wir Ihnen noch den Vorschlag der Klassenvorlage zur Speicherung eines geschachtelten Elements zeigen:

'** Beispielcode zur Speicherung eines geschachtelten Elements *** 
' Zur Speicherung eines Kindelements, das weitere Kindelemente ent- 
' hält, kopieren Sie die folgende Zeile und ersetzen den String 
' "MyNestedElement" durch den Namen des Kindelements. 
'MyNestedElement.CreateNode node

Auch hier folgten wir einfach wieder dem Vorschlag und änderten den Namen von "MyNestedElement" in "Customer", mit folgendem Ergebnis:

Customer.CreateNode node

Die vollständige CreateNode-Funktion der Klasse CCustomer aus der Datei CCustomer.cls finden Sie in Listing L3.

Im vierten Schritt ergänzten wir die LoadNode-Methode der Klasse um den erforderlichen Code für die Attribute und Kindelemente. Aufgabe der LoadNode-Methode ist es, einen XML DOM-Knoten einzulesen und die Klassenproperties anhand der Werte zu setzen, die sie im Knoten vorfindet. Wie zuvor bietet die Klassenvorlage auch dafür auskommentierte Codebeispiele an, mit denen sich Attribute, Datenelemente und geschachtelte Elemente laden lassen. Listing L4 zeigt die vollständige LoadNode-Methode aus unserer CCustomer-Klasse. Die LoadNode-Methode greift zur Ermittlung des Textwertes von benannten Attribute oder Kindelementen auf unsere Hilfsfunktion GetNodeText zurück.

L4 Die LoadNode-Methode aus der CCustomer-Klasse

' Setze die Properties der Klasse anhand der Daten aus dem gegebenen 
' Knoten. 
Public Sub LoadNode(node As MSXML2.IXMLDOMNode) 
    ' Überprüfe, ob ein gültiger Knoten übergeben wurde. 
    If node Is Nothing Then Exit Sub 
    '******************************************************************** 
    'TODO: Füge hier den Code zum Laden der Properties der Klasse 
    ' aus dem gegebenen Knoten ein. In den folgenden Zeilen finden 
    ' sich auskommentierte Codebeispiele. 
    ID = GetNodeText(node, "@" & ATTRIBUTE_ID) 
    Name = GetNodeText(node, ELEMENT_Name) 
    Address = GetNodeText(node, ELEMENT_Address) 
    City = GetNodeText(node, ELEMENT_City) 
    State = GetNodeText(node, ELEMENT_State) 
    ZIP = GetNodeText(node, ELEMENT_ZIP) 
    Telephone = GetNodeText(node, ELEMENT_Telephone) 
    '******* Beispielcode zum Laden eines Attributs ******* 
    ' Zum Laden eines Properties aus einem Attribut kopieren Sie die 
    ' folgende Zeile und ersetzen "MyAttribute" durch den Namen des 
    ' Attributs. Ersetzen Sie außerdem "MyDataType" durch den passen- 
    ' den Datentyp für das Attribut. 
    'MyAttribute = GetNodeText(node, "@" & ATTRIBUTE_MyAttribute) 
    '******* Beispielcode zum Laden eines Datenelements ******* 
    ' Zum Laden eines Properties aus einem Kindelement, das einen 
    ' Wert mit einem Basistyp enthält, kopieren Sie die folgende  
    ' Zeile und ersetzen "MyDataElement" durch den für das Attribut 
    ' passenden Datentyp. 
    'MyDataElement = GetNodeText(node, ELEMENT_MyDataElement) 
    '*** Beispielcode zum Laden eines geschachtelten Elements **** 
    ' Zum Laden eines Properties aus einem Kindelement, das weitere 
    ' Kindelemente enthält, kopieren Sie die folgende Zeile und  
    ' ersetzen "MyNestedElement" durch den Namen des Kindelements. 
    'MyNestedElement.LoadNode _ 
    '  node.selectSingleNode(MyNestedElement.NodeName) 
End Sub

Im nächsten Schritt ergänzten wir die Class_Initialize-Routine um den Code, der die Kindobjekte anlegt. Kindobjekte werden, um es zu wiederholen, zur Darstellung von Kindelementen benutzt, die selbst Attribute und zusätzliche geschachtelte Elemente enthalten. Unsere CCustomer-Klasse hat keine Kindobjekte. Daher bleibt diese Unterroutine in dieser Klasse leer. Die folgenden Zeilen zeigen die Class_Initialize-Routine aus unserer Klasse COrder:

Private Sub Class_Initialize() 
    'TODO: Lege die Kindelementobjekte an. 
    ' Kopieren Sie die folgende Zeile und ersetzen Sie  
    ' "MyNestedElement" durch den Namen des Kindelements. 
    'Set MyNestedElement = New CMyNestedElement 
    Set OrderItems = New COrderItems 
    Set Customer = New CCustomer 
End Sub

Diese Klasse enthält zwei Kindklassen, nämlich COrderItems und CCustomer, die als Verpackungen für die Elemente OrderItems und Customer dienen.

Im letzten Schritt erhält die Class_Terminate-Routine den Code zur Entsorgung der Kindobjekte, die in Class_Initialize angelegt wurden. Auch in diesem Fall kommt die Klasse CCustomer ohne Codezusätze aus, weil sie keine Kindobjekte hat. In der Klasse COrder sieht die Unterroutine Class_Terminate allerdings so aus:

Private Sub Class_Terminate() 
    'TODO: Gib die Kindobjektreferenzen wieder ab. 
    ' Kopieren Sie die folgende Zeile und ersetzen Sie  
    ' "MyNestedElement" durch den Namen des Kindelements. 
    'Set MyNestedElement = Nothing 
    Set OrderItems = Nothing 
    Set Customer = Nothing 
End Sub

Das war's. Nachdem unsere Customer-Klasse nun fertig ist, kann sie XML lesen und schreiben, ohne dass der Anwender der Klasse etwas darüber wissen müsste, wie das eigentlich gemacht wird.

 

Sich wiederholende Kindelemente

Bisher war nur von Kindelementen die Rede, die genau einmal auftauchen. Nun kann das OrderItem-Element in unserem Schema aber null oder mehr Male in seinem Elternelement OrderItems auftauchen. Um darauf vorbereitet zu sein, müssen wir unsere COrderItems noch entsprechend anpassen. In diesem Abschnitt geht es also darum, wie man mehrere Kindelemente desselben Typs einbinden kann.

Zuerst fügen Sie eine neue Klasse in unser Projekt ein, die wieder auf der CXMLWrapper-Klassenvorlage beruht, und nennen sie COrderItems. Im Code der neuen Klasse suchen Sie die Definition der Konstanten NODE_NAME und ändern sie so ab, dass sie auf OrderItems lautet, wie in der folgenden Zeile:

Private Const NODE_NAME = "OrderItems"

Direkt im Anschluss an die NODE_NAME-Definition fügen Sie folgenden Codeblock ein:

' Dieser Codeblock wurde zur Einbindung sich wiederholender 
' <OrderItem>-Elemente eingefügt. Er gehört nicht zur  
' Standardvorlage. 
    ' Die sich wiederholenden Einträge werden in einer 
    ' Sammlung (Collection) erfasst. 
    Private m_colItems As Collection 
' Ende des Zusatzcodes für sich wiederholende <OrderItem>-Elemente.

Dadurch wird ein Collection-Objekt deklariert, das die sich wiederholenden COrderItem-Objekte aufnehmen soll.

Bauen Sie den folgenden Code direkt im Anschluss an den TODO-Kommentar in die CreateNode-Funktion ein. Er geht in einer Schleife über die COrderItem-Objekte und speichert sie im DOM-Knoten ab.

' Dieser Codeblock wurde zur Einbindung sich wiederholender 
' <OrderItem>-Elemente eingefügt. Er gehört nicht zur  
' Standardvorlage. 
    ' Speichere die sich wiederholenden <OrderItem>-Elemente. 
    Dim objOrderItem As COrderItem 
    For Each objOrderItem In m_colItems 
        objOrderItem.CreateNode node 
    Next 
    Set objOrderItem = Nothing 
' Ende des Zusatzcodes für sich wiederholende <OrderItem>-Elemente.

Bringen Sie den Codeblock aus Listing L5 in der Funktion LoadNode unter, und zwar direkt im Anschluss an den TODO-Kommentar. Er lädt die COrderItem-Objekte aus dem DOM-Knoten.

L5 Lade COrderItem-Objekte

' Dieser Codeblock wurde zur Einbindung sich wiederholender 
' <OrderItem>-Elemente eingefügt. Er gehört nicht zur  
' Standardvorlage. 
    ' Lade sich wiederholende <OrderItem>-Elemente. 
    Dim objOrderItem As COrderItem 
    Dim Nodes As MSXML2.IXMLDOMNodeList 
    Dim OrderNode As MSXML2.IXMLDOMNode 
    Set objOrderItem = New COrderItem 
    Set m_colItems = New Collection 
    Set Nodes = node.selectNodes(objOrderItem.NodeName) 
    For Each OrderNode In Nodes 
        Set objOrderItem = New COrderItem 
        objOrderItem.LoadNode OrderNode 
        m_colItems.Add objOrderItem 
    Next 
    Set objOrderItem = Nothing 
' Ende des Zusatzcodes für sich wiederholende <OrderItem>-Elemente.

Bringen Sie den folgenden Code in der Class_Initialize-Routine unter, wiederum direkt im Anschluss an den TODO-Kommentar. Er initialisiert das Collection-Objekt, das die COrderItem-Objekte aufnimmt.

' Codeblock zur Einbindung sich wiederholender <OrderItem>-Elemente. 
' Er gehört nicht zur Standardvorlage. 
    ' Lege eine Instanz des Sammlungsobjekts an, das die  
    ' COrderItem-Objekte aufnimmt. 
    Set m_colItems = New Collection 
' Ende des Zusatzcodes für sich wiederholende <OrderItem>-Elemente.

Der folgende Code gehört in die Class_Terminate-Routine, wieder direkt hinter den TODO-Kommentar. Er gibt das Sammlungsobjekt wieder ans System zurück.

' Codeblock zur Einbindung sich wiederholender <OrderItem>-Elemente. 
' Er gehört nicht zur Standardvorlage. 
'Release the collection of items.  
'Set m_colItems = Nothing 
' Ende des Zusatzcodes für sich wiederholende <OrderItem>-Elemente.

Am Ende der Klassendatei bringen Sie noch den Code aus Listing L6 unter. Er bringt der Klasse die Collection-Semantik bei, einschließlich der Fähigkeit, die einzelnen Posten aus der Bestellung in For Each..Next-Schleifen aufzuzählen.

Listing L7 zeigt, wie der Zugriff auf unsere COrderItems-Objekte erfolgt. Er zeigt eine Funktion namens CreateDummyOrder, die aus unserem Projekt order_business stammt. Beachten Sie bitte, wie ein COrderItem erzeugt und initialisiert wird. Anschließend wird es in die COrderItems-Sammlung aufgenommen.

L6 Hier lernt die Klasse die Collection-Semantik

' Codeblock zur Einbindung sich wiederholender <OrderItem>-Elemente. 
' Er gehört nicht zur Standardvorlage. 
    ' Nimm ein COrderItem-Objekt auf. 
    Public Sub Add(ByVal OrderItem As COrderItem) 
        m_colItems.Add OrderItem 
    End Sub 
    ' Beschaffe das Objekt, das am gegebenen Index liegt. 
    Public Property Get Item(ByVal Index As Long) As COrderItem 
        Set Item = m_colItems(Index) 
    End Property 
    ' Setze ein neues Objekt an die Position, die dem gegebenen  
    ' Index entspricht. 
    Public Property Set Item( _ 
        ByVal Index As Long, _ 
        ByVal OrderItem As COrderItem _ 
    ) 
        Set m_colItems(Index) = OrderItem 
    End Property 
    ' Gib die Anzahl der enthaltenen Objekte an. 
    Public Property Get Count() As Long 
        Count = m_colItems.Count 
    End Property 
    ' Entferne ein Objekt. 
    Public Sub Remove(ByVal Index As Long) 
        m_colItems.Remove Index 
    End Sub 
    ' Für "For Each" 
    Public Property Get NewEnum() As IUnknown 
        Set NewEnum = m_colItems.[_NewEnum] 
    End Property 
' Ende des Zusatzcodes für sich wiederholende <OrderItem>-Elemente.

 

Das Beispielprogramm

Zur Illustration dieser Konzepte haben wir ein einfaches System zur Erfassung von Bestellungen entwickelt, das insgesamt vier Klassen nach unserer Methode implementiert. Zwei dieser Klassen haben Sie bereits kennen gelernt. Außerdem hat das Programm noch eine Schnittstelle zum Anwender und eine improvisierte Geschäftsdatenschicht, aus der ebenfalls hervorgeht, wie man diese Objekte benutzen würde.

Laden Sie die Projektgruppe xml.vbg in Visual Basic. Diese Gruppe enthält drei Projekte:

  • Order_xml implementiert die Klassen, die unser Beispiel-Schema verpacken. Damit repräsentiert es den Schwerpunkt dieses Artikels. Das Beispiel-Schema finden Sie in Listing L2.

  • Order_business implementiert eine improvisierte Geschäftsdatenschicht. Es enthält eine einzige Klasse, nämlich COrderAccessor, deren Aufgabe das Laden und Speichern von Bestellungen ist. Genau diese Lade- und Speicherfunktionen sind nur improvisiert. Dahinter steht hier nämlich keine Datenbank, in der man die Bestellungen normalerweise unterbringen würde.

  • Order_ui realisiert die hypothetische Präsentationsschicht. Das Projekt enthält ein einziges Formular, nämlich frmOrder. Es hat nur die Aufgabe, den Zugriff auf die beiden anderen Projekte zu ermöglichen.

Zum Start des Beispielprogramms geben Sie im Menü Ausführen den Befehl Starten. Dadurch wird das Startprojekt order_ui angeworfen. Nach dem Start werden Sie feststellen, dass noch keine Bestellungen eingegangen sind.

Zur Anzeige der einzigen erfassten Bestellung können Sie die Schaltfläche Open Order anklicken. Damit lösen Sie eine Aktualisierung der Anzeige aus (Bild B2). Das Beispielprogramm ermöglicht Ihnen, diese Bestellung durch zusätzliche Posten zu ergänzen. Mit einer durchdachten Methode zur Erfassung neuer Bestellungen oder zur Löschung der vorhandenen Bestellung kann es aber leider nicht dienen.

L7 Die Funktion z_CreateDummyOrder

Private Function z_CreateDummyOrder() As String 
    Dim Order   As New COrder 
    Dim Item    As COrderItem 
    With Order 
        ' Angaben zur Bestellung 
        .Contact = "Melissa Thompson" 
        .CreatedDate = Now() 
        .DueDate = DateAdd("m", 1, Now) 
        .ID = 5533390 
        ' Angaben über den Kunden 
        .Customer.Name = "Acme" 
        .Customer.State = "CA" 
        .Customer.Address = "10 Hedwig Lane" 
        .Customer.ID = 7888100 
        .Customer.ZIP = "90210" 
        .Customer.Telephone = "(555)555-2011" 
        .Customer.city = "Death Valley" 
        ' Einige Posten... 
        Set Item = New COrderItem 
        Item.Description = "This device will throw rocks far." 
        Item.Name = "Rock Catapult" 
        Item.ID = "667791" 
        Item.Quantity = 1 
        Item.Location = "Warehouse 23" 
        .OrderItems.Add Item 
        Set Item = New COrderItem 
        Item.Description = "These shoes will allow you to jump." 
        Item.Name = "Spring Shoes" 
        Item.ID = "1298791" 
        Item.Quantity = 5 
        Item.Location = "Warehouse 12" 
        .OrderItems.Add Item 
        Set Item = New COrderItem 
        Item.Description = "This device will allow you travel fast" 
        Item.Name = "Rocket Powered Roller-Skates" 
        Item.ID = "231545" 
        Item.Quantity = 1 
        Item.Location = "Warehouse 10" 
        .OrderItems.Add Item 
    End With 
    z_CreateDummyOrder = Order.XML 
End Function

Bild02

B2 Das Beispielprogramm enthält sogar schon eine Bestellung.

Die Baumdarstellung auf der linken Seite des Fensters zeigt die Hierarchie, die in der Bestellung herrscht. Die Wurzel stellt die Bestellung als Ganzes dar und die Blätter stehen für die einzelnen Posten. Auf der rechten Seite des Fensters erscheinen Angaben über den jeweils auf der linken Seite angewählten Posten (Bild B3).

Bild03

B3 Angaben über den jeweils angewählten Einzelposten.

Die Darstellung auf der rechten Seite kommt auf einem kleinen kreativen Umweg zustande. Das Beispielprogramm generiert "auf die Schnelle" den entsprechenden HTML-Code und übergibt ihn zur Anzeige an ein WebBrowser-Steuerelement (Das WebBrowser-Steuerelement sollten Sie spätestens dann im Visual Basic-Werkzeugkasten vorfinden, wenn Sie die Microsoft Internet Controls einbinden.).

Die Erzeugung des HTML-Codes erfolgt in zwei Schritten. Zuerst fordert das Programm vom zuständigen XML-Verpackungsobjekt eine XML-Version der Daten an, indem es auf dessen XML-Property zugreift. Wenn Sie sich den Code der Klasse CCustomer genauer anschauen, werden Sie feststellen, dass er ein XML-Property enthält. Die XML-Form der Daten erhält das Programm von diesem Property des CCustomer-Objekts. Die Routine Property Get XML ruft einfach CreateNode auf und verschafft sich auf diese Weise den DOM-Dokumentknoten, der die Klasse und ihre Hierarchie repräsentiert. Anschließend gibt sie dessen XML-Wert zurück. Zur Anzeige der Daten eines Einzelpostens wird das entsprechende COrderItem-Objekt derselben Prozedur unterzogen. Es hat ebenfalls ein XML-Property, mit dem sich die Anwendung die XML-Form der Objektdaten verschaffen kann.

Der zweite Schritt auf dem Weg zum HTML-Code besteht in der Umwandlung dieser XML-Daten durch ein MSXML-Objekt, und zwar nach der Vorschrift aus einer der XSL-Dateien, die zum Projekt gehören (order.xls zur Umwandlung der XML-Daten von einem COrder-Objekt und orderitem.xsl zur Transformation der XML-Daten, die von einem COrderItem-Objekt stammen). Der Übersichtlichkeit halber (und weil es in diesem Artikel nicht um XSL-Transformationen geht) möchten wir an dieser Stelle nicht weiter auf die Erstellung der XSL-Dateien eingehen. Sie finden die XSL-Dateien für unser Beispielprogramm aber auf der Begleit-CD dieses Hefts. Das Ergebnis der Umwandlung ist der HTML-Code, der an das WebBrowser-Steuerelement weitergegeben wird.

Bild04

B4 Die rohen XML-Daten.

Zur Anzeige des XML-Codes, der die Darstellung steuert, klicken Sie die Schaltfläche View Raw XML an. Auf der rechten Seite des Fensters erscheint dann der noch nicht umgewandelte rohe XML-Code (Bild B4). Ein weiterer Klick auf die Schaltfläche View Raw XML führt wieder zur normalen Anzeige. Falls der XML-Code nicht in einem hierarchischen Format erscheint, wie in Bild B4 gezeigt, sondern in einer einzigen Zeile, so haben Sie wahrscheinlich noch nicht den Internet Explorer 5.0 installiert. Mit Hilfe der horizontalen Rolleiste können Sie sich trotzdem den gesamten XML-String ansehen. Er ist dann nur nicht so schön formatiert.

 

Raum für Erweiterungen

Je nach Ihren Ansprüchen bieten sich eine ganze Reihe von Erweiterungen unserer Methode an. Beachten Sie bitte, dass bei manchen Erweiterungen aber beträchtliche Änderungen in der Klassenvorlage anfallen.

Die erste Erweiterung wäre die Darstellung aller Elemente als Klassen. Bei unserer Methode werden Elemente, die keine Kindelemente haben, einfach in der Klasse des Elternelements als Properties geführt. Bei dieser Unterbringung können solche Elemente aber keine Attribute haben. Diese Beschränkung ließe sich überwinden, indem man alle Elemente als Klassen repräsentiert.

Die nächste Erweiterung, die sich anbietet, ist die Änderung des Namensschemas für die Klassenproperties. Falls im XML-Schema Elementnamen auftauchen, die zu den gültigen Schlüsselwörtern von Visual Basic gehören, werden Sie diese Elementnamen wohl kaum als Propertynamen verwenden können.

Außerdem könnten Sie die Klassenvorlage abspeicherbar machen (Stichwort: Persistenz). Speichern Sie in den Routinen ReadProperties und WriteProperties nur das XML-Property der Klasse in einem PropertyBag.

Und schließlich könnten Sie noch ein Add-In für Visual Basic entwickeln, das ein XML-Schema einliest und anhand der Klassenvorlage die entsprechenden Verpackungsklassen generiert.

 

Ein Wort zum Schluss

Wir haben die Methode, die wir Ihnen in diesem Artikel vorgestellt haben, auch in "richtigen" Anwendungen eingesetzt. Sie ermöglicht die schnelle und einfache Entwicklung von XML-Anwendungen, und zwar selbst dann, wenn die XML-Hierarchie schon relativ komplex wird.

Der wichtigste Vorteil der Klassenvorlage, die wir Ihnen in diesem Artikel vorgestellt haben, und der damit entwickelten Klassen dürfte wohl sein, dass ein Entwickler damit auf die Leistungsfähigkeit von MSXML zurückgreifen kann, ohne dessen Objektmodell zu kennen.