XPath-Abfragen von Objekten mit ObjectXPathNavigator

 

Steve Saxon
Dell Computer Corporation

2. März 2003

Zusammenfassung: Steve Saxon erläutert den ObjectXPathNavigator, der die Leistungsfähigkeit und Flexibilität des XML-Programmiermodells bietet, ohne dass ein gesamtes Objektdiagramm in ein XmlDocument-Objekt serialisiert werden muss. (21 gedruckte Seiten)

Laden Sie die datei XML03172003_sample.exe herunter.

Anforderungen Für diesen Download muss das Microsoft.NET Framework 1.0 installiert sein.

Anmerkung desHerausgebers Die Monats-Installation von Extreme XML wurde von Gast-Kolumnist Steve Saxon geschrieben. Steve lebt in Austin, Texas und ist technischer Architekt für Worldwide Content Publishing auf Dell.com. Er arbeitet seit mehreren Jahren eng mit Microsoft XML-Produkten zusammen und leitete das Team, das das interne XML-basierte Inhaltsveröffentlichungssystem von Dell im Mai 2001 über ASP.NET in Betrieb nahm (bevor Beta 2 veröffentlicht wurde!). Steve veröffentlicht seit 1998 Webinhalte aus XML und hat sogar ein Kapitel zu diesem Thema im Sybex-Buch Mastering XML mitverfasst.

Einführung

Seit xml in den späten 1990er Jahren erstmals auf die Bühne kam, haben Entwickler versucht, effiziente Wege zu finden, um ihre Objektmodelle in XML zu integrieren, damit sie ihre Daten übergeben oder einfach die Daten mithilfe von Mechanismen wie XSLT bearbeiten können. Am Anfang schrieben entwickler die Daten direkt heraus, aber dies war sehr unzuverletzlich, da Sie mit der Semantik von XML vorsichtig sein mussten und spezielle Fälle behandeln mussten, z. B. wenn Ihre Daten reservierte Zeichen wie "<" und "&" enthielten.

Das Microsoft .NET Framework hat uns noch einfachere Möglichkeiten direkt aus der Box gegeben. Wir können nun die System.Xml lassen. Die XmlTextWriter-Klasse kümmert sich um die XML-Semantik. Diese Klasse ist relativ einfach zu verwenden, aber wie das folgende Beispiel zeigt, kann ausführlich sein.

Verwenden von XmlTextWriter zum Ausgeben von XML

XmlTextWriter writer = new XmlTextWriter( Console.Out );

writer.Formatting = Formatting.Indented;

writer.WriteStartElement( "sampleNode" );
writer.WriteAttributeString( "anAttribute", "Test" );
writer.WriteStartElement( "nestedNode" ); 
writer.WriteString( "XmlTextWriter is easy" );
writer.WriteEndElement();
writer.WriteEndElement();

Ausgabe

<sampleNode anAttribute="Test">
    <nestedNode>XmlTextWriter is easy</nestedNode>
</sampleNode>

Wenn wir uns nicht um die genaue Darstellung der Ausgabe kümmerten, könnten wir die System.Xml haben. Serialization.XmlSerializer-Klasse serialisieren das gesamte Objektdiagramm in einem Methodenaufruf. Alle diese Mechanismen weisen jedoch einen gemeinsamen Fehler auf: Es gibt keine Möglichkeit, diese Ausgabe zu filtern oder abzufragen, bis der gesamte Datenstrom in ein XmlDocument-Objekt geladen wird, auch wenn Sie nur an einem oder zwei Knoten aus der Ausgabe interessiert sind.

In diesem Artikel erfahren Sie, wie Sie XPath-Abfragen und XSLT direkt für Ihre Objekte verwenden können, ohne serialisieren zu müssen.

Warum direkte Abfragen?

Möglicherweise fragen Sie sich, warum Sie XPath direkt für Ihre Objekte verwenden möchten? Wenn es nicht so schwierig ist, es zu serialisieren, warum tun Sie es nicht einfach, dann lesen Sie es erneut ein, und fragen Sie es ab.

Abgesehen von den offensichtlichen Leistungsauswirkungen eines solchen Schemas ist es unbeholfen, Ihre Anwendung auf diese Weise codieren zu müssen, da dies bedeutet, temporäre XML-Dokumente zu erstellen (obwohl Sie sie im Arbeitsspeicher mit System.IO.MemoryStream aus dem .NET Framework erstellen könnten). Daher würden die meisten Entwickler wahrscheinlich nicht in Erwägung ziehen, so etwas zu tun, was ich schade finde, da es eine Reihe von hervorragenden Möglichkeiten gibt, die sich eröffnen, sobald Sie erwägen, Ihre Objekte über XPath verfügbar zu machen.

Komplexe Objektabfragen

Die erste und vielleicht offensichtlichste Möglichkeit ist die komplexe Objektabfrage. Damit meine ich die Idee, Abfragen schreiben zu können, die mehrere Teile der internen Struktur des Objekts auf eine komplexe, möglicherweise willkürliche Weise berühren.

Betrachten Sie beispielsweise den Code, den Sie möglicherweise erstellen müssen, wenn Sie ein Order Form-Objekt geschrieben haben und eine Anforderung erhalten haben, "alle Elemente in diesem Bestellformular zu finden, die nach Texas geliefert werden sollen". Das Schreiben von Langhandcode für solche Nachschlagevorgänge kann mühsam sein, da Sie einen Drilldown zur richtigen Geschäftsobjektauflistung durchführen und diese dann auflisten müssen, um das Feld abzugleichen. Möglicherweise haben Sie folgendes:

ArrayList texasItems = new ArrayList();

foreach( LineItem item in this.Items )
{
    if( item.ShippingAddress.State.StateCode == "TX" )
    {
        texasItems.Add( item );
    }
}

Stellen Sie sich vor, Sie erhalten eine neue Anforderung, diese Suche zu ändern, um "es sei denn, die Lieferadresse entspricht einer der Abrechnungsadressen". Plötzlich wird Ihr Code viel komplizierter, da er dann eine logische Verknüpfung mit einem anderen Teil Ihres Geschäftsobjektdiagramms durchführen muss. Dies ist die Art von Sache, für die XPath sich hervorragend eignet, aber bisher gab es keine Möglichkeit, XPath direkt für Ihre Objekte zu verwenden.

Das Problem der Nichtübereinstimmung zwischen Client und Server

Das zweite Problem, bei dem XPath-Aktivierung Ihrer Objekte hilfreich ist, ist, dass ein Teil der internen Daten Ihrer Anwendung für die Außenwelt verfügbar gemacht werden muss. Betrachten Sie ein Szenario, in dem zwei verschiedene Teams über zwei Anwendungen verfügen, die Geschäftsobjekte zwischen ihnen übergeben müssen. Wenn Sie Glück haben, kann es möglich sein, denselben Geschäftsobjektcode oder dasselbe Objektmodell zu teilen. In diesem Fall können Sie Mechanismen wie Webdienste oder .NET Remoting verwenden, um zwischen den beiden Anwendungen zu kommunizieren. Was tun Sie jedoch, wenn die beiden Anwendungen über ganz unterschiedliche interne Objektmodelle verfügen?

Wenn Sie bereits entschieden haben, dass die beiden Anwendungen über XML kommunizieren müssen (sodass Sie nicht an binäre Protokolle gebunden sind), können Sie, wie oben beschrieben, einfach die Objekte aus der ersten Anwendung serialisieren und an die zweite Anwendung übergeben. Wenn sich die Objektdiagramme unterscheiden, können Sie den XML-Code über eine XSLT-Transformation übergeben, um sie in das von der Zielanwendung erwartete Format zu übertragen. Der Nachteil dieses Ansatzes besteht darin, dass die zweite Anwendung nur einen Bruchteil der Ausgabe der Clientanwendung sein muss. In diesem Fall werden viele Daten verworfen, was bedeutet, dass Sie viel Zeit mit der Serialisierung von Daten verschwendet haben, die nie verwendet wurden.

Wie ich bereits gezeigt habe, können Sie sich dafür entscheiden, dass die Clientanwendung die XML-Datei Stück für Stück erstellt, um sie direkt in das Format zu bringen, das die Zielanwendung versteht. Wie wir jedoch gesehen haben, kann XmlTextWriter ziemlich mühsam zu verwenden sein, und der resultierende Code ist schwer zu lesen und zu verwalten. Außerdem müssen Sie Codeänderungen vornehmen, wenn der Client oder die Zielanwendung ihr Objektmodell ändert.

In einer idealen Welt könnten Sie alle Vorteile von XSLT nutzen (der Standard hier, dass Sie die Client-/Zieldatenzuordnung außerhalb des Codes Ihrer Anwendung verwalten können). Dabei möchten Sie jedoch im Idealfall, dass die XSLT-Transformation direkt über Ihre Objekte ausgeführt werden kann, wenn ein Großteil der Clientdaten ignoriert wird.

Einführung in XPathNavigator

Wenn Sie MSXML jemals verwendet haben, sind Sie zweifellos mit der DOM- oder Dokumentobjektmodell-API vertraut, die sie für die Bearbeitung von XML bereitstellt. Als die .NET Framework veröffentlicht wurde, war es keine Überraschung, dass sie einen DOM-API-Satz enthielt. Was Sie möglicherweise noch nicht erkannt haben, ist, dass Microsoft eine ganz andere Möglichkeit zum Bearbeiten von XML-Daten enthält – das XPathNavigator-Modell . Im Gegensatz zum knotenbasierten Dokumentobjektmodell bietet XPathNavigator ein schreibgeschütztes, strukturbasiertes Cursormodell zum Durchlaufen eines XML-Datasets. Was Sie möglicherweise nicht erkannt haben, ist, dass XPathNavigator nicht speziell auf XML beschränkt ist. Mit der entsprechenden Implementierung können Sie sie verwenden, um das Dateisystem, die Windows-Registrierung®, einen Active Directory-Speicher® oder eine andere Art von hierarchischen Datenspeicher abzufragen.

Wie sieht es mit Objekten aus? Auch sie haben eine Art implizite Hierarchie für sie. Sie haben das Objekt, mit dem Sie beginnen, und alle seine Eigenschaften sind wie Attribute oder untergeordnete Knoten in XML. Wenn Sie jemals einen Drill in ein Objekt im Überwachungsfenster in Microsoft Visual Studio® .NET durchgeführt haben, wissen Sie, was ich meine.

Klicken Sie für ein größeres Bild

Abbildung 1. Microsoft Visual Studio .NET Watch-Fenster

Geben Sie ObjectXPathNavigator ein.

Wenn wir eine Möglichkeit hätten, einen XPathNavigator für die Navigation über ein Objektdiagramm zu erstellen, könnten wir XPath-Abfragen ausführen, damit wir komplexe Abfragen direkt für unsere Objekte schreiben können. Sie können den XPathNavigator auch direkt über das Objekt in eine XSLT-Transformation übergeben, ohne es zuerst serialisieren zu müssen. Das XSLT-Objekt könnte dann nach Bedarf ihre Objekthierarchie durchlaufen und nur die Objekte berühren, auf die Sie verwiesen haben. Genau das macht mein ObjectXPathNavigator .

Um Ihnen zu zeigen, wie es funktioniert, habe ich ein Beispiel um einen Warenkorb herum erstellt, der eine Reihe von Elementen enthält. Alle Elemente sind vom Typ Book oder PowerTool (z. B. ein Power Drill). Da alle Warenkorbelemente bestimmte Dinge gemeinsam haben, habe ich beide eine Basisklasse namens Item verwenden, die Eigenschaften wie Description, Quantity und UnitPrice bereitstellt. Ich habe auch ein Array von Zeichenfolgen hinzugefügt, um das Element zu kategorisieren und zu sehen, wie Sammlungen herauskommen.

public class Item
{
    private string      m_desc;
    private int         m_quantity;
    private decimal     m_unitPrice;
    private string[]    m_cat;

    protected Item( string desc, int quantity, decimal unitPrice,
                    string[] cat )
    {
        m_desc        = desc;
        m_quantity    = quantity;
        m_unitPrice    = unitPrice;
        m_cat        = cat;
    }

    public string Description
    {
        get { return m_desc; }
    }

    public int Quantity
    {
        get { return m_quantity; }
    }

    public decimal UnitPrice
    {
        get { return m_unitPrice; }
    }
    
    public string[] Categories
    {
        get { return m_cat; }
    }
}

public class Book : Item
{
    public Book( string desc, int quantity, decimal unitPrice, string[] 
    cat )
        : base( desc, quantity, unitPrice, cat )
    {
    }
}

public class PowerTool : Item
{
    public PowerTool( string desc, int quantity, decimal unitPrice, 
    string[] cat )
        : base( desc, quantity, unitPrice, cat )
    {
    }
}

Nachdem nun die grundlegenden Warenkorbelemente funktionieren, erstellen wir tatsächlich eine Sammlung von ihnen, um unseren Warenkorb darzustellen:

ArrayList basket = new ArrayList();

basket.Add( new Book( "Repair your car with twine", 100, 25,
                 new string[] { "Automotive", "Crafts" } ) );
basket.Add( new PowerTool( "Uber Drill 9000", 2, 1200,
                 new string[] { "Drills" } ) );
basket.Add( new PowerTool( "Texas Chainsaw", 1, 1700,
                 new string[] { "Saws", "Tree Care" } ) );
basket.Add( new Book( "Quantum Physics for Beginners", 2, 50,
                 new string[] { "Science" } ) );

Nun erstellen wir eine XPathNavigator-instance, um XPath mit diesem Objekt zu verwenden:

XPathNavigator    nav = new ObjectXPathNavigator( basket );
XPathNodeIterator iter;

Probieren wir es aus. Zunächst finde ich alle PowerTool-Objekte im Warenkorb. Die XPath-Abfrage hierfür ist , was bedeutet, dass es mir egal ist "/*/PowerTool", wie der Dokumentelementknoten heißt (d. h. der Name der Auflistung selbst), aber alle Darin genannten PowerTool Elemente finden:

iter = nav.Select( "/*/PowerTool" );

while( iter.MoveNext() )
{
    // get the Description attribute (value of the Item.Description 
    property)
    Console.WriteLine(iter.Current.GetAttribute( "Description", 
    string.Empty ) );
}

Wenn wir diesen Code ausführen, sollte die folgende Ausgabe angezeigt werden:

Uber Drill 9000
Texas Chainsaw

Was in dieser Ausgabe möglicherweise nicht offensichtlich ist, ist, dass ich tatsächlich jede Abfrage ausführen könnte, die XPath zulässt. Betrachten wir eine viel umfangreichere Abfrage, z. B. das Suchen aller Elemente im Warenkorb mit einer Einzelpositionssumme (Einzelpreis * Menge) über 2.000 USD:

iter = nav.Select( "/*/*[(@UnitPrice * @Quantity) > 2000]" );

while( iter.MoveNext() )
{
    string itemTotal = iter.Current.Evaluate( "@UnitPrice * @Quantity" 
    ).ToString();

    // get the Description attribute (value of the Item.Description 
    property)
    Console.WriteLine(iter.Current.GetAttribute( "Description", 
    string.Empty )
                      + " = " + itemTotal );
}

Wenn wir diesen Code ausführen, sollte die folgende Ausgabe angezeigt werden:

Repair your car with twine = 2500
Uber Drill 9000 = 2400

Beachten Sie, wie der zugrunde liegende XPathNavigator die UnitPrice - und Quantity-Eigenschaften des -Objekts abfragen und diese zusammen multiplizieren konnte (obwohl es sich um einen Dezimaltyp und das andere um eine ganze Zahl handelt).

Die XPath-Abfrage hier kann in Laienbegriffen als übersetzt werden: "Mir ist es egal, wie der Dokumentelementknoten heißt, und es ist mir egal, wie die Darin enthaltenen Elemente genannt werden, aber ich möchte diejenigen finden, bei denen ihre UnitPrice * Quantity über 2000 ist."

Zum Schluss versuchen wir noch einige XSLT-Daten. Wir verwenden eine einfache XSLT, um die Book - und PowerTool-Objekte separat zu gruppieren und als XML anzuzeigen. Zunächst müssen wir das XSLT-Stylesheet in den Arbeitsspeicher laden:

XslTransform transform = new XslTransform();

// navigate from the bin\debug folder up to the project folder
string       path      = Path.Combine( Environment.CurrentDirectory,
                                       @"..\..\sample.xslt" );

// load the XSLT stylesheet
transform.Load( path );

Die Datei sample.xslt sieht wie folgt aus (der Einfachheit halber habe ich die gesamte Datei nicht eingeschlossen):

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:msxsl="urn:schemas-microsoft-com:xslt">

    <xsl:template match="/">
        <!-- calculate the item totals -->
        <xsl:variable name="itemTotals">
            <xsl:for-each select="/*/*">
                <ItemTotal><xsl:value-of select="@UnitPrice * @Quantity" />
                </ItemTotal>
            </xsl:for-each>
        </xsl:variable>

        <basket>
            <!-- put the subtotal in an attribute -->
            <xsl:attribute name="SubTotal">
                <xsl:value-of select="sum(msxsl:node-set
                ($itemTotals)/ItemTotal)" />
            </xsl:attribute>
            
            <!-- copy the power tools -->
            <powertools>
                <xsl:copy-of select="/*/PowerTool" />
            </powertools>

            <!-- copy the Books -->
            <books>
                <xsl:copy-of select="/*/Book" />
            </books>
        </basket>
    </xsl:template>

</xsl:stylesheet>

Ein interessantes Feature dieser XSLT ist, dass die funktion sum()XPath verwendet wird, um ein Teilergebnis aller Elemente im Warenkorb zu berechnen. Dazu habe ich zuerst XSLT erhalten, um ein XML-Dokument im Arbeitsspeicher mit <xsl:variable>zu erstellen, das ich dann mit einer Gruppe von Knoten aufgefüllt habe, die die berechnete Elementsumme für ein bestimmtes Zeilenelement enthalten:

<xsl:variable name="itemTotals">
    <xsl:for-each select="/*/*">
      <ItemTotal><xsl:value-of select="@UnitPrice * @Quantity" />
      </ItemTotal>
    </xsl:for-each>
</xsl:variable>

Von dort aus wird die Summe mit der summe() -Funktion von XPath wie folgt berechnet:

<xsl:value-of select="sum(msxsl:node-set($itemTotals)/ItemTotal)" />

Nun erstellen wir einen XmlTextWriter , damit wir einen schön eingerückten XML-Code aus der XSLT-Transformation erhalten:

XmlTextWriter writer = new XmlTextWriter( Console.Out );
writer.Formatting = Formatting.Indented;

Schließlich müssen wir den ObjectXPathNavigator aus unserem Basket-Objekt erstellen und die XSLT-Transformation aufrufen:

XPathNavigator nav = new ObjectXPathNavigator( basket );
transform.Transform( nav, null, writer );

Wenn wir diesen Code ausführen, erhalten wir die folgende Ausgabe:

<basket SubTotal="6700" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
  <powertools>
    <PowerTool UnitPrice="1200" Quantity="2" Description="Uber Drill 
    9000">
      <Categories>
        <String>Drills</String>
      </Categories>
    </PowerTool>
    <PowerTool UnitPrice="1700" Quantity="1" Description="Texas Chainsaw">
      <Categories>
        <String>Saws</String>
        <String>Tree Care</String>
      </Categories>
    </PowerTool>
  </powertools>
  <books>
    <Book UnitPrice="25" Quantity="100" Description="Repairing your car 
    with twine">
      <Categories>
        <String>Automotive</String>
        <String>Crafts</String>
      </Categories>
    </Book>
    <Book UnitPrice="50" Quantity="2" Description="Quantum Physics for 
    Beginners">
      <Categories>
        <String>Science</String>
      </Categories>
    </Book>
  </books>
</basket>

IXPathNavigable– XPath-Aktivierung Ihrer Objekte

Bevor ich auf die Funktionsweise von XPathNavigator eingeht, wollte ich auf eine coole Ergänzung hinweisen, die ich an diesem Code hätte vornehmen können, wodurch das gesamte XPath-Ding leicht in Ihren vorhandenen Anwendungen ausgenutzt werden kann.

System.Xml stellt eine wenig bekannte Schnittstelle namens IXPathNavigable bereit, die eine einzelne Methode implementiert, CreateNavigator(), die einen XPathNavigator-instance zurückgibt. Noch interessanter ist, dass die XSLT-Transformation eine instance dieser Schnittstelle als Parameter anstelle eines XPathNavigators akzeptiert.

Dies führt uns zu einer interessanten Idee. Wenn Sie Objekte haben, die Sie mit XPath oder XSLT verwenden möchten, sollten Sie ihre Objekte dazu bringen, IXPathNavigable zu implementieren und einen neuen ObjectXPathNavigator-instance in der CreateNavigator()-Methode zurückzugeben, um es an den Konstruktor zu übergeben:

public class YourClass : IXPathNavigable
{
    public XPathNavigator CreateNavigator()
    {
        return new ObjectXPathNavigator( this );
    }

    // the rest of your class goes here
}

Wenn ich meine eigene Basket-Klasse (anstelle der einfachen ArrayList, die ich zuvor verwendet habe) erstellt und die IXPathNavigable-Schnittstelle implementiert hätte, könnte ich den Warenkorb selbst direkt an eine XSLT-Transformation übergeben:

Basket basket = new Basket();

basket.Add( new Book( "Repair your car with twine", 100, 25,
                 new string[] { "Automotive", "Crafts" } ) );

XslTransform transform = new XslTransform();
transform.Load("sample.xslt" );

// now give the basket directly to the transform!
transform.Transform( basket, null, writer );

Grundlegendes zum XPathNavigator-Cursormodell

Wie bereits erwähnt, basiert XPathNavigator auf einem schreibgeschützten Cursormodell. Das bedeutet, dass Sie nicht wie beim Dokumentobjektmodell einen Drilldown durch eine Knotenstruktur durchführen, sondern über einen Cursor verfügen, der sich im zugrunde liegenden XML-Code des Navigators bewegt. Abbildung 2 zeigt einige der XPathNavigator-Cursorvorgänge , die Sie möglicherweise ausführen können, wenn Sie sich derzeit in einem Item in einem Basket-Objekt befinden.

Abbildung 2. Verschieben eines XPathNavigators von einem Knoten auf einen anderen im XML-Code

Der folgende Code zeigt dieselbe Struktur in herkömmlicher XML-Form.

<Basket>
    <Item Desc="Uber Drill 9000">
        <Category />
    </Item>
    <Item />
</Basket>

Die XPathNavigator-Implementierung

XPathNavigator ist eine abstrakte Basisklasse. Das bedeutet, dass es an sich keine Reihe der verfügbar gemachten Eigenschaften und Methoden implementiert. Wenn Sie Ihre eigene Klasse von XPathNavigator ableiten möchten, müssen Sie mindestens die in den folgenden Tabellen aufgeführten abstrakten Eigenschaften und Methoden implementieren.

Abstract-Eigenschaft Definition
Baseuri Ruft den Basis-URI für den aktuellen Knoten ab.
HasAttributes Ruft einen Wert ab, der angibt, ob der Elementknoten über Attribute verfügt.
HasChildren Ruft einen Wert ab, der angibt, ob der aktuelle Knoten über untergeordnete Knoten verfügt.
IsEmptyElement Ruft einen Wert ab, der angibt, ob der aktuelle Knoten ein leeres Element ist (z.B. <MyElement/>).
Localname Ruft den Namen des aktuellen Knotens ohne das Namespacepräfix ab.
Name Ruft den gekennzeichneten Namen des aktuellen Knotens ab.
Namespaceuri Ruft den Namespace-URI (wie in der W3C-Namespacespezifikation definiert) des aktuellen Knotens ab.
NameTable Ruft die dieser Implementierung zugeordnete XmlNameTable ab.
NodeType Ruft den Typ des aktuellen Knotens ab.
Präfix Ruft das Präfix ab, das dem aktuellen Knoten zugeordnet ist.
Wert Ruft den Textwert des aktuellen Knotens ab.
XmlLang Ruft den xml:lang-Bereich für den aktuellen Knoten ab.
Abstrakte Methode Definition
Clone() Erstellt einen neuen XPathNavigator , der auf demselben Knoten wie dieser XPathNavigator positioniert ist.
GetAttribute() Ruft den Wert des Attributs mit dem angegebenen LocalName und NamespaceURI ab.
GetNamespace() Gibt den Wert des Namespaceknotens zurück, der dem angegebenen lokalen Namen entspricht.
IsDescendant() Bestimmt, ob der angegebene XPathNavigator ein Nachkomme des aktuellen XPathNavigators ist.
IsSamePosition() Bestimmt, ob sich der aktuelle XPathNavigator an derselben Position wie der angegebene XPathNavigator befindet.
MoveTo() Bewegt sich an dieselbe Position wie der angegebene XPathNavigator.
MoveToAttribute() Wechselt zum Attribut mit übereinstimmenden LocalName und NamespaceURI.
MoveToFirst() Wechselt zum ersten gleichgeordneten Knoten des aktuellen Knotens.
MoveToFirstAttribute() Wechselt zum ersten Attribut.
MoveToFirstChild() Wechselt zum ersten untergeordneten Element des aktuellen Knotens.
MoveToFirstNamespace() Verschiebt den XPathNavigator auf den ersten Namespaceknoten des aktuellen Elements.
MoveToId() Wechselt zu dem Knoten, der über ein Attribut vom Typ ID verfügt, dessen Wert mit der angegebenen Zeichenfolge übereinstimmt.
MoveToNamespace() Verschiebt den XPathNavigator in den Namespaceknoten mit dem angegebenen lokalen Namen.
MoveToNext() Wechselt zum nächsten Gleichgeordneten des aktuellen Knotens.
MoveToNextAttribute() Wechselt zum nächsten Attribut.
MoveToNextNamespace() Verschiebt den XPathNavigator auf den nächsten Namespaceknoten.
MoveToParent() Wechselt zum übergeordneten Knoten des aktuellen Knotens.
MoveToPrevious() Wechselt zum vorherigen gleichgeordneten Knoten des aktuellen Knotens.
MoveToRoot() Wechselt zum Stammknoten, zu dem der aktuelle Knoten gehört.

Das Interessante an diesen Methoden ist nicht die Zahl, sondern dass Sie keine Implementierung für die komplexeste, aber am häufigsten verwendete Methode , Select(), bereitstellen müssen. Die Select()- Methode ist die XPath-Funktion "magic happens here", die Ihren XPath-Ausdruck übernimmt und versucht, Knoten zu finden, die dem angeforderten Entsprechen. Die XPathNavigator-Basis verwendet all diese kleinen atomaren Move-Methoden , um die XML-Struktur zu durchlaufen, Attribute abzufragen und was sonst noch immer erforderlich ist, um die angeforderten Knoten zu finden, unabhängig davon, wie die XML-Struktur physisch implementiert ist. Dies ist ein entscheidender Vorteil abstrakter Basisklassen wie XPathNavigator gegenüber Schnittstellen. Während Sie bei einer Schnittstelle die gesamte Implementierung bereitstellen müssen, kann eine abstrakte Basisklasse zumindest einige allgemeine oder komplexe Aufgaben für Sie ausführen.

Erstellen eines Just-in-Time-Objektproxys

Wie wird das Cursormodell einem zugrunde liegenden Knoten zugeordnet? Denken Sie daran, dass Sie im Dokumentobjektmodell von Knoten zu Knoten wechseln, aber mit dem Cursormodell den Cursor tatsächlich verschieben. Dies bedeutet, dass in unserem Modell der Cursor und der zugrunde liegende Knoten nicht dasselbe Objekt sind.

Um dieses Ergebnis zu erzielen, habe ich Proxyobjekte erstellt, die der Navigator zum Drillup oder Drilldown durch das zugrunde liegende Objektmodell verwendet. Diese Proxyobjekte helfen, wenn ich von Knoten zu Knoten wechseln muss, da ich beim Erstellen des Proxys bereits herausgearbeitet habe, welche Knoten vorhanden sind. Es ermöglicht auch eine spezialisierte Behandlung für bestimmte Typen wie Sammlungen und Wörterbücher, ohne dass der Navigator selbst überkompliziert werden muss.

Um ein natürlich aussehendes XML zu erhalten, habe ich mich dazu entschieden, einfache Typen wie Zeichenfolgen und Zahlen Attributen zuzuordnen und nur zu einem untergeordneten Element für einen komplexen Typ wie ein Objekt, auf das verwiesen wird, oder für Sammlungen und Wörterbücher zu wechseln.

Schließlich, da wir nur einige der untergeordneten Objekte berühren, habe ich die untergeordneten Objektproxys als Just-in-Time erstellt, sodass sie nur funktionieren, wenn sie angefordert werden. Auf diese Weise erspare ich mir wieder die Kosten für das Erstellen von Proxys für das gesamte Objektdiagramm (was am Ende des Tages nicht besser wäre als serialisieren).

Die Rolle des Objektproxys besteht darin, den Aufwand zu reduzieren, der für die Navigation zu einem bestimmten Objekt erforderlich ist, indem er eine einmalige Bestimmung der Eigenschaftswerte des Objekts und Verweise auf andere Objekte durchführt. Wie spielt also das XPathNavigator-Cursormodell in all dies ein? Wenn Sie objectXPathNavigator zum ersten Mal erstellen, wird ein Objektproxy für das Objekt erstellt, das Sie übergeben (das Objekt, über das navigiert wird). Dieses Objekt entspricht dem document-Element in XML. Unsere anfängliche Cursorposition befindet sich jedoch auf dem sogenannten Stammknoten in XPath, der als normal dem XML-Dokument selbst und nicht einem bestimmten Knoten darin entspricht.

Abbildung 3. Beziehung zwischen dem XPathNavigator und dem Objektproxy

Wenn Sie MoveToFirstChild()aufrufen, wird der Cursor zum Objektproxy des document-Elements verschoben, und der aktuelle Knotentyp ist auf XPathNodeType.Element festgelegt. Von hier aus können wir zum ersten Attribut oder zu diesem ersten untergeordneten Element des Knotens navigieren. Wenn wir den Objektproxy nach diesen Informationen fragen, verwendet er einen Just-in-Time-Initialisierungsmechanismus, um das Attribut und die untergeordneten Auflistungen zu erstellen, auf denen wir dann einen Drilldown durchführen können. Dieses Muster wird dann wiederholt, wenn wir uns durch die Struktur nach unten bewegen.

Die MoveToFirstChild()- Methode muss auch über eine spezielle Behandlung für Objekte verfügen, die Text verfügbar machen (z. B. ein Zeichenfolgenobjekt oder ein anderer Werttyp). In diesem Fall ändern wir den Knotentyp in XPathNodeType.Text, sodass wir wissen, dass wir uns im Textkontext des aktuellen Objekts und nicht im Element befinden.

public override bool MoveToFirstChild()
{
    if( m_nodeType == XPathNodeType.Root )
    {
        // move to the document element
        this.MoveNavigator( m_docElem );                
        return true;
    }
    
    if( m_nodeType != XPathNodeType.Element )
    {
        return false;
    }

    // drop down to the text value (if any)
    if( m_currentElem.HasText )
    {
        m_nodeType   = XPathNodeType.Text;
        m_values     = null;
        m_valueIndex = -1;
        
        return true;
    }        

    // drop down to the first element (if any)
    IList coll    = m_currentElem.Elements;
                
    if( coll.Count > 0 )
    {
        MoveNavigator( (ObjectXPathProxy) coll[0] );
        
        return true;
    }

    return false;
}

Der Objektproxy selbst macht mehrere Eigenschaften und Methoden zur Verwendung durch die XPathNavigator-Implementierung verfügbar, um sie vom zugrunde liegenden gebundenen Objekt zu abstrahieren. Eine typische ObjectXPathNavigator-Eigenschaft ist unten dargestellt.

public bool HasChildren
{
    get
    {
        Activate();

        return ( m_elements != null ) || HasText;
    }
}

Beachten Sie den Activate() -Aufruf, der sicherstellt, dass der Proxy ordnungsgemäß initialisiert wird, bevor ich versuche, auf die untergeordneten Elemente des Knotens in der nächsten Zeile zu verweisen. Der Activate() -Aufruf führt eine Just-in-Time-Initialisierung des Objekts durch, wenn es zum ersten Mal aufgerufen wird. Während dieser Initialisierungsproxyphase untersuche ich die Eigenschaften des gebundenen Objekts. Wenn der Eigenschaftswert eine Zeichenfolge oder einen Werttyp (z. B. eine ganze Zahl) zurückgibt, füge ich ihn einer Auflistung von Attributnamen-Wert-Paaren hinzu. Andernfalls erstelle ich einen weiteren Proxy, der an das von der -Eigenschaft zurückgegebene Objekt gebunden ist, und füge diesen neu erstellten Proxy der Elements-Auflistung hinzu.

Warum nicht einfach das Objekt im Konstruktor vollständig initialisieren? Zugegeben, dafür ist es, aber in diesem Fall wissen wir nicht, wann oder sogar ob die verschiedenen Methoden in unserem Objektproxy aufgerufen werden. Der Sinn dieser Übung besteht darin, jemandem zu ermöglichen, unser Objekt entweder direkt über XPath oder auf komplexere Weise mithilfe von XSLT abzufragen. Zweitens gibt es einige Eigenschaften, die Activate() nicht aufrufen müssen, da sie die Daten, die Activate() initialisiert, nicht benötigen oder verwenden. Beispielsweise die Value-Eigenschaft , die einfach den ToString() -Wert des Objekts zurückgibt, an das der Proxy gebunden ist:

public string Value
{
    get
    {
        if( HasText )
        {
            return m_binding.ToString();
        }
      
        return string.Empty;
    }
}

Wenn ich also nichts anderes als einen <xsl:value-of select="." /> gegen unseren Objektproxy tue, verschwende ich keine unnötige Zeit beim Initialisieren von Sammlungen von untergeordneten Daten, die während dieses Aufrufs nicht erforderlich sind.

Behandeln von Attributen

Ein besonderer Aspekt bei der XPathNavigator-Implementierung ist die Handhabung von Attributen. Für Objekte, auf die verwiesen wird, erstellt der Objektproxy ein Array von untergeordneten Objektproxys, die dann vom XPathNavigator als untergeordnete Elemente verfügbar gemacht werden.

Zeichenfolgen- und Wertetypen werden dagegen als Wörterbuch implementiert, wobei der ursprüngliche Eigenschaftsname dem Wert zugeordnet wird, den der XPathNavigator als Attribute verfügbar machen muss. Obwohl ich das Leben durch das Erstellen von Objektproxys für jedes Attribut erleichtert hätte, würde dies zu einem übermäßigen Arbeitsspeicher- und Leistungsaufwand führen (da der Speicherbedarf für den Proxy selbst viel mehr als wenige Bytes beträgt, die von den meisten Werttypen belegt werden).

Die Lösung, die ich gewählt habe, war, die Attributnamen als Sammlung verfügbar zu machen. Wenn der XPathNavigator in einem Attribut positioniert ist, verwalte ich einen Index, um zu wissen, auf welchem Attributnamen XPathNavigator basiert. Wenn Sie den Wert des XPathNavigators anfordern, verwende ich dann den indizierten Attributnamen, um seinen Wert aus dem Proxy nachzuschlagen (indem ich eine Wörterbuchsuche durchführt, um den Wert zu erhalten). An diesem Punkt sind Vorgänge wie MoveToFirst() oder MoveToNextAttribute() so einfach wie das Ändern des Attributindexes.

Behandeln von Sammlungen und Wörterbüchern

Bisher haben wir den einfachen Fall einer Eigenschaft im gebundenen Objekt betrachtet, die entweder einen Zeichenfolgen-/Werttyp zurückgegeben hat (der in unserer logischen XML-Darstellung als Attribut behandelt wird). Alles, was komplexer ist, wurde als untergeordnetes Element behandelt, das die meiste Zeit gut funktioniert, aber für Sammlungen und Wörterbücher fällt. Der Grund dafür ist, dass diese Typen die darin enthaltenen Objekte nicht speziell in einer einfachen 1:1-Beziehung verfügbar machen, wie dies bei den meisten anderen Eigenschaften der Fall ist. Um einen bestimmten Eintrag aus der Auflistung abzurufen, müssen Sie ihn indizieren. Wir brauchen also eine spezielle Handhabung für Sammlungen und Wörterbücher.

Der folgende Codeausschnitt zeigt den Code, den die Activate()-Methode aufruft, wenn der Proxy an ein Objekt gebunden ist, das ICollection implementiert (die ICollection-Schnittstelle ist die Standardbasisklasse, die von allen Auflistungstypen im .NET Framework implementiert wird).

private void ActivateCollection()
{
    // create a holding array
    ArrayList elements = new ArrayList();
    
    // go through each item in the array
    foreach( object val in (ICollection) m_binding )
    {
        // ignore null values
        if( val == null )
        {
            continue;
        }
        
        // build a proxy to the object, using its type as the
        // logical element name
        elements.Add( new ObjectXPathProxy( val, val.GetType().Name, this, 
        m_nt ) );
    }

    m_elements    = ( elements.Count != 0 ) ? elements : null;
} 

Eine interessante Sache an diesem Code ist, dass er die Typen der Objekte in der Auflistung verwendet, um sie im XML-Code zu benennen. Um zu verstehen, wie dies funktioniert, stellen wir uns vor, Sie verfügen über ein Warenkorbobjekt namens Basket, das eine Sammlungseigenschaft namens Items enthält, bei denen es sich um die Elemente im Warenkorb handelt. So sieht die XML-Ausgabe aus, wenn in der Items-Auflistung drei Objekte vorhanden wären (zwei vom Typ Book und eines vom Typ PowerDrill):

<Basket>
    <Items>
        <Book />
        <Book />
        <PowerDrill />
    </Items>
<Basket> 

Wörterbücher (Objekte, die IDictionary implementieren) stellen ein zusätzliches Problem dar. Jeder Eintrag in einem Wörterbuch verfügt über einen Schlüssel, aber der Schlüssel muss nicht mit einer Eigenschaft übereinstimmen, die dem Wert zugeordnet ist. Stellen Sie sich ein Wörterbuch vor, das us-Bundesstaatscodes den Staatsnamen zugeordnet hat. In diesem Fall müssen wir dem logischen XML-Dokument ein spezielles Attribut hinzufügen, um den Wert des Schlüssels darzustellen, damit wir diesen Wert verwenden können, um die XML-Darstellung abzufragen. Um das spezielle Attribut zu registrieren, macht der Objektproxy eine Methode namens AddSpecialName() verfügbar, die der Attributauflistung einen Eintrag hinzufügt, sodass das XPathNavigator das Attribut so sieht, als wäre es eine Eigenschaft des gebundenen Objekts.

Der folgende Code wird aufgerufen, wenn Sie ein Wörterbuchobjekt aktivieren:

private void ActivateDictionary()
{
    ArrayList        elements = new ArrayList();
    ObjectXPathProxy item;

    foreach( DictionaryEntry entry in (IDictionary) m_binding )
    {
        if( entry.Value == null )
        {
            continue;
        }

        // create a proxy onto the entry's value
        item = new ObjectXPathProxy( entry.Value, 
        entry.Value.GetType().Name, this, m_nt );
        
        elements.Add( item );
        
        // register the key as a special attribute
        item.AddSpecialName( "key", entry.Key.ToString() );
    }

    m_elements    = ( elements.Count != 0 ) ? elements : null;
} 

Um zu sehen, wie dies in der realen Welt aussieht, sehen wir uns ein Beispiel an. Erstellen wir zunächst ein einfaches Wörterbuch:

IDictionary states = new Hashtable();

states["TX"] = "Texas";
states["WA"] = "Washington";
states["CA"] = "California"; 

Durch die Übergabe an ObjectXPathNavigator würde das folgende logische XML-Dokument aus diesem Wörterbuch erstellt. Jetzt können wir das spezielle oxp:key Attribut verwenden, um den Wörterbucheintrag mithilfe des zugehörigen Schlüssels abzufragen.

<states xmlns:oxp="urn:ObjectXPathNavigator">
    <string oxp:key="TX">Texas</string>
    <string oxp:key="WA">Washington</string>
    <string oxp:key="CA">California</string>
</states>

Geschachtelte IXPathNavigable nicht unterstützt

Ein spezifischer Fall, der in der aktuellen Version von ObjectXPathNavigator nicht unterstützt wird, ist der von geschachtelten Implementierungen von IXPathNavigable. Wenn ein Objekt in Ihrem Objektdiagramm IXPathNavigable implementiert, können Sie davon ausgehen, dass es XML auf eigene benutzerdefinierte Weise bereitstellen sollte.

Wie bereits beschrieben, stellen Objekte, die IXPathNavigable implementieren, eine CreateNavigator() -Implementierung bereit, die einen XPathNavigator zurückgeben kann. Das Problem beim Versuch, dies in ObjectXPathNavigator zu unterstützen, besteht darin, dass der Navigator, den das geschachtelte Objekt zurückgibt, nicht weiß oder erwartet, Teil einer größeren Struktur zu sein. Wenn Sie MoveToRoot() aufrufen würden, würde es in den Stamm von sich selbst verschoben, nicht zum Des ObjectXPathNavigator , der ihn enthält.

Natürlich könnten Sie dies umgehen, indem Sie einen weiteren Proxy erstellen, der den inneren Navigator umschloss und alle Aufrufe erfasst (und bei Bedarf umgeleitet) hat. Dies überschreitet den Rahmen dieses Artikels, sodass ich diese Implementierung als Übung Ihnen, dem sanften Leser, überlasse.

Zusammenfassung

Die Verwendung von ObjectXPathNavigator über ein Objektdiagramm bietet die Leistungsfähigkeit und Flexibilität des XML-Programmiermodells, ohne dass ein gesamtes Objektdiagramm in ein XmlDocument serialisiert werden muss. Dank des kompositorischen Designs der System.Xml Frameworks kann dieser ObjectXPathNavigator auch in anderen XML-Verarbeitungsszenarien verwendet werden. Beispielsweise in einer XslTransform, um eine deklarative XSLT-Transformation eines beliebigen Objektgraphen zu erreichen.