Search code examples
xmldelphixml-parsingtclientdataset

Delphi Modify Existing XML Document Structure


I have an existing XML Document with nested tables. I want to open it, read in and MODIFY the structure (i.e. add or delete columns/fields). Ignoring the nested tables, here's a complete XML test doc:

<DATAPACKET Version="2.0">
  <METADATA>
    <FIELDS>
      <FIELD attrname="StringField" fieldtype="string" WIDTH="20" /> 
      <FIELD attrname="IntField" fieldtype="i4" /> 
    </FIELDS>
    <PARAMS CHANGE_LOG="1 0 4 2 0 4" /> 
  </METADATA>
  <ROWDATA>
    <ROW RowState="4" StringField="String" IntField="234" /> 
    <ROW RowState="4" StringField="234" IntField="24" /> 
  </ROWDATA>
</DATAPACKET>

The following code throws an exception on open that "testField" was not found, presumably because it doesn't exist in the underlying XML file.

ClientDataSet1.Close;

with TStringField.Create(ClientDataSet1) do 
begin
  FieldName := 'testField';
  DataSet := ClientDataSet1; 
end; 

with ClientDataSet1 do 
begin 
  CreateDataSet; 
  Open; 
end; 

If I add:

with ClientDataSet1 do
begin
  FieldDefs.Clear;
  Fields.Clear;
end;

an exception isn't thrown, but the first two fields disappear and the new structure is not written to the XML doc file unless I enter some data.

<DATAPACKET Version="2.0">
  <METADATA>
    <FIELDS>
      <FIELD attrname="testField" fieldtype="string" WIDTH="20" /> 
    </FIELDS>
    <PARAMS CHANGE_LOG="1 0 4" /> 
  </METADATA>
  <ROWDATA>
    <ROW RowState="4" testField="12321" /> 
  </ROWDATA>
</DATAPACKET>

Is there a standard or recommended way of adding a field to an existing XML doc without losing the data?

Cheers, Tanner


Solution

  • You are not quite going about this the right way; for starters, CreateDataSet completely removes whatever data was previously in the ClientDataSet

    The next thing is that you don't want to be doing this with persistent Fields and/or FieldDefs in place, so clear them while you're doing your changes. Whether you create them afterwards is up to you, but if you are going to create TFields in code, you should create one for every field in the XML metadata, starting from an empty Fields list in the CDS.

    The example project below should show you how to get what you're after. It

    • Loads the dataset from the XML in a TMemo, Memo1. In mine, I just copied and pasted the XML from your q. This step is basically to show that the dataset is correctly populated;

    • Then, the code in AddFieldToXML adds the new field to the Metadata in the XML and copies the result to Memo2, and saves it to disk. Note: As written, it does not write any data to the new field, but you should be able to get the idea for how to do that from AddFieldToXML.

    Finally, it closes and reopens the CDS by loading it from the altered XML

    Code:

    uses
      Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
      StdCtrls, ExtCtrls, DBCtrls, Grids, DBGrids, DB, DBClient, MSXML;
    
    type
      TForm1 = class(TForm)
        CDS1: TClientDataSet;
        DataSource1: TDataSource;
        DBGrid1: TDBGrid;
        DBNavigator1: TDBNavigator;
        Button1: TButton;
        Memo1: TMemo;
        Memo2: TMemo;
        procedure Button1Click(Sender: TObject);
        procedure FormCreate(Sender: TObject);
      public
        ExistingFN : String;
        NewFN : String;
        procedure AddFieldToXML;
        procedure LoadNewData;
      end;
    [...]
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      ExistingFN :=  'C:\Temp\Data.XML';
      NewFN := 'C:\Temp\NewData.XML';
    
      Memo1.Lines.SaveToFile(ExistingFN);
    
      CDS1.Fields.Clear;
      CDS1.FieldDefs.Clear;
      CDS1.LoadFromFile(ExistingFN);
    
    end;
    
    procedure TForm1.AddFieldToXML;
    var
      XmlDoc: IXMLDOMDocument;
      NodeList : IXmlDOMNodeList;
      Node,
      NewNode : IXmlDomNode;
      E : IXmlDomElement;
      PathQuery : String;
    begin
      PathQuery := '/DATAPACKET/METADATA/FIELDS';
    
      Memo2.Lines.Clear;
      XmlDoc := CoDOMDocument.Create; //CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
      XmlDoc.Async := False;
      XmlDoc.LoadXML(Memo1.Lines.Text);
      if xmlDoc.parseError.errorCode <> 0 then
        raise Exception.Create('XML Load error:' + xmlDoc.parseError.reason);
    
      NodeList := XmlDoc.documentElement.SelectNodes(PathQuery);
    
      if NodeList.length > 0 then begin
        E := XMLDoc.createElement('FIELD');
        NewNode := E as IXMLDomNode;
        E.setAttribute('attrname', 'testField');
        E.setAttribute('fieldtype', 'string');
        E.setAttribute('WIDTH', '20');
        NodeList.item[0].appendChild(NewNode);
      end;
      Memo2.Lines.Text := XMLDoc.documentElement.xml;
      Memo2.Lines.SaveToFile(NewFN);
    end;
    
    procedure TForm1.LoadNewData;
    begin
      CDS1.Close;
      CDS1.Fields.Clear;
      CDS1.FieldDefs.Clear;
      CDS1.LoadFromFile(NewFN);
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    begin
      AddFieldToXML;
      LoadNewData;
    end;
    

    Once you've saved the new XML to disk, you can load it into the CDS in the IDE by right-clicking the CDS and using Load from MyBase file (for D7, similar for later versions), and then create persistent TFields if you want.

    The XML code is for the version of MSXML.Pas that came with D7, btw. I tend to post code for D7 unless a later Delphi version is required by the q.