Wednesday, October 14, 2009

RTTI & XmlSerial.pas Refactoring - SVN Changes

XmlSerial.pas started as really cool demo of what you can do with RTTI in Delphi 2010. Most of the time demo code is an example that should be looked at learned from and thrown away.

The problem I face is that I need XmlSerial to be far more than a Demo. I need a fully functional serializer that will inter-op with classes using the .NET XmlSerializer.

After committing a couple of my refactoring changes to make it more than just a demo. I realized that I did not want to keep committing all of my changes to the SVN
Trunk.
I really want to keep committing code each day that may not even remotely work. I have some major changes in design planned that will require that for a few days.

So as such I just created a branch for my RTTI work if you want to follow that work, while I refactor to provide an elegant and far more supportable framework. When this work stabilizes I will merge the changes back to Trunk.

Here are the few things that I intend to do to the code:

For example, I intend to create an Adapter for XML Parsing to allow current use of TxmlDocument to continue, but also allow for other types of Parsing that may not be DOM based to work. Specifically I am doing this because, I am concerned with the speed and memory footprint that is required by DOM.

I also want to create a set of adapters and factory that will allow things like TList, Dynamic Arrays and TDataset all to look the same to the serialization engine, so I don't have to write custom code in multiple places. Instead I can just delegate that functionality to a class that will hide the implementation details of how the data is stored. I can see this work being used in a variety of applications outside of XML Serialization.

Do you have specific needs for XML Serialization? I would like to know as now is the time to see if I can address them.

Thursday, October 8, 2009

Xml Serialization - Control via Attributes

The XmlSerial.pas unit now supports the Attributes. I had to fix a couple of bugs to get it working, just in case you grabbed the copy I posted earlier today.

Now you can use 4 Different attributes to control XML serialization process. Specifically XmlRoot, XmlElement, XmlAttribute, and XmlIgnore. I modified the original class I used in the previous XML serialization post to use these.


[XmlRoot('Person')]
TPerson = class(TObject)
private
FLastName: String;
FBirthday: TDateTime;
FMiddleName: String;
FFirstName: String;
function GetFullName: String;
published
public
[XmlAttribute('First_Name')]
property FirstName : String read FFirstName write FFirstName;
[XmlElement('LAST_NAME')]
property LastName : String read FLastName write FLastName;
[XmlIgnore]
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;

And now the XML that it outputs and imports is:

<Person First_Name="John">
<LAST_NAME>Doe</LAST_NAME>
<Birthday>34744</Birthday>
</Person>


So basically this mimics the behavior of the same attributes in the XML .NET Serialization. Although, it does not support namespaces yet.

RTTI Article List

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

INI persistence the RTTI way

This post is based around the IniPersit.pas code that I just released.

I commonly create configuration classes to create a common and easy way to access information stored in INI, Registry, or XML. In these examples I will show how I used the new RTTI and Attributes in Delphi 2010 to provide a new way of creating a configuration class that access information stored in an INI file.

Lets first start off showing how to use the New Unit, then we can pull back the covers and show how it works.


unit ConfigSettings;
interface
uses
IniPersist;

type
TConfigSettings = class (TObject)
private
FConnectString: String;
FLogLevel: Integer;
FLogDirectory: String;
FSettingsFile: String;
public
constructor create;
// Use the IniValue attribute on any property or field
// you want to show up in the INI File.
[IniValue('Database','ConnectString','')]
property ConnectString : String read FConnectString write FConnectString;
[IniValue('Logging','Level','0')]
property LogLevel : Integer read FLogLevel write FLogLevel;
[IniValue('Logging','Directory','')]
property LogDirectory : String read FLogDirectory write FLogDirectory;

property SettingsFile : String read FSettingsFile write FSettingsFile;
procedure Save;
procedure Load;

end;

implementation
uses SysUtils;

{ TApplicationSettings }

constructor TConfigSettings.create;
begin
FSettingsFile := ExtractFilePath(ParamStr(0)) + 'settings.ini';
end;

procedure TConfigSettings.Load;
begin
// This loads the INI File Values into the properties.
TIniPersist.Load(FSettingsFile,Self);
end;

procedure TConfigSettings.Save;
begin
// This saves the properties to the INI
TIniPersist.Save(FSettingsFile,Self);
end;

end.


program Project13;

{$APPTYPE CONSOLE}

uses
SysUtils,
IniPersist,
ConfigSettings;

var
Settings : TConfigSettings;

begin
Settings := TConfigSettings.Create;
try
Settings.ConnectString := '\\127.0.0.1\DB:2032';
Settings.LogLevel := 3;
Settings.LogDirectory := 'C:\Log';
Settings.Save;
finally
Settings.Free;
end;

Settings := TConfigSettings.Create;
try
Settings.Load;
WriteLn(Settings.ConnectString);
Writeln(Settings.LogLevel);
Writeln(Settings.LogDirectory);
finally
Settings.Free;
end;
Readln;
end.

Output:

\\127.0.0.1\DB:2032
3
C:\Log

Resulting INI File:

[Database]
ConnectString=\\127.0.0.1\DB:2032
[Logging]
Level=3
Directory=C:\Log


As you can see by the above code there really is not much too it, if you want a field or a property to be stored in the INI File, you just need to add the IniValue Attribute.

TExampleClass = class (TObject)
private
FConnectString: String;
public
[IniValue('Database','ConnectString')]
property ConnectString : String read FConnectString write FConnectString;
end;

The constructor of the INIValue allows you to specify the Section, Name you the field or Property stored in. It also allows you to specify a default value if the name & section did not exist in the INI File.

IniValueAttribute = class(TCustomAttribute)
private
FName: string;
FDefaultValue: string;
FSection: string;
published
constructor Create(const aSection : String;const aName : string;const aDefaultValue : String = '');
property Section : string read FSection write FSection;
property Name : string read FName write FName;
property DefaultValue : string read FDefaultValue write FDefaultValue;
end;

...

constructor IniValueAttribute.Create(const aSection, aName, aDefaultValue: String);
begin
FSection := aSection;
FName := aName;
FDefaultValue := aDefaultValue;
end;


So the magic is really contained in TIniPersist


TIniPersist = class (TObject)
private
class procedure SetValue(aData : String;var aValue : TValue);
class function GetValue(var aValue : TValue) : String;
class function GetIniAttribute(Obj : TRttiObject) : IniValueAttribute;
public
class procedure Load(FileName : String;obj : TObject);
class procedure Save(FileName : String;obj : TObject);
end;

The load and save methods are nearly identical, so lets take a look at load.

class procedure TIniPersist.Load(FileName: String; obj: TObject);
var
ctx : TRttiContext;
objType : TRttiType;
Field : TRttiField;
Prop : TRttiProperty;
Value : TValue;
IniValue : IniValueAttribute;
Ini : TIniFile;
Data : String;
begin
ctx := TRttiContext.Create;
try
Ini := TIniFile.Create(FileName);
try
objType := ctx.GetType(Obj.ClassInfo);
for Prop in objType.GetProperties do
begin
IniValue := GetIniAttribute(Prop);
if Assigned(IniValue) then
begin
Data := Ini.ReadString(IniValue.Section,IniValue.Name,IniValue.DefaultValue);
Value := Prop.GetValue(Obj);
SetValue(Data,Value);
Prop.SetValue(Obj,Value);
end;
end;
for Field in objType.GetFields do
begin
IniValue := GetIniAttribute(Field);
if Assigned(IniValue) then
begin
Data := Ini.ReadString(IniValue.Section,IniValue.Name,IniValue.DefaultValue);
Value := Field.GetValue(Obj);
SetValue(Data,Value);
Field.SetValue(Obj,Value);
end;
end;
finally
Ini.Free;
end;
finally
ctx.Free;
end;
end;

So you can see we basically loop through all the properties and field check for an Attribute, if it exists we get the current value which sets the TypeInfo in the TValue object. Then we assign the string returned from the INI file into the TValue and call SetValue()

Lets look at the two methods called.

class procedure SetValue(aData : String;var aValue : TValue);
class function GetIniAttribute(Obj : TRttiObject) : IniValueAttribute;

Lets look first at SetValue(). You will see that it depends on the TypeInfo being present in the TValue being passed in. We check the TValue and perform the conversions required to convert the String to the Correct Type before storing it
into the TValue.

class procedure TIniPersist.SetValue(aData: String;var aValue: TValue);
var
I : Integer;
begin
case aValue.Kind of
tkWChar,
tkLString,
tkWString,
tkString,
tkChar,
tkUString : aValue := aData;
tkInteger,
tkInt64 : aValue := StrToInt(aData);
tkFloat : aValue := StrToFloat(aData);
tkEnumeration: aValue := TValue.FromOrdinal(aValue.TypeInfo,GetEnumValue(aValue.TypeInfo,aData));
tkSet: begin
i := StringToSet(aValue.TypeInfo,aData);
TValue.Make(@i, aValue.TypeInfo, aValue);
end;
else raise EIniPersist.Create('Type not Supported');
end;
end;

Now lets take a look at GetIniAttribute() the goal of this method is to check to see if a given TRttimember (Field or Property) has the IniValue attribute, and if it does return it, otherwise return NIL.

class function TIniPersist.GetIniAttribute(Obj: TRttiObject): IniValueAttribute;
var
Attr: TCustomAttribute;
begin
for Attr in Obj.GetAttributes do
begin
if Attr is IniValueAttribute then
begin
exit(IniValueAttribute(Attr)); // Exit with a parameter new in Delphi 2010
end;
end;
result := nil;
end;

So all in all, its really not much code, and it make usage simple. Now granted TIniValue is not all that complex, but this situation could be applied to a variety of other applications.

RTTI Article List

RTTI - Practical Examples

Well it took a lot longer than I wanted to get this code out.

There were many more rough edges than I wanted, but after some gentle and firm requests I realized I needed to get this out before the rough edges are finished.

Specifically the XmlSerial.pas has a road map in source code detailing what I still need to get done.

What is released:

  • IniPersist.pas Allows easy mapping of properties and fields to an INI File.

  • XmlSerial.pas Object and Record Serialization and De-serialization to XML

  • ObjDs.pas Read-only mapping of Objects to TClientDataSets.

  • RttiUtils.pas Things to help with common RTTI needs.



How to get the code:
Follow the above links to each unit, or just use SVN.

I am now working on some blog posts to show the how to use this code.