Bob
Bob

Reputation: 707

Adding Nodes to an IXMLNodeCollection in Delphi XE7

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

Answers (1)

Remy Lebeau
Remy Lebeau

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

Related Questions