Bob Swart (aka Dr.Bob)
Delphi 6 XML Mapper

Een van de gebieden waarop Delphi 6 is uitgebreid met nieuwe features en extra ondersteuning is XML en het werken met XML documenten of XML datasets. Er zit zelfs een geheel nieuwe tool bij Delphi 6 (ook als losse tool bruikbaar) genaamd de XML Mapper, waarvan ik deze keer wil laten zien wat je er mee kan.

XML Documenten
De XML Mapper is als externe tool beschikbaar (maar ook onder het Delphi 6 Tools menu), en kan gebruikt worden voor het omzetten van een XML document naar een (DataSnap compatible) dataset, een XML document naar een (ander) XML document, of een dataset terug naar een XML document. De XML Mapper weet al hoe een dataset eruit moet zien, zodat we alleen nog moeten aangeven hoe ons XML document eruit ziet. Hiervoor kunnen we twee dingen gebruiken: het XML document zelf, of een XML schema bestand. Een XML schema is waar we vroeger een DTD bestand voor gebruikte, en bevat de beschrijving van het XML bestand. Omdat niet iedereen een XML Schema zal hebben, zal ik als voorbeeld beginnen met een normaal XML document, en wel eentje die gebruikt kan worden om conferentie sessies in te vullen (bijvoorbeeld als resultaat van een "Call for Papers"). Het SESSIONS.xml document dat ik gebruik beschrijft een drietal sessies die ik voor de Europese Borland Conferentie heb gehouden, die zelf weer op twee locaties is geweest. Een op twee plaatsen genest XML document dus (waarbij ik de abstract in de listing even leeg heb gemaakt om niet teveel ruimte in beslag te nemen):

  <?xml version="1.0" standalone="yes"?>
  <Conference>
  <Name>BorCon Europe 2001</Name>
  <Location>
    <Date>2001/09/17-2001/09/18</Date>
    <Place>London, UK</Place>
  </Location>
  <Location>
    <Date>2001/09/20-2001/09/21</Date>
    <Place>Noordwijkerhout, NL</Place>
  </Location>

  <Sessions>
   <Session ID="1001">
     <Title>Building WAP Apps with Delphi</Title>
     <Type>regular session</Type>
     <Tracks>Delphi/Kylix, Internet</Tracks>
     <Level>Intermediate</Level>
     <Description>Building WAP applications in Delphi using WebBroker</Description>
     <Abstract>...</Abstract>
     <Prerequisite>None</Prerequisite>
   </Session>
   <Session ID="1002">
     <Title>Crossplatform Application Development with Delphi and Kylix</Title>
     <Type>regular session</Type>
     <Tracks>Delphi</Tracks>
     <Level>Beginning, Intermediate</Level>
     <Description>Crossplatform application development using Kylix Delphi 5 and 6</Description>
     <Abstract>...</Abstract>
     <Prerequisite>None</Prerequisite>
   </Session>
   <Session ID="1003">
     <Title>VisiBroker for Delphi (CORBA)</Title>
     <Type>regular session</Type>
     <Tracks>Delphi</Tracks>
     <Level>Intermediate</Level>
     <Description>CORBA Programming Techniques using VisiBroker for Delphi</Description>
     <Abstract>...</Abstract>
     <Prerequisite>None</Prerequisite>
   </Session>
  </Sessions>
  </Conference>
Het volledige XML document kan bijvoorbeeld gebruikt worden om in één keer mijn sessie abstracts op te sturen als reactie op de call-for-papers, terwijl op dit moment nog vaak via een ouderwets HTML web interface een formulier moet worden ingevuld, of een word template moet worden bewerkt. Wellicht kunnen we over een jaar of wat een XML schema krijgen als input, en dan een ingevuld XML document opleveren als output. De bewerking daar weer van kan dan ook wat makkelijker gebeuren, zoals ik in dit artikel zal proberen te laten zien.

XML Document Programming
Om op de meest direkte (maar ook minst fijne) manier gebruik te kunnen maken van de structuur en inhoud van een XML document, kun je de TXMLDocument component van de Internet tab proberen. Dit component implementeert het DOM interface (default door de MS XML te gebruiken in de DOMVendor property), maar bevat daarnaast ook een eigen IXMLDocument interface, wat door Borland is geschreven en iets uitgebreider is dan het IDOMDocument interface. Zet een TXMLDocument component op een Form (of een andere plek), en laat de FileName proeprty wijzen naar het externe XML document, zoals sessions.xml in ons geval. Je kan ook de XML "met de hand" schrijven in de XML property, maar let er daarbij op dat de FileName en XML property elkaar uitsluiten: als je bij de een iets invult, zal de waarde van de andere verdwijnen! Door de Active property op True te zetten wordt het XML Document geopend, en zal de structuur in het geheugen (van de component) beschikbaar zijn om te gebruiken in onze Delphi toepassing. Er zijn nog een paar andere properties die handig kunnen zijn, zoals de doAutoSave vlag van de Options property, die ervoor zorgt dat het XML document automatisch wordt opgeslagen (in de FileName) als het gewijzigd is in de Delphi toepassing (dit opslaan gebeurt dan automatisch op het moment dat het XML Document weer niet Active wordt). Het gebruiken van de structuur gaat helaas op een minder handige manier: noch de IDOMDocument noch de IXMLDocument interface hebben enig verstand van de daadwerkelijke inhoud van het XML document, en bieden ons slechts een interface van Nodes en verzameling van Nodes aan. In het geval van de IXMLDocument (de editie van Borland), kun je bijvoorbeeld de inhoud uit het XML document ophalen en in een TMemo component zetten met de volgende code:

  procedure TForm1.Button1Click(Sender: TObject);
  var
    Location, Sessions, Session: IXMLNode;
  begin
    Memo1.Lines.Clear;
    try
      Memo1.Lines.Add(XMLDocument1.DocumentElement.ChildNodes['Name'].Text);

      Location := XMLDocument1.DocumentElement.ChildNodes['Location'];
      Memo1.Lines.Add(Location.ChildNodes['Date'].Text);
      Memo1.Lines.Add(Location.ChildNodes['Place'].Text);

      Sessions := XMLDocument1.DocumentElement.ChildNodes['Sessions'];
      Session := Sessions.ChildNodes[2]; // 0..2
      Memo1.Lines.Add('Title: ' + Session.ChildNodes['Title'].Text);
      Memo1.Lines.Add('Type: ' + Session.ChildNodes['Type'].Text);
      Memo1.Lines.Add('Track: ' + Session.ChildNodes['Track'].Text);
      Memo1.Lines.Add('Level: ' + Session.ChildNodes['Level'].Text);
      Memo1.Lines.Add('Description: ' + Session.ChildNodes['Description'].Text);
      Memo1.Lines.Add('Abstract: ' + Session.ChildNodes['Abstract'].Text);
      Memo1.Lines.Add('Prerequisite: ' + Session.ChildNodes['Prerequisite'].Text);
    except
      on E: Exception do
        Memo1.Lines.Add(E.Message)
    end
  end;
Het grote nadeel van deze benadering is dat we geen ondersteuning kunnen krijgen van Code Insight, en het is dus eenvoudig (helaas) om tikfouten te maken in de strings die we als argumenten meegeven aan de ChildNodes. Zelfs als we hier constanten voor gebruiken is het nog geen fijne manier van werken. Zeker niet als je bedenkt dat het schrijven van een waarde naar een foutieve naam (van een Child Node) als gevolg heeft dat er vrolijk een nieuwe knoop in de XML boom (en daarmee het document) wordt gehangen. Geen foutmelding, niks.

Data Binding Wizard
Delphi zou Delphi niet zijn als we niet een wat fijnere ondersteuning mogelijk was voor XML Documenten. De meest direkte vorm van ondersteuning kunnen we vinden door te dubbelklikken op de XMLComponent zelf, waardoor de XML Data Binding Wizard wordt gestart.

Deze zal automatisch zien met welk XML document de XMLDocument component gekoppeld is, en dit document analyseren en het resultaat presenteren. We kunnen met behulp van deze Wizard meteen Delphi code genereren die "op het lijf geschreven is" voor het betreffende XML document.

Het resultaat van de Delphi XML Data Binding Wizard is een unit die zowel de interface definities als de class implementaties bevat die het XML document kunnen benaderen op een gebruikersvriendelijke manier: we kunnen nu van het XML document meteen de Name property vragen, of de Location (met subproperties Date en Place) en de Sessions - een array, waarbij ieder Session element weer de bijbehorende properties heeft. De interface sectie van de gegenereerde unit Sessions.pas ziet er overigens als volgt uit (ik heb de implemenatie weggelaten om niet te veel ruimte te verspillen):

  {*************************************************}
  {                                                 }
  {             Delphi XML Data Binding             }
  {                                                 }
  {         Generated on: 2002-03-25 11:34:08       }
  {       Generated from: D:\src\XML\sessions.xml   }
  {   Settings stored in: D:\src\XML\sessions.xdb   }
  {                                                 }
  {*************************************************}
  unit Sessions;
  interface
  uses
    xmldom, XMLDoc, XMLIntf;

  type

  { Forward Decls }

    IXMLConferenceType = interface;
    IXMLLocationType = interface;
    IXMLSessionsType = interface;
    IXMLSessionType = interface;

  { IXMLConferenceType }

    IXMLConferenceType = interface(IXMLNode)
      ['{77971121-CF4B-459B-AB77-A85E4D3B5694}']
      { Property Accessors }
      function Get_Name: WideString;
      function Get_Location: IXMLLocationType;
      function Get_Sessions: IXMLSessionsType;
      procedure Set_Name(Value: WideString);
      { Methods & Properties }
      property Name: WideString read Get_Name write Set_Name;
      property Location: IXMLLocationType read Get_Location;
      property Sessions: IXMLSessionsType read Get_Sessions;
    end;

  { IXMLLocationType }

    IXMLLocationType = interface(IXMLNode)
      ['{2350EBF4-9C20-4D02-931D-DF6451ED70E8}']
      { Property Accessors }
      function Get_Date: WideString;
      function Get_Place: WideString;
      procedure Set_Date(Value: WideString);
      procedure Set_Place(Value: WideString);
      { Methods & Properties }
      property Date: WideString read Get_Date write Set_Date;
      property Place: WideString read Get_Place write Set_Place;
    end;

  { IXMLSessionsType }

    IXMLSessionsType = interface(IXMLNodeCollection)
      ['{5A452C31-4308-4945-85D3-0231AE0862D1}']
      { Property Accessors }
      function Get_Session(Index: Integer): IXMLSessionType;
      { Methods & Properties }
      function Add: IXMLSessionType;
      function Insert(const Index: Integer): IXMLSessionType;
      property Session[Index: Integer]: IXMLSessionType read Get_Session; default;
    end;

  { IXMLSessionType }

    IXMLSessionType = interface(IXMLNode)
      ['{61B90DB4-9076-42A8-8E72-EAEAD91C28A2}']
      { Property Accessors }
      function Get_ID: WideString;
      function Get_Title: WideString;
      function Get_Type_: WideString;
      function Get_Track: WideString;
      function Get_Level: WideString;
      function Get_Description: WideString;
      function Get_Abstract: WideString;
      function Get_Prerequisite: WideString;
      procedure Set_ID(Value: WideString);
      procedure Set_Title(Value: WideString);
      procedure Set_Type_(Value: WideString);
      procedure Set_Track(Value: WideString);
      procedure Set_Level(Value: WideString);
      procedure Set_Description(Value: WideString);
      procedure Set_Abstract(Value: WideString);
      procedure Set_Prerequisite(Value: WideString);
      { Methods & Properties }
      property ID: WideString read Get_ID write Set_ID;
      property Title: WideString read Get_Title write Set_Title;
      property Type_: WideString read Get_Type_ write Set_Type_;
      property Track: WideString read Get_Track write Set_Track;
      property Level: WideString read Get_Level write Set_Level;
      property Description: WideString read Get_Description write Set_Description;
      property Abstract: WideString read Get_Abstract write Set_Abstract;
      property Prerequisite: WideString read Get_Prerequisite write Set_Prerequisite;
    end;

  { Forward Decls }

    TXMLConferenceType = class;
    TXMLLocationType = class;
    TXMLSessionsType = class;
    TXMLSessionType = class;

  { TXMLConferenceType }

    TXMLConferenceType = class(TXMLNode, IXMLConferenceType)
    protected
      { IXMLConferenceType }
      function Get_Name: WideString;
      function Get_Location: IXMLLocationType;
      function Get_Sessions: IXMLSessionsType;
      procedure Set_Name(Value: WideString);
    public
      procedure AfterConstruction; override;
    end;

  { TXMLLocationType }

    TXMLLocationType = class(TXMLNode, IXMLLocationType)
    protected
      { IXMLLocationType }
      function Get_Date: WideString;
      function Get_Place: WideString;
      procedure Set_Date(Value: WideString);
      procedure Set_Place(Value: WideString);
    end;

  { TXMLSessionsType }

    TXMLSessionsType = class(TXMLNodeCollection, IXMLSessionsType)
    protected
      { IXMLSessionsType }
      function Get_Session(Index: Integer): IXMLSessionType;
      function Add: IXMLSessionType;
      function Insert(const Index: Integer): IXMLSessionType;
    public
      procedure AfterConstruction; override;
    end;

  { TXMLSessionType }

    TXMLSessionType = class(TXMLNode, IXMLSessionType)
    protected
      { IXMLSessionType }
      function Get_ID: WideString;
      function Get_Title: WideString;
      function Get_Type_: WideString;
      function Get_Track: WideString;
      function Get_Level: WideString;
      function Get_Description: WideString;
      function Get_Abstract: WideString;
      function Get_Prerequisite: WideString;
      procedure Set_ID(Value: WideString);
      procedure Set_Title(Value: WideString);
      procedure Set_Type_(Value: WideString);
      procedure Set_Track(Value: WideString);
      procedure Set_Level(Value: WideString);
      procedure Set_Description(Value: WideString);
      procedure Set_Abstract(Value: WideString);
      procedure Set_Prerequisite(Value: WideString);
    end;

  { Global Functions }

  function GetConference(Doc: IXMLDocument): IXMLConferenceType;
  function LoadConference(const FileName: WideString): IXMLConferenceType;
  function NewConference: IXMLConferenceType;

  implementation
Het gebruik van deze gegenereerde unit is veel eenvoudiger - en prettiger. De drie globale functies kunnen gebruikt worden om een XMLDocument component (dat wijst naar sessions.xml) te gebruiken om via GetConference een interface van type IXMLConferenceType terug te geven. We kunnen ook een nieuw - leeg - XML document starten, met behulp van NewConference, of zonder expliciete TXMLDocument component het sessions.xml document laden via LoadConference. De code die we eerder schreven om de inhoud van het XML document in een TMemo component te zetten kunnen we bijvoorbeeld vervangen door de volgende code, die volledig met behulp van Code Insight ingevuld kan worden:
  procedure TForm1.Button2Click(Sender: TObject);
  var
    Conference: IXMLConferenceType;
    Location: IXMLLocationType;
    Sessions: IXMLSessionsType;
    Session: IXMLSessionType;
  begin
    Memo1.Lines.Clear;
    try
      Conference := GetConference(XMLDocument1);
      Memo1.Lines.Add(Conference.Name);

      Location := Conference.Location;
      Memo1.Lines.Add(Location.Date);
      Memo1.Lines.Add(Location.Place);

      Sessions := Conference.Sessions;
      Session := Sessions[2]; // 0..2
      Memo1.Lines.Add('Title: ' + Session.Title);
      Memo1.Lines.Add('Type: ' + Session.Type_);
      Memo1.Lines.Add('Track: ' + Session.Track);
      Memo1.Lines.Add('Level: ' + Session.Level);
      Memo1.Lines.Add('Description: ' + Session.Description);
      Memo1.Lines.Add('Abstract: ' + Session.Abstract);
      Memo1.Lines.Add('Prerequisite: ' + Session.Prerequisite);
    except
      on E: Exception do
        Memo1.Lines.Add(E.Message)
    end
  end;
Merk op dat het veld "Type" naar de property genaamd "Type_" is vertaald, terwijl het veld "Abstract" gewoon Abstract kan blijven. Al met al vind ik deze manier van werken - met de gegenereerde code van de XML Data Binding Wizard - veel fijner dan de ouderwetse manier, waarbij we erg moeten oppassen geen tikfouten te maken. Het kan echter nog een stapje verder, door het XML document daadwerkelijk op een dataset af te beelden (te "mappen") met de XML Mapper, oftewel de XML Mapping Tool die ook onderdeel is van Delphi 6 Enterprise (maar ook beschikbaar is in Kylix 2 Enterprise en C++Builder 6 Enterprise).

XML Mapping Tool
Start nu de XML Mapper, en doe File | Open om het voorbeeld XML Document te laten analyseren. Het scherm van de XML Mapper bestaat uit drie delen. Links staat het XML Document, rechts het Datapacket (komen we zo op), en in het midden de Transformation informatie.

Om met links te beginnen: het XML document kan zowel in de oorspronkelijke Document View bekeken worden als in de Schema View. Dat laatste is leuk om naar te kijken, ook als je geen XML Schema had, want de XML Mapper is in staat om zelf een soort XML Schema te genereren voor je XML Document. Het ziet er niet geweldig uit, maar je kan het wel gebruiken om zelf mooier te maken (tip: met de rechtermuisknop in de Schema View kun je het XML Schema opslaan, en daarna gewoon als XSD document verder bewerken).
  <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="Conference" type="ConferenceType"/>
    <xs:complexType name="ConferenceType">
      <xs:sequence>
        <xs:element name="Name" type="NameType"/>
        <xs:element name="Location" type="LocationType" minOccurs="0" maxOccurs="unbounded"/>
        <xs:element name="Sessions" type="SessionsType"/>
      </xs:sequence>
    </xs:complexType>
    <xs:element name="Name" type="NameType"/>
    <xs:simpleType name="NameType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Location" type="LocationType"/>
    <xs:complexType name="LocationType">
      <xs:sequence>
        <xs:element name="Date" type="DateType"/>
        <xs:element name="Place" type="PlaceType"/>
      </xs:sequence>
    </xs:complexType>
    <xs:element name="Date" type="DateType"/>
    <xs:simpleType name="DateType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Place" type="PlaceType"/>
    <xs:simpleType name="PlaceType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Sessions" type="SessionsType"/>
    <xs:complexType name="SessionsType">
      <xs:sequence>
        <xs:element name="Session" type="SessionType" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
    </xs:complexType>
    <xs:element name="Session" type="SessionType"/>
    <xs:complexType name="SessionType">
      <xs:sequence>
        <xs:element name="Title" type="TitleType"/>
        <xs:element name="Type" type="TypeType"/>
        <xs:element name="Tracks" type="TracksType"/>
        <xs:element name="Level" type="LevelType"/>
        <xs:element name="Description" type="DescriptionType"/>
        <xs:element name="Abstract" type="AbstractType"/>
        <xs:element name="Prerequisite" type="PrerequisiteType"/>
      </xs:sequence>
      <xs:attribute name="ID" type="xs:string"/>
    </xs:complexType>
    <xs:element name="Title" type="TitleType"/>
    <xs:simpleType name="TitleType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Type" type="TypeType"/>
    <xs:simpleType name="TypeType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Tracks" type="TracksType"/>
    <xs:simpleType name="TracksType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Level" type="LevelType"/>
    <xs:simpleType name="LevelType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Description" type="DescriptionType"/>
    <xs:simpleType name="DescriptionType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Abstract" type="AbstractType"/>
    <xs:simpleType name="AbstractType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
    <xs:element name="Prerequisite" type="PrerequisiteType"/>
    <xs:simpleType name="PrerequisiteType">
      <xs:restriction base="xs:string"/>
    </xs:simpleType>
  </xs:schema>
Het middelste deel van de XML Mapper kan overigens ook gebruikt worden om de properties van de verschillende knopen in de XML boom (links in de Document View) aan te passen. Bij het inlezen van het document wordt namelijk voor iedere knoop het type bepaald en de maximale lengte. Dit laatste gebeurt gewoon door de lengte van de waarden te nemen, en er vanuit te gaan dat dat meteen de maximale lengte is. Bij Title is de maximale lengte dus kennelijk 59, omdat we een Session met de Title "Crossplatform Application Development with Delphi and Kylix" hebben. Door op de Node Properties tab te klikken (in het midden van de XML Mapper) kun je per Node de properties aanpassen, zoals de Max Length voor Title (van 59 naar 64 ofzo).

Overigens krijg je de data van het XML document te zien als je op de "Data View" checkbox klikt in het linker deel van het scherm. Dat levert echter niet een duidelijker overzicht op, vandaar dat ik zelf deze optie vrijwel nooit gebruik - hooguit een keer om te kijken hoe de data achter de knopen hangt.

Transformeren
Als je klaar bent met het aanpassen van de node properties, kun je in het linker deel van de XML Mapper aangeven welke onderdelen van de boom je eigenlijk allemaal wilt transformeren. Het zou bijvoorbeeld kunnen zijn dat je alleen interesse hebt in het geneste Session deel. Klik dan met de rechtermuisknop op de Session knoop, en kies "Select All Children" (alleen Select heeft weinig zin, want de Session knoop zelf heeft geen inhoud). Hierna zul je de geselecteerde knopen in het midden van het scherm terugzien, onder de Mapping tab. In mijn geval ben ik echter geïnteresseerd in de volledige inhoud van het XML document, inclusief de twee geneste delen Location en Session, dus ik klik met de rechter-muisknop en kies "Select All" (Ctrl+A). Aals je dat doet nadat je "Select all Children" hebt gedaan dan is de volgorde van de knopen in de mapping verkeerd - klik dan met de rechter-muisknop eerst in het midden van het scherm en kies "Clear" (Ctrl+C) om het middenstuk weer leeg te maken. Klik vervolgens weer links, en kies "Select all".
Om ook de rechterkant van de XML Mapper te vullen moet je nog eenmaal met de rechtermuisknop klikken en kiezen voor "Create Datapacket from XML" (Ctrl+D). In het middelste deel van de XML Mapper zie je nu zowel de geselecteerde knopen als de getransformeerde velden die erbij horen. Daarnaast is in het rechterdeel nu de gegenereerde dataset te zien. Merk op dat Location en Session allebei als geneste dataset zijn opgenomen. Die zullen we straks nog in detail terugzien.

We zijn er bijna, maar nog niet helemaal.

Create and Test
Om nu de transformatie daadwerkelijk te starten moeten we nog even expliciet op de grote button met "Create and Test Transformation" drukken. We krijgen dan een form te zien met daarin de "Test transformation XML Document -> Datapacket" in de vorm van een DBGrid dat aan een ClientDataSet verbonden is. Naast het veld Naam bevat dit twee geneste datasets: Location en Session. Beide geneste datasets kunnen vertoond worden door de velden in het grid te selecteren en daarbij op de elipsis (...) te klikken. Dit levert per geneste dataset een pop-up scherm op met daarin weer een DBGrid en de records van de betreffende geneste dataset.

Bewaren
Via File | Save kunnen we nu zowel een ClientDataSet definitie bestand bewaren in XML formaat (dus de vertaling van het XML Schema naar een XML definitie), als de transformatie informatie. Om met het eerste te beginnen: de ClientDataSet XML bestaat uit één lange regel van 915 bytes, maar kan iets leesbaarder gemaakt worden tot:
  <?xml version="1.0" standalone="yes"?>
  <DATAPACKET Version="2.0">
  <METADATa>
   <FIELDS>
    <FIELD attrname="Name" fieldtype="string" WIDTH="18"/>
    <FIELD attrname="Location" fieldtype="nested">
     <FIELDS>
      <FIELD attrname="Date" fieldtype="string" WIDTH="21"/>
      <FIELD attrname="Place" fieldtype="string" WIDTH="19"/>
     </FIELDS>
     <PARAMS/>
    </FIELD>
    <FIELD attrname="Session" fieldtype="nested">
     <FIELDS>
      <FIELD attrname="ID" fieldtype="string" WIDTH="4"/>
      <FIELD attrname="Title" fieldtype="string" WIDTH="59"/>
      <FIELD attrname="Type" fieldtype="string" WIDTH="15"/>
      <FIELD attrname="Tracks" fieldtype="string" WIDTH="22"/>
      <FIELD attrname="Level" fieldtype="string" WIDTH="23"/>
      <FIELD attrname="Description" fieldtype="string" WIDTH="173"/>
      <FIELD attrname="Abstract" fieldtype="bin.hex" SUBTYPE="Text"/>
      <FIELD attrname="Prerequisite" fieldtype="string" WIDTH="4"/>
     </FIELDS>
     <PARAMS/>
    </FIELD>
   </FIELDS>
   <PARAMS/>
  </METADATa>
  <ROWDATA/>
  </DATAPACKET>
Zoals kenner meteen zullen zien bevat dit XML dataset bestand alleen maar de metadata, en geen inhoud voor de rowdata. Oftewel: dit XML dataset bestand bevat de definitie, maar is verder een lege dataset. Dat kan toch handig zijn, want hiermee kun je data-entry schermen bouwen.
Wat echter vaak handiger is, is om de transformatie informatie op te slaan, zodat XML documenten met inhoud omgezet kunnen worden naar een XML dataset met de hierboven beschreven definitie. Hiervoor moeten we de transformatie informatie opslaan in een XTR bestand, waarvan de XML Mapper als naam ToDp.xtr voorstelt (To DataPacket). Het aardig is dat ook de transformatie informatie nu is opgeslagen in XML formaat. Hierdoor kun je later de transformatie nog met de hand aanpassen, alhoewel het altijd makkelijker zal blijven om daarvoor de XML Mapper zelf weer te gebruiken. In ieder geval is het ToDp.xtr bestand de informatie die we nodig hebben om het oorspronkelijke XML document om te zetten naar een datapacket dat we in een ClientDataSet kunnen gebruiken. Als we ook de weg terug nodig hebben (om het ClientDataSet datapacket weer terug om te zetten naar een XML document), dan zullen we ook de transformatie in de andere richting moeten maken. Dat gaat makkelijk, want het middenste deel van de XML Mapper bevat de "Transform Direction" opties, die default op "XML to Datapacket" staat, maar we ook op "Datapacket to XML" kunnen zetten. Ook daarna moeten we weer expliciet op de knop "Create and Test Transformation" drukken om een nieuwe transformatie te kunnen maken en testen. Het resultaat is een XML document (hetzelfde als in het linker deel van het scherm, maar dan met de data ingevuld). Hierna kunnen we weer verschillende zaken bewaren, zoals de transformatie informatie die deze keer in ToXml.xtr bewaard wil worden. Beide XTR bestanden hebben we nodig om de conversie van een XML document naar een datapacket en terug te kunnen realiseren. Sluit dan nu de XML Mapper af, en start Delphi 6 om de transformatie informatie bestanden toe te passen.

XMLTransformProvider
Het makkelijkst is om te beginnen met een nieuwe data module, aangezien we het XML document in feite omzetten tot een dataset. Allereerst hebben we de XMLTransformProvider nodig van de Data Access tab van het Component Palette. De TransformRead property heeft een subproperty TransformationFile, en deze moet naar de ToDp.xtr wijzen (om van het XML document een data packet te maken). En als we toch bezig zijn: de TransformWrite heeft ook een subproperty TransformationFile, en die moet uiteraard naar ToXml.xtr wijzen. Blijft er nog één property over die een waarde moet krijgen, en dat is de XMLDataFile property die naar het SESSIONS.xml input bestand zelf moet wijzen.

Met deze drie properties op hun plaats, is de XMLTransformProvider in staat om het XML input document te "voeren" aan een ClientDataSet component, en de eventuele updates daarvan weer terug te schrijven naar het XML document (dat in beide gevallen gevonden wordt via de XMLDataFile property).
We hebben maar liefst drie ClientDataSets nodig: eentje voor het resultaat van de XMLTransformProvider, en twee om naar de Location en Session nested datasets te kunnen verwijzen. De eerste noem ik ClientDataSetXML (omdat die het XML document representeert), en de andere twee resp. ClientDataSetLocation en ClientDataSetSession.

De ProviderName property van ClientDataSetXML moet wijzen naar XMLTransformProvider1. Hierna moet je met de rechter-muisknop klikken op ClientDataSetXML en de Fields Editor opstarten. We moeten expliciet alle velden toevoegen (met Ctrl+F) en daarmee persistent maken zodat de andere twee ClientDataSets ze kunnen vinden om mee te koppelen. We kunnen daarna voor de ClientDataSetLocation de DataSetField property de waarde ClientDataSetXMLLocation geven, en voor ClientDataSetSession de DataSetField de waarde ClientDataSetXMLSession geven. Van deze laatste twee ClientDataSets is het niet echt noodzakelijk om persistent fields te maken (ik doe het meestal wel, want het is wel makkelijk op direct met de velden zelf te werken).
Als we nu de ClientDataSetXML openen (of de Active property op True zetten) dan zal deze aan zijn provider om data vragen. De XMLTransformProvider zal deze data kunnen leveren door het XML document (verwezen in de XMLDataFile property) te transformeren van een XML document naar een data packet, volgens de transformatie informatie die in de TransformRead.TransformationFile aangetroffen wordt.

XML Formulier
Nu we via de XMLTransformProvider op het data module een XML document in feite kunnen splitsen in drie ClientDataSet componenten wordt het tijd om die te gebruiken op een beetje zinvolle manier. Ga terug naar het main form van je project, en zorg dat de data module in de uses clause is opgenomen.
We hebben nu om te beginnen drie DataSources nodig op ons main form; een voor iedere ClientDataSet in de data module. Van de DataSource die naar ClientDataSetXML wijst gebruik ik alleen het "Name" veld (de andere twee, Location en Session zijn immers geneste datasets). Voor de DataSource die naar ClientDataSetLocation wijst gebruik ik een DBGrid om de velden Date en Place te laten zien. En tenslotte wil ik alle velden van de ClientDataSetSession laten zien in DBEdit controls, met uitzondering van het Abstract veld dat in een DBMemo vertoond moet worden. Een DBNavigator verbonden met de DataSource van de ClientDataSetSession zorgt er bovendien voor dat we makkelijk door de verschillende abstracts heen kunnen lopen.
Als laatste wil ik nog twee buttons toevoegen. Eentje om eventuele wijzigingen die we in het formulier hebben aangebracht terug te sturen naar het oorspronkelijke XML document, en eentje om eventuele foutieve wijzigingen ongedaan te maken en de oorspronkelijke data uit het XML document weer terug te krijgen.

De twee buttons zullen de enige plaats zijn waar we enige code moeten schrijven - en zelfs die is niet lang. Voor het doorsturen van de gewijzigde gegevens moeten we ApplyUpdates van de ClientDataSet aanroepen. En wel bij de ClientDataSetXML - de "moeder" van de twee geneste ClientDataSets. De ClientDataSetXML stuurt het delta data packet vervolgens door naar zijn provider, wat in dit geval de XMLTransforProvider is. Deze zal de transformatie van data packet terug naar XML document maken volgens de transformatie informatie die in de TransformationFile subproperty van TransformWrite wordt aangetroffen. De code voor de ApplyButton is dan ook als volgt:
  procedure TForm1.BtnApplyClick(Sender: TObject);
  begin
    (DataSourceXML.DataSet AS TClientDataSet).ApplyUpdates(-1)
  end;
Overigens moeten we de unit DBClient ook aan de uses clause van ons main form toevoegen (anders kent de compiler TClientDataSet niet).
Om te testen kun je bijvoorbeeld de waarvan de het Prerequisite veld van de eerste session abstract wijzigen van "None" in "n/a", en daarna op de Apply button drukken. Op het eerste gezicht is er niks veranderd, maar als je de toepassing afsluit en opnieuw opstart zie je dat je nu plotseling het tweede session abstract (met ID 1002) als eerste te zien krijgt. De abstract die we zojuist hebben gewijzigd is achteraan de lijst terecht gekomen! Mocht dit een probleem zijn (stel dat je de data in de ClientDataSet altijd op ID gesorteerd wilt hebben), dan kun je dit oplossen door ID op te geven als waarde voor de IndexFieldName property van de ClientDataSetSession component.
De laatste regel code moeten we schrijven voor de Cancel button, die alle wijzigingen gedaan in het XML formulier weer ongedaan maakt - en dus in feite de data uit het oorspronkelijke XML document weer ophaalt. Dat laatste is niet nodig, want de ClientDataSet zelf heeft een methods "CancelUpdates" genaamd die we hiervoor kunnen gebruiken. De code voor de CancelButton is dan ook als volgt:
  procedure TForm1.BtnCancelClick(Sender: TObject);
  begin
    (DataSourceXML.DataSet AS TClientDataSet).CancelUpdates
  end;
Tot slot nog een paar regels code om te controleren of er nog wijzigingen zijn gemaakt die nog niet opgeslagen zijn. Ook hier kunnen we de ClientDataSet zelf voor gebruiken, en controleren of ChangeCount een waarde groter dan 0 heeft. Zo ja, dan moeten we even vragen of de wijzingen bewaard moeten worden. In de OnClose van de main form schreef ik hiervoor de volgende code:
  procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
  begin
    if DataSourceXML.State in [dsEdit,dsInsert] then
      DataSourceXML.DataSet.Post;
    if (DataSourceXML.DataSet AS TClientDataSet).ChangeCount > 0 then
      if MessageDlg('Veranderingen bewaren?',
        mtConfirmation, [mbYes,mbNo], 0) = mrYes then
        (DataSourceXML.DataSet AS TClientDataSet).ApplyUpdates(-1)
  end;
Merk op dat ik eerst nog even de "State" van de DataSourceXML bekijk om te zien of de laatste wijzigingen (in het huidige record) al opgeslagen zijn.
Mocht er gekozen worden voor het opslaan van de wijzigingen (of direct op de Apply knop gedrukt worden), dan vinden we de nieuwe inhoud in het XML document. In feite doet het XML document dienst als database tabel. Maar het voordeel is natuurlijk dat we dit XML document (met of zonder het XML schema bestand) uit een andere bron hadden kunnen ontvangen, of naar een andere bestemming kunnen sturen. De XML Mapper stelt ons in ieder geval in staat om een XML document te gebruiken alsof het een dataset is, en dat werkt in Delphi nu eenmaal erg makkelijk.

Meer Informatie
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via . Wie meer wil weten over XML document programming en de XML Mapper moet zeker eens overwegen om zich in te schrijven voor een van mijn Delphi Clinics.


Dit artikel is eerder verschenen in SDGN Magazine #69 - december 2001

This webpage © 2001-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.