在Delphi XE7中将节点添加到IXMLNodeCollection

时间:2017-01-15 16:15:40

标签: xml delphi

我一直在使用IXMLNodeCollection接口和TXMLNodeCollection类时遇到问题。我把问题缩小到了这个范围。考虑一个简单的XML文档,例如:

<?xml version="1.0"?>
<Root id="27">
  <SomeItems>
    <SomeItem id="69"/>
    <SomeItem id="84"/>
    <SomeItem id="244"/>
  </SomeItems>
</Root>

基本上,Root节点包含SomeItemsSomeItem节点集合。我已将其实现为三个基于IXMLNode / iXMLNodeCollection的接口,以及相关的TXMLNode / TXMLNodeCollection类,如下所示:

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.

现在在我的应用中,我使用IXMLDocument作为根节点创建IXMLParent,然后我想在该文档的SomeItems集合中创建一些节点。这是我添加单个节点的代码

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;

而不是打印NumItems: 1,它说有4个项目。此外,尝试访问集合中的第一个节点时会抛出异常!似乎par.SomeItems[0]构造不返回IXMLSomeItem引用。为什么呢?

真奇怪的是,如果我保存XML文档,然后重新加载它,它现在可以工作了!这是整个计划:

程序测试;

{$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.

以下是该程序的整个输出:

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

(请注意,这个程序最后抛出一个异常;我不关心这个,因为这是一个人为的演示。而且,奇怪的增加缩进很烦人,但并不重要;如果你知道如何防止这种情况,好吧,奖励积分!)这种保存/阅读解决方法在过去有效,但在实践中它很笨拙。我真的希望能够添加到IXMLNodeCollection然后能够在创建它们之后立即访问代码中的节点。有人可以解释我做错了吗?

1 个答案:

答案 0 :(得分:3)

  

而不是打印NumItems:1,它说有4个项目。此外,尝试访问集合中的第一个节点时会抛出异常! par.SomeItems [0]构造似乎没有返回IXMLSomeItem引用。为什么呢?

一个词:空白

您正在启用doNodeAutoIndent标志,该标志在元素之间插入缩进空格。 XML表示使用DOM中的额外节点的空白,例如:

Root
|_ whitespace
|_ SomeItems
    |_ whitespace
    |_ SomeItem
    |_ whitespace
    |_ SomeItem
    |_ whitespace
    |_ SomeItem
    |_ whitespace
  

真奇怪的是,如果我保存XML文档,然后重新加载它,它现在可以工作了!

当您加载XML文档时,如果(T|I)XMLDocument.ParseOptions属性未启用poPreserveWhiteSpace标志,则解析器会丢弃空白,因此DOM中不存在这些额外的空白节点。

Root
|_ SomeItems
    |_ SomeItem
    |_ SomeItem
    |_ SomeItem
  

请注意,此程序最后会抛出异常

在调用CoUnitialize()之前,您没有释放所有接口。非nil接口在超出范围时会自动释放,在这种情况下是end之后。您将par变量设置为nil而不是item变量。接口是引用计数的,并且XML元素节点具有对其父节点和拥有文档的引用,因此{n} par变量使item对象保持活动状态。

  

我真的希望能够添加到IXMLNodeCollection,然后能够在创建它们之后立即访问代码中的节点。

这就是Add()返回新添加的节点的原因,因此只需按原样使用它,您就不需要使用Nodes[]属性来检索它。如果你使用Nodes[],你需要很好地理解DOM的布局。