Reputation: 707
I have been having trouble using IXMLNodeCollection interface and TXMLNodeCollection class for some time now. I have narrowed down the problem to this. Consider a simple XML document such as this:
<?xml version="1.0"?>
<Root id="27">
<SomeItems>
<SomeItem id="69"/>
<SomeItem id="84"/>
<SomeItem id="244"/>
</SomeItems>
</Root>
Basically, the Root
node contains a SomeItems
collection of SomeItem
nodes. I have implemented this as three IXMLNode/iXMLNodeCollection based interfaces, with associated TXMLNode/TXMLNodeCollection cllasses like this:
unit XNodes;
interface
uses
System.SysUtils, Xml.xmldom, Xml.XMLDoc, Xml.XMLIntf;
type
IXMLAParent = interface;
IXMLSomeItems = interface;
IXMLSomeItem = interface;
{ AParent }
IXMLAParent = interface(IXMLNode)
['{61AB551A-F1B5-437C-A265-9FB4BBBC8A8B}']
{ Property Accessors }
function GetId: Integer;
procedure SetId(Value: Integer);
function GetSomeItems: IXMLSomeItems;
{ Methods & Properties }
property Id: Integer read GetId write SetId;
property SomeItems: IXMLSomeItems read GetSomeItems;
end;
TXMLAParent = class(TXMLNode, IXMLAParent)
protected
function GetId: Integer;
procedure SetId(Value: Integer);
function GetSomeItems: IXMLSomeItems;
public
procedure AfterConstruction; override;
end;
{ SomeItems }
IXMLSomeItems = interface(IXMLNodeCollection)
['{7071899E-8F58-4685-A908-8E4E1C13F556}']
{ Property Accessors }
function GetSomeItem(Index: Integer): IXMLSomeItem;
{ Methods & Properties }
function Add: IXMLSomeItem;
function Insert(const Index: Integer): IXMLSomeItem;
property SomeItem[Index: Integer]: IXMLSomeItem read GetSomeItem; default;
end;
TXMLSomeItems = class(TXMLNodeCollection, IXMLSomeItems)
protected
{ IXMLSomeItems }
function GetSomeItem(Index: Integer): IXMLSomeItem;
function Add: IXMLSomeItem;
function Insert(const Index: Integer): IXMLSomeItem;
public
procedure AfterConstruction; override;
end;
{ SomeItem }
IXMLSomeItem = interface(IXMLNode)
['{4F1FC343-9EAD-4C33-93E1-BD511E59F44A}']
{ Property Accessors }
function GetId: Integer;
procedure SetId(Value: Integer);
{ Methods & Properties }
property Id: Integer read GetId write SetId;
end;
TXMLSomeItem = class(TXMLNode, IXMLSomeItem)
protected
{ IXMLSomeItem }
function GetId: Integer;
procedure SetId(Value: Integer);
public
procedure AfterConstruction; override;
end;
implementation
{ TXMLAParent }
procedure TXMLAParent.AfterConstruction;
begin
RegisterChildNode('SomeItems', TXMLSomeItems);
inherited;
end;
function TXMLAParent.GetId: Integer;
begin
try
Result := AttributeNodes['id'].NodeValue;
except
on Exception do
begin
AttributeNodes['id'].NodeValue := -1;
end;
end;
end;
procedure TXMLAParent.SetId(Value: Integer);
begin
SetAttribute('id', Value);
end;
function TXMLAParent.GetSomeItems: IXMLSomeItems;
begin
try
Result := ChildNodes['SomeItems'] as IXMLSomeItems;
except
on E: Exception do
begin
Result := Self.AddChild('SomeItems') as IXMLSomeItems;
end;
end;
end;
{ TXMLSomeItems }
procedure TXMLSomeItems.AfterConstruction;
begin
RegisterChildNode('SomeItem', TXMLSomeItem);
ItemTag := 'SomeItem';
ItemInterface := IXMLSomeItem;
inherited;
end;
function TXMLSomeItems.GetSomeItem(Index: Integer): IXMLSomeItem;
begin
Result := List[Index] as IXMLSomeItem;
end;
function TXMLSomeItems.Add: IXMLSomeItem;
begin
Result := AddItem(-1) as IXMLSomeItem;
end;
function TXMLSomeItems.Insert(const Index: Integer): IXMLSomeItem;
begin
Result := AddItem(Index) as IXMLSomeItem;
end;
{ TXMLSomeItem }
procedure TXMLSomeItem.AfterConstruction;
begin
inherited;
end;
function TXMLSomeItem.GetId: Integer;
begin
Result := AttributeNodes['id'].NodeValue;
end;
procedure TXMLSomeItem.SetId(Value: Integer);
begin
SetAttribute('id', Value);
end;
end.
Now in my app, I create an IXMLDocument
using IXMLParent
as the root node, then I want to create some nodes in the SomeItems
collection in that document. Here is my code that adds a single node
par := NewXMLDocument.GetDocBinding('Root', TXMLAParent, '') as IXMLAParent;
par.OwnerDocument.Options := par.OwnerDocument.Options + [doNodeAutoIndent];
par.Id := 27;
WriteLn('NewXML:');
WriteLn(par.XML);
WriteLn('NumChild: ', par.SomeItems.Count);
item := par.SomeItems.Add;
item.Id := 69;
WriteLn('Add Item:');
WriteLn(par.XML);
WriteLn('NumItems: ', par.SomeItems.Count);
try
WriteLn('Does this fail? ', par.SomeItems[0].Id);
except
WriteLn('Yes, that failed!');
end;
Instead of printing NumItems: 1
, it says there are 4 items. Furthermore, an exception is thrown when trying to access the first node in the collection! It seems the par.SomeItems[0]
construct doesn't return a IXMLSomeItem reference. Why?
The really odd thing is that, if I save the XML document, then reload it, it now works! Here is the entire program:
program test;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, ActiveX, Xml.xmldom, Xml.XMLDoc, Xml.XMLIntf, XNodes;
var
par: IXMLAParent;
item: IXMLSomeItem;
doc: IXMLDocument;
begin
CoInitialize(nil);
par := NewXMLDocument.GetDocBinding('Root', TXMLAParent, '') as IXMLAParent;
par.OwnerDocument.Options := par.OwnerDocument.Options + [doNodeAutoIndent];
par.Id := 27;
WriteLn('NewXML:');
WriteLn(par.XML);
WriteLn('NumChild: ', par.SomeItems.Count);
item := par.SomeItems.Add;
item.Id := 69;
WriteLn('Add Item:');
WriteLn(par.XML);
WriteLn('NumItems: ', par.SomeItems.Count);
try
WriteLn('Does this fail? ', par.SomeItems[0].Id);
except
WriteLn('Yes, that failed!');
end;
doc := par.OwnerDocument;
doc.Active := True;
doc.Options := doc.Options + [doNodeAutoIndent];
doc.SaveToFile('temp.xml');
par := nil;
par := LoadXMLDocument('temp.xml').GetDocBinding('Root', TXMLAParent, '') as IXMLAParent;
par.OwnerDocument.Options := par.OwnerDocument.Options + [doNodeAutoIndent];
WriteLn('OpenXML:');
WriteLn(par.XML);
WriteLn('NumItems: ', par.SomeItems.Count);
WriteLn('This works: ', par.SomeItems[0].Id);
par := nil;
CoUninitialize;
end.
Here is the entire output from that program:
NewXML:
<Root id="27"/>
NumChild: 0
Add Item:
<Root id="27">
<SomeItems>
<SomeItem id="69"/>
</SomeItems>
</Root>
NumItems: 4
Yes, that failed!
OpenXML:
<Root id="27">
<SomeItems>
<SomeItem id="69"/>
</SomeItems>
</Root>
NumItems: 1
This works: 69
(Note that this program throws an exception at the end; I don't care about that, as this is a contrived demo. Also, the odd increased indentation is annoying, but not crucial; if you know how to prevent that, well, bonus points!) This saving/reading workaround has worked in the past, but it is clumsy in practice. I would really like to be able to add to the IXMLNodeCollection
then be able to access the nodes in code, immediately after creating them. Can someone explain what I am doing wrong?
Upvotes: 0
Views: 1304
Reputation: 596206
Instead of printing NumItems: 1, it says there are 4 items. Furthermore, an exception is thrown when trying to access the first node in the collection! It seems the par.SomeItems[0] construct doesn't return a IXMLSomeItem reference. Why?
One word: whitespace.
You are enabling the doNodeAutoIndent
flag, which inserts indentation whitespace between elements. XML represents whitespace using extra nodes in the DOM, eg:
Root |_ whitespace |_ SomeItems |_ whitespace |_ SomeItem |_ whitespace |_ SomeItem |_ whitespace |_ SomeItem |_ whitespace
The really odd thing is that, if I save the XML document, then reload it, it now works!
When you load an XML document, the parser discards whitespace if the (T|I)XMLDocument.ParseOptions
property does not have the poPreserveWhiteSpace
flag enabled, so those extra whitespace nodes would not exist in the DOM.
Root |_ SomeItems |_ SomeItem |_ SomeItem |_ SomeItem
Note that this program throws an exception at the end
You are not releasing all of the interfaces before calling CoUnitialize()
. Non-nil interfaces are automatically released when they go out of scope, which in this case is after the end
. You are setting the par
variable to nil but not the item
variable. Interfaces are reference counted, and the XML element nodes have a reference to their parent node and the owning document, so the par
object is being kept alive by the non-nil item
variable.
I would really like to be able to add to the IXMLNodeCollection then be able to access the nodes in code, immediately after creating them.
That is why Add()
returns the newly added node, so just use it as-is, you don't need to use the Nodes[]
property to retrieve it. If you do use Nodes[]
, you need to have a good understanding of the DOM's layout.
Upvotes: 4