It has been a very lean and easy option of .Net to able serialize/deserialize any serializable object instance.

Only closest option in Delphi is to stream the component using WriteComponent/WriteComponentRes of TStream/TWriter (used for Form storage as DFM, for example). It can be then read back using appropriate counterpart ReadComponent/ReadComponentRes.

Depend on your situation, simply calling .Assign method would work if you are coping data from one instance to another. But it only works between inherited classed which know about each other.

Can we find there something which will allow to pass objects states in more readable format?

There is a very powerful infrastructure available to do full serialization without knowing underlying class structure – RTTI (Run-time Type Information).

All functions we would be looking at are defined in TypInfo.pas unit.

First thing first. To be able to work within RTTI, you need to operate on the object which has published and public properties.

function GetPropList(TypeInfo: PTypeInfo;  out APropList: PPropList): integer;

A function will return number and reference to the list (array) of properties published (public and published) by the class (VMT information). List will also include published methods. Simply walking through it will give you an access to property/method information.

In our example we are not interested in the published methods, so lets filter it out:

var
  i: integer;
  lPropInfo: PPropInfo;
  lPropCount: integer;
  lPropList: PPropList;
  lPropType: PPTypeInfo;
begin
  lPropCount := GetPropList(PTypeInfo(AObject.ClassInfo), lPropList);
  for i := 0 to lPropCount - 1 do
  begin
   lPropInfo := lPropList^[i];
    lPropType := lPropInfo^.PropType;
    if lPropType^.Kind = tkMethod then
    Continue;
  // ... processing of the properties ...
  end;
end;

What other Kind of information is present in the list? Bellow is full definition of the type

type
  TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration,
tkFloat, tkString, tkSet, tkClass, tkMethod, tkWChar,
tkLString, tkWString, tkVariant, tkArray, tkRecord,
tkInterface, tkInt64, tkDynArray);

Now you are ready to get or set the value of the property:

function GetPropValue(Instance: TObject; const PropName: string;
  PreferStrings: Boolean = True): Variant;
procedure SetPropValue(Instance: TObject; const PropName: string;
  const Value: Variant);

And now you are ready to serialize your object, store it, load definition and deserialize an object back. And it could be not necessarily the same object.
You would have to loop through the properties, read the values, and store it as an XML for later use.

I am not going to go through all specific details in this post, instead, you can find a full code for XML Class serializer here.


20 Comments

lwgboy · Nov 25, 2008 at 06:33

Your Delphi VCL Component is very useful!
You’re a gret man!
Thank you very much!

Will · Jan 26, 2009 at 03:31

A very useful article.

Downloading the code, I couldn’t get the SerilializeObjectToXML to work without amending the code slightly:

see the changed line comment.

if aNodeName='' then
    begin
      if (aInstance is TComponent) and ((aInstance as TComponent).Name'') then
        lName := (aInstance as TComponent).Name
      else

//***** Changed line *****
        lName := (aInstance as TObject).ClassName;
//***** Changed line *****
    end
    else
      lName := aNodeName;

Am I using this incorrectly ?

Regards
Will

Serguei Dosyukov · Jan 26, 2009 at 16:47

Not exactly.

  • Serialized object has to have a name to be uniquely identified.
  • Class name is not unique Id for identification.
  • RTTI would not work properly with simple TObject, You would have to pass at least TPersistent descendant
  • Otherwise, you would not use the function but create TXMLSerializer instance, use SaveObject/LoadObject and supply aNodename

stoxx · Feb 14, 2009 at 08:49

Very good component !
But there is a small error in your component in procedure “RemoveExistingInfo”.
You mean:

procedure RemoveExistingInfo(aObjectName: string; aRootNode: IXMLNode);
  // remove class definition from existing document
  var
    lNode: IXMLNode;
  begin
    if aResetXML then
    begin
      aRootNode.ChildNodes.Clear;
    end
    else
    begin
      if aObjectName'' then
      begin

        lNode := Self.FindNode(aObjectName);
//        lNode := aRootNode.ChildNodes.FindNode(aObjectName);

        if Assigned(lNode) and (lNode.Attributes['classname'] = aInstance.ClassName) then
          aRootNode.ChildNodes.Remove(lNode);
      end;
    end;
  end;

stoxx · Feb 14, 2009 at 19:43

Hello,
the component has one failure.

when you save a TStringgrid

var sg : TStringgrid;
begin
 SaveToXML(sg);
 sg.defaultrowheight := 30;
 LoadFromXML(sg);
end;

the stringgrid do not restore ..

Serguei Dosyukov · Feb 15, 2009 at 12:44

Stoxx,
First issue raised was a bug and is addressed.
About the second issue, this is by design, since default values were not stored for the component. I have extended StorageOptions to include a new parameter – soStoreDefaultPropValues – which indicates that default values should be stored as well.
I will be uploading a new version tonight.

Thank you

stoxx · Feb 17, 2009 at 08:25

The parameter – soStoreDefaultPropValues – is very good.

First issue: I have added this function to your code now.

function TXMLSerializer.FindNodeByName(const aName: string; aRootNode: IXMLNode): IXMLNode;
  function lfFindNode(aRootNode: IXMLNode): IXMLNode;
  var
    i: Integer;
    lNode: IXMLNode;
  begin
    for i := 0 to aRootNode.ChildNodes.Count - 1 do
    begin
      lNode := aRootNode.ChildNodes[i];
      if (lNode.NodeName = 'class') and (lNode.Attributes['name']=aName) then
      begin
        Result := lNode;
        Break;
      end;
      Result := lfFindNode(lNode);
      if Assigned(Result) then
        Break;
    end;
  end;
begin
  Result := lfFindNode(aRootNode);
end;

Change in RemoveExistingInfo:

lNode := Self.FindNodeByName(aObjectName, aRootNode);

Are you interested to work together? I have developed some new features for your component..

Serguei Dosyukov · Feb 23, 2009 at 01:25

Stoxx,
thank you for your suggestions.
I have got a few minutes to spare and added a simple support for default properties.
Sorry, I have changed originally proposed set value to soResetDefaultValues since it is now a singleton operation performed in the beginning of the deserialization process.
Updated code is available for download from original location.
Some other code changes are introduced.

Logic described:
When enabled, it will force a component to reset to default values for Integer, Char, Set and Enumeration properties.
There is still a possibility of explicit “stored” logic being used for other types and/or default being set by other means. In this case we would be forced to come up with other way to reset those values in the code before deserialization is invoked.

And, off course, I am open to any further improvements and suggestions for the component.

stoxx · Feb 25, 2009 at 10:19

Thank you for updating the  component!

Is it possible not restore all default values, but only  the not saved values?
In your case there were bad optical effects, because of two restoration.

Serguei Dosyukov · Feb 25, 2009 at 10:27

Yes, it is possible. It will require a two parse loops though. I’ll see how to make it less intense.
On another hand many controls has BeginUpdate/EndUpdate methods which allow prevent unnecessary redraw until operation is completed. It may be useful here.

Leave a Reply