XML in .NET (met Delphi for .NET)
In dit artikel laat ik zien hoe we met XML kunnen werken in Delphi for .NET. We kunnen hierbij elke versie van Delphi for .NET gebruiken, want de XML ondersteuning zit gewoon in het .NET Framework zelf, zoals in de System.Xml.dll assembly die o.a. de namespaces System.Xml, System.Xml.Schema, System.Xml.XPath en System.Xml.Xsl bevat.
Er zijn verschillende manieren om met een XML document om te gaan: stapje voor stapje of in een keer alles
Er zijn verschillende manieren om met een XML-document om te gaan: stapje voor stapje of in een keer alles. De stapje-voor-stapje benadering werkt iedere node in volgorde af, terwijl de alternatieve benadering het hele document in één keer inleest. Ieder heeft zijn voordelen en nadelen, en ik zal van allebei een voorbeeld laten zien. Aan het eind van het artikel laat ik dan ook nog zien hoe we een bestaand XML document kunnen aanpassen met behulp van XSLT.
XmlReader en XmlWriter
Ik begin met de benadering waarbij we elke node achter elkaar tegenkomen. Om een XML-document op deze manier in te lezen kunnen we een XmlReader gebruiken, en om er eentje weg te schrijven kunnen we een vergelijkbare XmlWriter nemen.
XmlReader en XmlWriter kunnen we echter niet zomaar gebruiken, want het zijn allebei abstract classes. Het zijn de afgeleide classes XmlTextReader, XmlNodeReader en XmlValidatingReader die we wel kunnen instantiëren en gebruiken om een XML-document te lezen, en de XmlTextWriter class om een XML-document weg te schrijven.
Om vanuit een Delphi for .NET WinForms project met een van XmlReader of XmlWriter afgeleide class te werken, hoeven we alleen maar System.Xml aan de uses clause toe te voegen (de System.Xml.dll assembly staat al in de References lijst, dus die hoeven we niet zelf toe te voegen).
XmlWriter
Voor ik iets ga lezen, lijkt het me zinvol om eerst iets te gaan schrijven (dan kunnen we dat straks gaan gebruiken). Als kleine toepassing wil ik het zoeken naar bestanden van een bepaald type makkelijker maken. Het type zelf is niet belangrijk, maar het zou bijvoorbeeld handig zijn om een overzicht te hebben van alle JPG-plaatjes, MP3-liedjes of AVI-filmpjes op mijn machine te zien. Of alle Delphi-projecten, om het een beetje dichter bij de (lezers) doelgroep te houden.
Uitgaande van een startdirectory en een bestandsuitgang kan ik het zoeken naar dergelijke bestanden als volgt – recursief – programmeren, gebruikmakend van de DirectoryInfo en FileInfo classes uit de System.IO namespace van het .NET Framework:
procedure FindFiles(const DirName: String); overload;
var
CurrDir,NextDir: DirectoryInfo;
CurrFile: FileInfo;
begin
CurrDir := DirectoryInfo.Create(DirName);
for CurrFile in CurrDir.GetFiles('*.dpr') do
{ doe iets } ;
for NextDir in CurrDir.GetDirectories do
FindFiles(TextWriter,DirName + NextDir.Name + '\');
end;
Nu moeten we alleen nog de XmlWriter toevoegen, en die zowel de directory als de gevonden bestanden laten rapporteren. De constructor van de XmlWriter kunnen we de naam van ons te genereren XML-bestand meegeven, of de Console.Out om te zorgen dat het resultaat naar de console wordt geschreven (is handig bij de eerste tests – dan zie je sneller wanneer het fout gaat).
Voor iedere directory wil ik een nieuw element in het XML-document wegschrijven, en dat kan met de volgende aanroepen:
TextWriter.WriteStartElement('', 'directory', '');
TextWriter.WriteAttributeString('name', CurrDir.Name);
TextWriter.WriteEndElement;
De WriteStartElement heeft als argumenten eerst de prefix, dan de naam van het element zelf, en tot slot de namespace. Zoals je ziet laat ik de prefix en namespace leeg, maar de naam moet uiteraard ingevuld zijn. Met de WriteAttributeString voeg ik een attribute “name” toe met de naam van de directory. Daarna sluit ik het element met een aanroep naar WriteEndElement.
Alle mogelijke bestanden en subdirectories die in deze directory staan moeten worden toegevoegd nog voor het EndElement wordt geschreven, zodat we een diep genest XML-document krijgen als resultaat.
XML-elementen kunnen subelementen bevatten of attributen. Bij de directory heb ik de naam als attribuut toegevoegd, maar dit hadden we ook als subelement kunnen doen. Bij gevonden bestanden voeg ik de naam als attribuut toe, maar de CreationTime, LastAccessTime en LastWriteTime als subelementen, en wel als volgt (we zien aan het eind van dit artikel hoe we deze beslissing kunnen aanpassen – ook als we al een XML-document hebben gegenereerd):
TextWriter.WriteStartElement('', 'file', '');
// prefix, name, ns
TextWriter.WriteAttributeString('name', CurrFile.Name);
TextWriter.WriteElementString('size',
CurrFile.Length.ToString);
TextWriter.WriteElementString('creationtime',
CurrFile.CreationTime.ToString);
TextWriter.WriteElementString('lastaccesstime',
CurrFile.LastAccessTime.ToString);
TextWriter.WriteElementString('lastwritetime',
CurrFile.LastWriteTime.ToString);
TextWriter.WriteEndElement;
Overigens is de LastWriteTime de tijd die we meestal te zien krijgen, maar dat terzijde.
In totaal levert dit de volgende code op, waarbij het pattern waarop gezocht wordt in een TextBox wordt opgegeven door de gebruiker:
procedure TWinFormXML.btnXmlWriter_Click(
sender: System.Object;
e: System.EventArgs);
var
pattern: String;
procedure FindFiles(TextWriter: XmlTextWriter;
const DirName: String); overload;
var
CurrDir,NextDir: DirectoryInfo;
CurrFile: FileInfo;
begin
CurrDir := DirectoryInfo.Create(DirName);
TextWriter.WriteStartElement('', 'directory', '');
TextWriter.WriteAttributeString(
'name', CurrDir.Name);
for CurrFile in CurrDir.GetFiles(pattern) do
begin
TextWriter.WriteStartElement('', 'file', '');
// prefix, name, ns
TextWriter.WriteAttributeString('name',
CurrFile.Name);
TextWriter.WriteElementString('size',
CurrFile.Length.ToString);
TextWriter.WriteElementString('creationtime',
CurrFile.CreationTime.ToString);
TextWriter.WriteElementString('lastaccesstime',
CurrFile.LastAccessTime.ToString);
TextWriter.WriteElementString('lastwritetime',
CurrFile.LastWriteTime.ToString);
TextWriter.WriteEndElement;
end;
for NextDir in CurrDir.GetDirectories do
FindFiles(TextWriter,DirName+NextDir.Name+'\');
TextWriter.WriteEndElement;
end;
var
TextWriter: XmlTextWriter;
begin
ChDir('\');
Pattern := tbPattern.Text; // input gebruiker
TextWriter := XmlTextWriter.Create('disk.xml', nil);
//TextWriter := XmlTextWriter.Create(Console.Out);
try
TextWriter.Formatting := Formatting.Indented;
TextWriter.WriteStartDocument;
FindFiles(TextWriter, '\');
finally
TextWriter.WriteEndDocument;
TextWriter.Flush;
TextWriter.Close;
end
end;
Let op, dat we op het eind de Flush- en Close-methods van de XmlTextWriter moeten aanroepen om te voorkomen dat het XML-document niet helemaal volledig wordt weggeschreven.
Op mijn ontwikkelmachine levert dit na een dikke tiental seconden een XML-bestand op van ruim 1 MB. Dit bestand wil ik gebruiken als “invoer” voor de overige toepassingen door de XmlReader te gebruiken.
XmlReader
Als je eenmaal een XmlWriter hebt gezien, dan is een XmlReader nog makkelijker in gebruik. Bij de constructor geef je op welk XML-document je wilt lezen, en verder loop je er doorheen met de TextReader.Read method. Het enige waar je even rekening mee moeten houden in de praktijk is dat je nogal wat “Whitespace” kunt tegenkomen – dit zijn XmlNodes die ik het liefst negeer, omdat ze niks toevoegen aan de structuur of inhoud van het XML-document zelf.
Om van een bestaand XML-document het aantal echte elementen te tellen (dus de Whitespace negerend), gebruik ik altijd de volgende routine:
procedure TWinFormXML.btnXmlReader_Click(
sender: System.Object;
e: System.EventArgs);
var
TextReader: XmlTextReader;
NodeTypes: Array[XmlNodeType] of Integer;
i: XmlNodeType;
Nodes: Integer;
begin
Nodes := 0;
for i:=Low(XmlNodeType) to High(XmlNodeType) do
NodeTypes[i] := 0;
TextReader := XmlTextReader.Create('disk.xml');
try
while TextReader.Read do
begin
Inc(Nodes);
Inc(NodeTypes[TextReader.NodeType]);
end;
MessageBox.Show(Nodes.ToString+' nodes found');
for i:=Low(XmlNodeType) to High(XmlNodeType) do
begin
if NodeTypes[i] > 0 then
case i of
XmlNodeType.None:
MessageBox.Show(NodeTypes[i].ToString +
‘None');
XmlNodeType.Element:
MessageBox.Show(NodeTypes[i].ToString +
‘Element');
XmlNodeType.Attribute:
MessageBox.Show(NodeTypes[i].ToString +
‘Attribute');
XmlNodeType.Text:
MessageBox.Show(NodeTypes[i].ToString +
‘Text');
XmlNodeType.CDATA:
MessageBox.Show(NodeTypes[i].ToString +
‘CDATA');
XmlNodeType.EntityReference:
MessageBox.Show(NodeTypes[i].ToString +
‘EntityReference');
XmlNodeType.Entity:
MessageBox.Show(NodeTypes[i].ToString +
‘Entity');
XmlNodeType.ProcessingInstruction:
MessageBox.Show(NodeTypes[i].ToString +
‘ProcessingInstruction');
XmlNodeType.Comment:
MessageBox.Show(NodeTypes[i].ToString +
‘Comment');
XmlNodeType.Document:
MessageBox.Show(NodeTypes[i].ToString +
‘Document');
XmlNodeType.DocumentType:
MessageBox.Show(NodeTypes[i].ToString +
‘DocumentType');
XmlNodeType.DocumentFragment:
MessageBox.Show(NodeTypes[i].ToString +
‘DocumentFragment');
XmlNodeType.Notation:
MessageBox.Show(NodeTypes[i].ToString +
‘Notation');
XmlNodeType.Whitespace:
MessageBox.Show(NodeTypes[i].ToString +
‘Whitespace');
XmlNodeType.SignificantWhitespace:
MessageBox.Show(NodeTypes[i].ToString +
‘SignificantWhitespace');
XmlNodeType.EndElement:
MessageBox.Show(NodeTypes[i].ToString +
‘EndElement');
XmlNodeType.EndEntity:
MessageBox.Show(NodeTypes[i].ToString +
‘EndEntity');
XmlNodeType.XmlDeclaration:
MessageBox.Show(NodeTypes[i].ToString +
‘XmlDeclaration');
end;
end;
finally
TextReader.Close
end
end;
Waar de XmlTextWriter nog enige tijd nodig heeft (die met name besteed wordt aan het afzoeken van de schijf), daar is de XmlTextReader bloedsnel, en vertelt me dat er alleen nodes van type Element, Text, Whitespace, EndElement of XmlDeclaration zijn (de laatste maar één keer overigens, voor het gehele document).
Als je eenmaal een XmlWriter hebt gezien, dan is een XmlReader nog makkelijker in gebruik
XML DOM
Waar de XmlWriter en XmlReader node voor node het XML-document bewerken, kan er soms een situatie zijn waarin we het hele XML-document in één keer in het geheugen willen laden en willen bewerken (zonder dat “op volgorde” van de elementen te moeten doen). Hiervoor moeten we het bekende Document Object Model (DOM) interface gebruiken, dat ook voor.NET beschikbaar is.
De class type heet XmlDocument, en we kunnen hiermee een XML-document inladen, wat vervolgens wordt ontleed in elementen, attributen, etc. die we vervolgens kunnen benaderen en zelfs wijzigen als we willen. De Load-methode kan zowel de (bestands)naam van een XML-document krijgen, als een URL naar een extern bestand, of zelfs een WSDL (Web Service Description Language) definitie van een web service. Als een XML-document eenmaal geladen is, kunnen we met de XmlNode- en XmlNodeList-classes werken.
Als voorbeeld kunnen we het gegenereerde DISK.XML bestand gaan bekijken, op zoek naar alle directories die gevonden zijn, maar die geen bestanden bevatten waar ik naar op zoek was. Die “lege” directories (niet echt leeg, maar in ieder geval niet gevuld met de bestanden die ik zoek) kunnen in eerste instantie geteld worden, en wel als volgt:
procedure TWinFormXML.btnXmlDOM _Click(
sender: System.Object;
e: System.EventArgs);
var
XMLDom: XmlDocument;
XMLNodes: XmlNodeList;
empty,i: Integer;
begin
XMLDom := XmlDocument.Create;
try
XMLDom.Load('disk.xml');
XMLNodes :=
XMLDom.GetElementsbyTagName('directory');
empty := 0;
for i:=0 to XMLNodes.Count-1 do
if XMLNodes[i].ChildNodes.Count = 0 then
// empty directory
Inc(empty);
MessageBox.Show(empty.ToString +
' empty directories on disk');
finally
XMLDom.Free
end
end;
De methode GetElementsbyTagName zoekt naar elementen die als naam ‘directory’ hebben, en kijkt dan of er bestanden of subdirectories inzitten (waarbij we ons nu nog even niet drukmaken of die subdirectories dan wel bestanden bevatten). We kunnen ook een lijst met “lege” directories produceren door behalve de GetElementsByTagName ook naar de Attributes-property te kijken en daarvan de Value (of de innerText) op te zoeken.
procedure TWinFormXML.btnXmlDOM _Click(
sender: System.Object;
e: System.EventArgs);
var
XMLDom: XmlDocument;
XMLNodes: XmlNodeList;
XMLElem: XmlElement;
empty,i: Integer;
begin
XMLDom := XmlDocument.Create;
try
XMLDom.Load('disk.xml');
XMLNodes :=
XMLDom.GetElementsbyTagName('directory');
empty := 0;
for i:=0 to XMLNodes.Count-1 do
begin
if XMLNodes[i].ChildNodes.Count = 0 then
begin // empty directory
XMLElem := XmlElement(XMLNodes[i]);
// MessageBox.Show('Empty directory: [' +
// XMLElem.Attributes['name'].Value + ']');
Inc(empty)
end
end;
MessageBox.Show(empty.ToString +
' empty directories on disk');
finally
XMLDom.Free
end;
end;
Wat echter veel nuttiger zou zijn, is de lijst met “lege” directories verwijderen uit het XML-document. En dat heeft tot gevolg dat een directory die hiervoor niet leeg was – maar uitsluitend subdirectories bevatte – nu plotseling ook leeg kan zijn, en dus ook verwijderd moet worden. De beste manier om dat te controleren is om het XML-document van achteren naar voren door te lopen (een subdirectory komt immers na zijn parent directory), wat nog een reden is waarom we hiervoor XML DOM moeten gebruiken.
We kunnen de RemoveChild methode gebruiken om van een item een van zijn children te verwijderen.
procedure TWinFormXML.btnXmlDOM _Click(
sender: System.Object;
e: System.EventArgs);
var
XMLDom: XmlDocument;
XMLNodes: XmlNodeList;
XMLElem: XmlElement;
empty,i: Integer;
begin
XMLDom := XmlDocument.Create;
try
XMLDom.Load('disk.xml');
XMLNodes :=
XMLDom.GetElementsbyTagName('directory');
empty := 0;
for i:=XMLNodes.Count-1 downto 0 do
begin // count backwards now...
if XMLNodes[i].ChildNodes.Count = 0 then
begin // empty directory
XMLElem := XmlElement(XMLNodes[i]);
XMLElem.ParentNode.RemoveChild(XMLElem);
// remove directory
Inc(empty)
end
end;
MessageBox.Show(empty.ToString +
' empty directories on disk');
finally
XMLDom.Save('diskdiff.xml');
XMLDom.Free
end;
end;
De vele aanroepen van RemoveChild kunnen overigens behoorlijk wat tijd kosten, maar het effect is wel een veel kleiner XML-document met daarin alleen maar de directories die daadwerkelijk de bestanden bevatten die we zochten (direct, of indirect via subdirectories die in de betreffende directory staan).
XSLT
Behalve een XML-document inlezen via de XmlDocument-class kunnen we ook XSL of XSLT gebruiken. XSL staat voor eXtensible Stylesheet Language, waarbij XSLT staat voor XSL Transformations van een XML-document naar iets anders (dat kan weer een XML-document zijn, maar ook HTML of zelfs PDF).
Met behulp van Delphi for .NET kunnen we een XML-document inladen en via een XslTransform-class omzetten naar een ander XML-document. Hierbij moeten we wel zelf het XSLT document schrijven. Een XSLT document is een XML-document dat met de volgende regel begint:
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
en eindigt met:
Tussen deze twee tags kunnen we dan de daadwerkelijke XSLT-expressies neerzetten, die bestaan uit een XPath-expressie en actie. Een voorbeeld van een XSLT-document dat iedere XML-tag weer op zichzelf afbeeldt (en daardoor een XML-document in feite kopieert), is als volgt:
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
De XPath-expressie die ik hier heb gebruikt voor de xsl:template is node() en die staat voor iedere node in het XML-document, waarna het xs:apply-template de @* weer vervangt door de node() zelf – een exacte kopie als resultaat.
Om deze XSL-transformaties (bijvoorbeeld in file DISK.XSLT) los te laten op een XML-document kunnen we de XslTransform-class gebruiken, bijvoorbeeld als volgt op ons DISK.XML document:
procedure TWinFormXML.btnXSLT_Click(
sender: System.Object; e: System.EventArgs);
var
XslTrans: XslTransform;
XmlDoc: XmlDocument;
XmlStream: FileStream;
begin
try
XslTrans := XslTransform.Create;
XslTrans.Load('disk.xslt');
XmlDoc := XmlDocument.Create;
XmlDoc.Load('disk.xml');
XmlStream := System.IO.FileStream.Create(
'disk-out.xml',
FileMode.Create);
XslTrans.Transform(XmlDoc, nil, XmlStream);
XmlStream.Flush;
except
on E: Exception do
writeln(E.ClassName, ': ', E.Message)
end;
XmlStream.Free;
XmlDoc.Free;
XslTrans.Free;
end;
We moeten hiervoor wel System.Xml.Xsl en System.Xml.XPath aan de uses-clause toevoegen.
Met behulp van Delphi for .NET kunnen we een XML-document inladen en via een XslTransform -lass omzetten naar een ander XML-document
Interessanter is het natuurlijk als we niet zomaar een XML-document kunnen kopiëren, maar als we er tussendoor wat aanpassingen in kunnen maken (en de “transform” daadwerkelijk toepassen).
Als voorbeeld kijk ik weer naar het DISK.XML document, waarbij we de naam van een bestand als attribuut opgenomen hebben, maar de creationtime, lastaccesstime en lastwritetime als subnode. Dit neemt relatief veel ruimte in beslag (omdat we een start- en end-element nodig hebben, waarbij we anders alleen de naam van het attribuut hoeven te gebruiken). Het oorspronkelijke DISK.XML document ziet er daardoor bijvoorbeeld als volgt uit:
258
2005-08-29 14:07:56
2005-09-13 08:44:27
2005-08-29 01:00:00
219
2005-09-01 09:06:30
2005-09-13 08:44:27
2005-09-01 09:14:39
Een XSL-transformatie die alle subnodes van een ‘file’ node kan omzetten naar attributen, is de volgende:
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
encoding="UTF-8" indent="yes"/>
De transformatie wordt in een aantal stappen gedaan. Ten eerste worden alle nodes op zichzelf afgebeeld. Voor de nodes met naam ‘file’ wordt een aparte behandeling uitgevoerd: alle attributen worden op zichzelf afgebeeld, maar alle subnodes worden omgezet in attributen waarbij de naam van het attribute de naam van de subnode wordt. Dit kan dus fout gaan als je meerdere subnodes heb met dezelfde naam (een directory met meerdere subdirectories zou dus fout gaan, omdat je niet meer dan één attribuut met dezelfde naam kunt hebben).
Bovendien zou dit fout kunnen gaan als je al een attribuut zou hebben met een naam van een subnode, maar dat komt in ons voorbeeld niet voor.
Het resultaat van deze XSLT op het oorspronkelijke DISK.XML document is als volgt:
creationtime="2005-08-29 14:07:56"
lastaccesstime="2005-09-13 08:44:27"
lastwritetime="2005-08-29 01:00:00">
creationtime="2005-09-01 09:06:30"
lastaccesstime="2005-09-13 08:44:27"
lastwritetime="2005-09-01 09:14:39">
Het XML-document is kleiner geworden, en in mijn ogen iets makkelijker om te bewerken (alle attributen die bij een bestand horen staan ook als attribuut opgenomen, en niet langer als subnode).
Als we juist alle attributen willen omzetten in subnodes, dan hoeven we alleen maar aan te geven dat iedere node() door zichzelf wordt vervangen, en ieder attribute wordt vervangen door een element met dezelfde naam als het attribuut (te vinden in local-name(.)).
Dit kunnen we als volgt uitdrukken:
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
encoding="UTF-8" indent="yes"/>
Samenvatting
In dit artikel heb ik laten zien hoe we XML-documenten kunnen lezen, schrijven en bewerken vanuit toepassingen geschreven met Delphi for .NET. Ik heb zowel XmlWriter, XmlReader, XmlDocument als de XslTransform gebruikt, en heb een korte introductie met wat voorbeelden van XSLT gegeven.
Ik heb uitsluitend .NET classes gebruikt, dus behalve Delphi for .NET kunnen de voorbeelden ook gebruikt worden met RemObjects Chrome (of op eenvoudige wijze vertaald worden naar C#).