1. Technology

Format XML Data - Properly Indent TXMLDocument XML Tags Without doNodeAutoIndent

Fixing the "This operation can not be performed with a Node of type PCDATA"

By

Delphi's TXmlDocument component can be used to either read (and process) an existing XML document or to construct a new, empty XML document.

When using the TXMLDocument to create XML documents you might receive a nasty EOleException: This operation can not be performed with a Node of type PCDATA.

If you want the resulting XML to look "pretty" - so that each element node appears on its own line, indented appropriately to reflect its nesting in the node hierarchy, you might be adding the doNodeAutoIndent flag in the Options property of the TXMLDocument instance.

When doNodeAutoIndent is included in the Options property, the child nodes that you add to the XML are automatically indented from their parent nodes. The NodeIndentStr property (defaults to 2 spaces " ") indicates the string that is inserted before nested nodes in the formatted XML text.

When Things Go Wrong: "This operation can not be performed with a Node of type PCDATA"

If you adding nodes to the DocumentElement property of the TXMLDocument (which is an IXMLNode) using some "complex" calls - where child nodes are sent to methods where more child nodes are added - you might receive the This operation can not be performed with a Node of type PCDATA exception.

The problem is in the doNodeAutoIndent. With xmlDoc.DocumentElement.AddChild('tag-name') a new node will be added to the root node of the XML document. AddChild adds a node of ntElement type.

When doNodeAutoIndent is set, adding a child ntElement node will also add the indentation string specified by the NodeIndentStr property - this string is treated as a node of type ntCData. CDATA sections identify blocks of text that would otherwise be interpreted as markup.

An ntCData node can't have any child nodes - and this is where the problem is!

Here's how to force the problem:

 var
   myXML : TXMLDocument;
   childNode : IXMLNode;
 begin
   myXML := TXMLDocument.Create(nil);
   try
     myXML.Options := [doNodeAutoIndent];
     myXML.Active := true;
     myXML.AddChild('document-element');
     childNode := myXML.DocumentElement.AddChild('node');
 
     //it is to expect that childNode = myXML.DocumentElement.ChildNodes.Last!
 
     //this would raise "This operation can not be performed with a Node of type PCDATA"
     myXML.DocumentElement.ChildNodes.Last.AddChild('child');
   finally
     myXML := nil;
   end;
 end; 
You would expect that "myXML.DocumentElement.ChildNodes.Last" is "childNode" node but it returns the ntCData type node as it was created after the childNode since doNodeAutoIndent is included in the Options property.

Fixing the "This operation can not be performed with a Node of type PCDATA"

Simply removing the doNodeAutoIndent from the Options property (or better to say "not setting it") would solve the problem!

BUT, there's a BUT. The resulting XML will look ugly - as (child) tags would not be nicely indented :(

With the doNodeAutoIndent set, the resulting XML would be nice:

 <document-element>
   <node>
     <child>
     </child>
   </node>
 </document-element>
 

Without the doNodeAutoIndent set, the resulting XML would be ugly:

 <document-element><node><child/></node></document-element> 
Note: this is not so ugly as the XML is short :( Imagine a BIG one with lots of node attribues and child nodes ;)

xmlDoc.FormatXMLData To The Rescue!

Now, I must be able to use "myXML.DocumentElement.ChildNodes.Last.AddChild()" AND I do want the resuting saved XML to be nicely formatted - BUT I CANNOT use the doNodeAutoIndent :(

Lucky for me, there's a nice method located in the xmlDoc.pas (where TXMLDocument is defined) called "FormatXMLData".

FormatXMLData converts an XML string into an XML string BUT having each element node on its own line, indented appropriately to reflect its nesting in the node hierarchy!!

Finally, the working code would look like:

 var
   myXML : TXMLDocument;
   childNode : IXMLNode;
 begin
   myXML := TXMLDocument.Create(nil);
   try
     //NO GO
     //myXML.Options := [doNodeAutoIndent];
     myXML.Active := true;
     myXML.AddChild('document-element');
     childNode := myXML.DocumentElement.AddChild('node');
 
     //it is to expect that childNode = myXML.DocumentElement.ChildNodes.Last!
 
     //this would raise "This operation can not be performed
     //with a Node of type PCDATA" with doNodeAutoIndent
 
     myXML.DocumentElement.ChildNodes.Last.AddChild('child');
 
     myXML.XML.Text := xmlDoc.FormatXMLData(myXML.XML.Text);
 
     myXML.Active := true;
     myXML.SaveToFile('nice-xml.xml');
   finally
     myXML := nil;
   end;
 end; 
That's it. RIP the nasty "doNodeAutoIndent".

Delphi tips navigator:
» Redirect Mouse Wheel Message To A Control Under The Mouse in Delphi Applications
« Preserve Code Folding When Reopening a Delphi Project

©2014 About.com. All rights reserved.