Thursday, October 8, 2009

Xml Serialization - Basic Usage

This post relates to XmlSerial.pas which provides XML serialization and de-serialization using the new RTTI in Delphi 2010.

This class has been built to serialize any public and published property or Field.
This was done to mimic the behavior found in the .NET Xml Serialization with the goal of having a set of objects that can serialize in .NET using Delphi Prism and Win32 using Delphi 2010. The complete goals of what I want to accomplish with XmlSerial.pas look at the source code, I detailed out what still needs to be done.

There are two ways to work with the Xml Serialization, one depends on a Pointer to the Type Information, the other uses Generics to get it form the type specified as a parameter.

// Method 1:
var
o : TypeIWantToSerailze;
s : TXmlTypeSerializer;
x : TXmlDocument;
v : TValue;
begin
s := TXmlTypeSerializer.create(TypeInfo(o));
x := TXmlDocument.Create(Self); // NEVER PASS NIL!!!
s.Serialize(x,o);
x.SaveToFile('FileName.txt');
v := s.Deserialize(x);
o := v.AsType<TypeIWantToSerailze>;
x.free;
s.free;
end;

// Method 2:
var
o : TypeIWantToSerailze;
s : TXmlSerializer<TypeIWantToSerailze>;
x : TXmlDocument;
begin
s := TXmlTypeSerializer<TypeIWantToSerailze>.create;
x := TXmlDocument.Create(Self); // NEVER PASS NIL!!!
s.Serialize(x,o);
x.SaveToFile('FileName.txt');
o := s.Deserialize(x);
x.free;
s.free;
end;

And here is the full code showing how to do this using the generic version.

unit uPerson;
interface

type
TPerson = class(TObject)
private
FLastName: String;
FBirthday: TDateTime;
FMiddleName: String;
FFirstName: String;
function GetFullName: String;
published
public
property FirstName : String read FFirstName write FFirstName;
property LastName : String read FLastName write FLastName;
property MiddleName : String read FMiddleName write FMiddleName;
property FullName : String read GetFullName;
property Birthday : TDateTime read FBirthday write FBirthday;
procedure Save(FileName : String);
class function Load(FileName : String) : TPerson;
end;


implementation
uses
XmlDoc,
Classes,
XmlSerial;

{ TPerson }

function TPerson.GetFullName: String;
begin
result := FFirstName + ' ' + FMiddleName + ' ' + FLastName;
end;

class function TPerson.Load(FileName: String): TPerson;
var
lSerialize : TXmlSerializer<TPerson>;
lOwner : TComponent;
lDoc : TxmlDocument;
begin
lOwner := TComponent.Create(nil); // Required to make TXmlDocument work!
try
lDoc := TXmlDocument.Create(lOwner); // will be freed with lOwner.Free
lDoc.LoadFromFile(FileName);
lSerialize := TXmlSerializer<TPerson>.Create;
try
result := lSerialize.Deserialize(lDoc);
finally
lSerialize.Free;
end;
finally
lOwner.Free;
end;
end;

procedure TPerson.Save(FileName: String);
var
lSerialize : TXmlSerializer<TPerson>;
lOwner : TComponent;
lDoc : TxmlDocument;
begin
lOwner := TComponent.Create(nil); // Required to make TXmlDocument work!
try
lDoc := TXmlDocument.Create(lOwner); // will be freed with lOwner.Free
lSerialize := TXmlSerializer<TPerson>.Create;
try
lSerialize.Serialize(lDoc,Self);
lDoc.SaveToFile(FileName);
finally
lSerialize.Free;
end;
finally
lOwner.Free;
end;
end;

end.

The project that shows how to use this object.

program Project12;

{$APPTYPE CONSOLE}

uses
SysUtils,
Windows,
XmlSerial,
Forms,
uPerson in 'uPerson.pas';

var
P : TPerson;

begin
Application.Initialize;
P := TPerson.Create;
try
P.FirstName := 'John';
P.MiddleName := 'C';
P.LastName := 'Doe';
P.Birthday := EncodeDate(1995,2,14);
P.Save('C:\test.xml');
finally
P.Free;
end;

p := TPerson.Load('C:\test.xml');
try
writeln(P.FullName);
Writeln(DateToStr(P.Birthday));
Readln;
finally
p.Free;
end;
end.

Output:

John C Doe
2/14/1995

Current output of the XML file, notice the Date, it's something I want to change, if you check the roadmap in the xmlserial.pas source.


<TPerson>
<FirstName>John</FirstName>
<LastName>Doe</LastName>
<MiddleName>C</MiddleName>
<Birthday>34744</Birthday>
</TPerson>


Hopefully this covers enough of how to use this to get you started.
I will show how to customize the behavior using attributes in a future post.

RTTI Article List

11 comments:

  1. Hi Robert, your articles are very nice.
    But why you haven't used TConverter<TSerial> ?

    ReplyDelete
  2. Because I did not know it existed.

    It is intresting and deserves further look, but right now looking at it with only a few minutes to review, I am not sure I would want to use it.

    ReplyDelete
  3. I have now looked with a bit more detail at TConvert<TSerial>

    It designed to serialize all of the fields only,
    which works for most serialization. Would might be great thing to look at for a binary serializer.

    But, when you want to mimic what .NET provides to provide a common code base, you need to work with just the public and published fields and properties.

    Although, I might be mistaken it also does not appear have an elegant way I could control if the serialization names and locations like I do with Attributes.

    http://robstechcorner.blogspot.com/2009/10/xml-serialization-control-via.html

    ReplyDelete
  4. Really cool. Is your serializer serializing only stuff with the [TSerialializable] attribute or all properties?

    ReplyDelete
  5. It's handles all properties and fields in public and published, unless you add the
    [XmlIgnore]

    ReplyDelete
  6. Is there any way to have a data structure that results in a parent-child xml, in .net I used an array inside the class to have that. I want to have an structure that results in something like this
    <people>
    <count>2</count>
    <person>
    <name>Person1</name>
    </person>
    <person>
    <name>Person2</name>
    </person>
    </people>

    Tnx

    ReplyDelete
  7. Why do you use TXMLDocument instead of IXMLDocument?
    Because IXMLDocument not require TComponent to make TXmlDocument work!

    ReplyDelete
  8. hi,
    TPerson in new class TInformation , if FInformation is null and lSerialize.Serialize(lDoc, person); error.
    How do I solve this problem?

    TPerson = Class(TPersistent)
    private
    FInformation:TInformation
    ...

    ReplyDelete
    Replies
    1. The example code does not deal with that concept. You would have to modify the code to deal with nil values.

      Delete
  9. TXmlCustomTypeSerializer.SerializeValue change
    my problem nill class.
    I solved:
    ...
    var
    Tmpclass:TClass;
    isOk:Boolean;
    begin
    ....
    if Not (Map.isList) then
    begin
    for MapItem in Map.List do
    begin
    Assert(Assigned(MapItem.Member));
    isOk:=true;
    if (Value.IsClass) then
    begin
    Tmpclass:=Value.AsClass;
    isOk:=Assigned(Tmpclass);
    end;
    if (isOk) then
    begin
    CurrentValue := MapItem.Member.GetValue(Value);
    SerializeValue(CurrentValue, MapItem, NewNode, Doc);
    end;

    ReplyDelete
  10. hi
    TXmlCustomTypeSerializer.DeSerializeValue change

    class is null and before control Child.ChildNodes.Count > 1 add code


    function TXmlCustomTypeSerializer.DeSerializeValue(Node: IXMLNode; MapType: TRttiType; Map: TMemberMap): TValue;
    var
    Children: IXMLNodeList;
    Child: IXMLNode;
    MapItem: TMemberMap;
    ChildValue: TValue;
    I: Integer;
    ListValue: TValue;
    lAdd: TElementAdd;
    ListMapItem: TMemberMap;
    begin
    if MapType.IsInstance then
    begin
    if Node.ChildNodes.Count > 1 then
    Result := MapType.AsInstance.MetaclassType.create;
    end
    else
    begin
    // Create an Empty Record, Value Type, etc....
    TValue.Make(nil, MapType.Handle, Result);
    end;
    // structure type with Mapped Members, and it's not a list type
    if (MapType is TRttiStructuredType) and (Length(Map.List) > 0) and (Not Map.isList) and (Node.ChildNodes.Count > 1) then
    begin
    Children := Node.ChildNodes;
    for MapItem in Map.List do
    begin
    case MapItem.NodeType of
    ntElement:
    Child := Children.FindNode(MapItem.NodeName);
    ntAttribute:
    begin
    if Node.HasAttribute(MapItem.NodeName) then
    Child := Node.AttributeNodes.FindNode(MapItem.NodeName)
    else
    Child := nil;
    end;
    end; { Case }

    if Assigned(Child) then
    begin
    if (MapItem.Member.MemberType.IsInstance) then
    begin
    if Child.ChildNodes.Count > 1 then
    begin
    ChildValue := DeSerializeValue(Child, MapItem.Member.MemberType, MapItem);
    if (not ChildValue.isEmpty) then
    MapItem.Member.SetValue(Result, ChildValue);
    end;
    end
    else
    begin
    ChildValue := DeSerializeValue(Child, MapItem.Member.MemberType, MapItem);
    if (not ChildValue.isEmpty) then
    MapItem.Member.SetValue(Result, ChildValue);
    end;
    end;
    end;
    end
    else if Map.isList then
    begin
    Children := Node.ChildNodes;
    // Create Correct Element Add Factory
    if Children.Count > 1 then
    lAdd := TElementAddFactory.CreateElementAdd(Result);
    // Loop through items to add.
    for I := 0 to Children.Count - 1 do
    begin
    Child := Children.Nodes[I];
    ListMapItem := Map;
    ListMapItem.isList := false;
    ListMapItem.NodeType := ntElement;
    ListValue := DeSerializeValue(Child, FCtx.GetType(lAdd.AddType), ListMapItem);
    lAdd.Add(ListValue);
    end;

    if Children.Count > 1 then
    begin
    lAdd.AddFinalize;
    Result := lAdd.List;
    end;
    end
    else // Not a structure Type or List, convert from String to Value
    begin
    Result := TextToValue(MapType, Node.Text);
    end;

    end;



    ReplyDelete