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