How to design a single interface to save different Collections in Delphi? - delphi

I have multiple collections like:
TFooList = TObjectDictionary<string,TFoo>;
TBarList = TObjectDictionary<string,TBar>;
....
TRoot = class
value : string
end;
TFoo = class(TRoot)
...
end;
TBar = class(TRoot)
...
end;
And I have an interface/class that could save or load collections:
ISave = interface
procedure Save( TDictionary<string, string> );
function Load: TDictionary<string, string>;
end;
Note that the interface expects a key/string pair collection in order to work properly.
I implemented some ISave classes in order to load/save the collection to/from file or databases:
TDbSave = class( TInterfacedObject , ISave )
....
end;
iSave := TDbSave( ConnString )
TFileSave = class( TInterfacedObject , ISave )
....
end;
iSave := TFileSave( fileName );
So, the last piece would be inherit from each collection and create the save/load methods to "translate" each collection into/from TDictionary(string, string)
TFooListSavable = TFooList;
procedure Create( save_load : ISave );
procedure Save;
procedure Load;
....
end;
procedure TFooListSavable.Save
// 1. create a TDictionary<string, string>
// 2. load the dictionary above with my collection translating
// each Foo object into a string
// 3. call save_load.Save( dictionary );
end;
procedure TFooListSavable.Load
// 1. create a TDictionary<string, string>
// 2. call save_load.load to load it
// 3. Move over the collection and translate string into TFoo and
// 4. AddOrEquals each TFoo created into TFooListSavable.
end;
So, I have two problems with this approach:
1) The interface that save or load expects a string value from the Collection and, although all the objects in each collection inherit from a class that has this string defined, I don't know how to transform a collection like TDictionary<string,TFoo> into a TDictionary<string,string> without resorting to the code above (which will duplicate the collection in order to pass it to iSave object).
2) I feel that, although I can replace iSave objects changing the way the collections would be saved/loaded without changing the collections themselves, I don't know if it is the best approach to save/load collections that keep related objects.

I think you are going about this the wrong way.
ISave should not have any concept of any TDictionary at all. It should just expose methods for reading/writing basic data types (integers, strings, etc). Let TFooListSavable and TBarListSavable decide how to serialize their respective TDictionary data however they want, calling the ISave methods as needed.
Even better would be if TFooListSavable and TBarListSavable pass ISave to each individual TFoo/TBar and let them serialize their own data members directly.
For example, something like this:
type
ISerialize = interface
function HasData: Boolean;
procedure StartWriteCollection;
procedure StartWriteItem;
procedure FinishWriteCollection;
procedure FInishWriteItem;
procedure WriteBoolean(value: Boolean);
procedure WriteInteger(value: Integer);
procedure WriteString(const value: String);
...
procedure StartReadCollection;
procedure StartReadItem;
procedure FinishReadCollection;
procedure FinishReadItem;
function ReadBoolean: Boolean;
function ReadInteger: Integer;
function ReadString: String;
...
end;
TRoot = class
public
value : string;
constructor Create; virtual;
procedure Save(Dest: ISerialize); virtual;
procedure Load(Src: ISerialize); virtual;
end;
TBaseList<T: TRoot, constructor> = class(TObjectDictionary<string, T>)
public
procedure Save(Dest: ISerialize);
procedure Load(Src: ISerialize);
end;
TFoo = class(TRoot)
public
myint: Integer;
...
procedure Save(Dest: ISerialize); override;
procedure Load(Src: ISerialize); override;
end;
TFooList = TBaseList<TFoo>;
TBar = class(TRoot)
mybool: Boolean;
...
procedure Save(Dest: ISerialize); override;
procedure Load(Src: ISerialize); override;
end;
TBarList = TBaseList<TBar>;
TDbSerialize = class(TInterfacedObject, ISerialize)
...
end;
TFileSerialize = class(TInterfacedObject, ISerialize)
...
end;
procedure TBaseList<T>.Save(Dest: ISerialize);
var
pair: TPair<string, T>;
begin
Dest.StartWriteCollection;
for pair in Self do
begin
Dest.StartWriteItem;
Dest.WriteString(pair.Key);
TRoot(pair.Value).Save(Dest);
Dest.FinishWriteItem;
end;
Dest.FinishWriteCollection;
end;
procedure TBaseList<T>.Load(Src: ISerialize);
var
Cnt, I: Integer;
key: string;
value: T;
begin
Self.Clear;
Src.StartReadCollection;
While Src.HasData do
begin
Src.StartReadItem;
key := Src.ReadString;
value := T.Create;
try
value.Load(Src);
Self.Add(key, value);
except
value.Free;
raise;
end;
Src.FinishReadItem;
end;
Src.FinishReadCollection;
end;
procedure TRoot.Save(Dest: ISerialize);
begin
Dest.WriteString(value);
end;
procedure TRoot.Load(Src: ISerialize);
begin
value := Src.ReadString;
end;
procedure TFoo.Save(Dest: ISerialize);
begin
inherited;
Dest.WriteInteger(myint);
end;
procedure TFoo.Load(Src: ISerialize);
begin
inherited;
myint := Src.ReadInteger;
end;
procedure TBar.Save(Dest: ISerialize);
begin
inherited;
Dest.WriteBoolean(mybool);
end;
procedure TBar.Load(Src: ISerialize);
begin
inherited;
mybool := Src.ReadBoolean;
end;

Related

How replace multi event with observer (Spring4D)

I have class with 2 events: OnConnect and OnDisconnect:
type
TEvent = reference to procedure;
TConnection = class
private
fOnConnect: TEvent;
fOnDisconnect: TEvent;
public
procedure SomeBehavior(aChoice: Boolean);
property OnConnect: TEvent read fOnConnect write fOnConnect;
property OnDisconnect: TEvent read fOnDisconnect write fOnDisconnect;
end;
implementation
{ TConnection }
procedure TConnection.SomeBehavior(aChoice: Boolean);
begin
if aChoice then
fOnConnect
else
fOnDisconnect;
//im not cheacking Assign(Events) to make example simple
end;
now I would like to do same thing but in more object style.
I mean use interfaces and observer pattern from String4D. And i made this:
interface
uses
Spring.DesignPatterns;
type
IObserver = interface
procedure ReactToConnect(aText: String);
procedure ReactToDisconnect(aTimeoutInMs: Integer);
end;
IConnection<T> = interface(IObservable<IObserver>)
procedure SomeBehavior(aChoice: Boolean);
end;
implementation
uses
System.SysUtils;
type
TConnection = class(TObservable<IObserver>, IConnection<IObserver>)
public
procedure SomeBehavior(aChoice: Boolean);
end;
{ TConnection }
procedure TConnection.SomeBehavior(aChoice: Boolean);
var
procOnConnect: TProc<IObserver>;
procOnDisconnect: TProc<IObserver>; // what if i want no parameters?
someText: String;
someNumber: Integer;
begin
someText := RandomText;
procOnConnect := procedure(aObserver: IObserver)
begin
aObserver.ReactToConnect(someText);
end;
someNumber := RandomInt;
procOnDisconnect := procedure(aObserver: IObserver)
begin
aObserver.ReactToDisconnect(someNumber);
end;
if aChoice then
Self.NotifyListeners(procOnConnect)
else
Self.NotifyListeners(procOnDisconnect);
end;
im doing it fisrt time and just want to ask if its proper way? or im doing somethink heretical here?

Notify the TObjectList when Object changed

Can the object of (TObjectList) know when some values of (TMyObject) was changed?
Some example:
TMyObject = class
oName: string;
end;
TMyObjectList = class(TObjectList<TMyObject>)
end;
procedure Form1.Button1.Click(Sender: TObject);
var
Obj: TMyObject;
List: TMyObjectList;
Begin
List:= TMyObjectList.Create;
Obj:= TMyObject.Create;
List.Add(Obj);
List[0].oName:= 'Test'; // here a want to know from var (List) when this object (Obj or List[0]) changed his value..
end;
Thanks for any help.
I just added the TObservableList<T> type to Spring4D (feature/observablelist branch). It is mostly modeled after .NET and uses the INotifyPropertyChanged interface to attach its event handler to any objects that support it. This class has been part of DSharp for quite some time and is used in production. It might change a bit in the future and become full part of the library.
Here is a small example how to use it so you get an idea:
program Project60;
{$APPTYPE CONSOLE}
uses
Spring,
Spring.Collections,
SysUtils;
type
TNotifyPropertyChangedBase = class(TInterfaceBase, INotifyPropertyChanged)
private
fOnPropertyChanged: Event<TPropertyChangedEvent>;
function GetOnPropertyChanged: IPropertyChangedEvent;
protected
procedure PropertyChanged(const propertyName: string);
end;
TMyObject = class(TNotifyPropertyChangedBase)
private
fName: string;
procedure SetName(const Value: string);
public
property Name: string read fName write SetName;
end;
TMain = class
procedure ListChanged(Sender: TObject; const item: TMyObject;
action: TCollectionChangedAction);
end;
{ TNotifyPropertyChangedBase }
function TNotifyPropertyChangedBase.GetOnPropertyChanged: IPropertyChangedEvent;
begin
Result := fOnPropertyChanged;
end;
procedure TNotifyPropertyChangedBase.PropertyChanged(
const propertyName: string);
begin
fOnPropertyChanged.Invoke(Self,
TPropertyChangedEventArgs.Create(propertyName) as IPropertyChangedEventArgs);
end;
{ TMyObject }
procedure TMyObject.SetName(const Value: string);
begin
fName := Value;
PropertyChanged('Name');
end;
{ TMain }
procedure TMain.ListChanged(Sender: TObject; const item: TMyObject;
action: TCollectionChangedAction);
begin
case action of
caAdded: Writeln('item added ', item.Name);
caRemoved, caExtracted: Writeln('item removed ', item.Name);
caChanged: Writeln('item changed ', item.Name);
end;
end;
var
main: TMain;
list: IList<TMyObject>;
o: TMyObject;
begin
list := TCollections.CreateObservableList<TMyObject>;
list.OnChanged.Add(main.ListChanged);
o := TMyObject.Create;
o.Name := 'o1';
list.Add(o);
o := TMyObject.Create;
o.Name := 'o2';
list.Add(o);
list[1].Name := 'o3';
Readln;
end.
There is nothing built in that can do what you ask. You will need to implement a notification mechanism yourself. This is the classic scenario for the Observer Pattern.
There are many implementations of this pattern already in existence. One obvious choice would be to use the implementation in Spring4D. Nick Hodges recent book, More Coding in Delphi, includes a chapter on this pattern which I would recommend.
Found the way, how to call method of TObjectList from TMyObject. Using TNotifyEvent in base Object.
Example:
TMyClass = class(TObject)
private
FName: string;
FOnNameEvent: TNotifyEvent;
procedure SetName(value: string);
public
property Name: string read FName write SetName;
property OnNameEvent: TNotifyEvent read FOnNameEvent write FOnNameEvent;
end;
procedure TMyClass.SetName(value: string);
begin
FName := value;
if Assigned(FOnNameEvent) then
FOnNameEvent(Self);
end;
procedure MyNameEvent(Sender: TObject);
var
i: Integer;
begin
for i := 0 to MyListOfMyClassObjects.Count -1 do
if Sender = MyListOfMyClassObjects.Item[i] then
begin
MessageBox(0, PChar(TMyClass(MyListOfMyClassObjects.Item[i]).Name), nil, MB_OK);
break;
end;
end;
procedure MyProc;
var
MyObject: TMyClass;
begin
MyObject := TMyClass.Create;
MyObject.OnNameEvent := MyNameEvent;
MyListOfMyClassObjects.Add(MyObject);
end;

Appropriate object creation - finding universal solution

There are 3 classes (there may be much more), which have the same procedure (procedure Populate). They are nearly identical and differs only by object creation. All I want is to write a universal procedure in the base class, which will replace this notorious repeating of code forever. I am not really sure, if I can express exactly what I am up to, but look at the code below and see.
TGrandFather = class(TObject)
end;
TFather = class(TGrandFather)
end;
TSon = class(TFather)
end;
TGrandson.... and so on...
TGrandFathers = class (TList)
public
procedure Populate(Amount:Integer);
end;
TFathers = class (TGrandFathers)
public
procedure Populate(Amount:Integer);
end;
TSons = class (TFathers)
public
procedure Populate(Amount:Integer);
end;
TGrandsons....
...
procedure TGrandFathers.Populate(Amount:Integer);
var i:integer;
xGrandFather:TGrandFather;
begin
for i := 0 to Amount do
begin
xGrandFather:=TGrandFather.Create;
Add(xGrandFather);
end;
end;
procedure TFathers.Populate(Amount:Integer);
var i:integer;
xFather:TFather;
begin
for i := 0 to Amount do
begin
xFather:=TFather.Create; //this is the point, which makes trouble
Add(xFather);
end;
end;
procedure TSons.Populate(Amount:Integer);
var i:integer;
xSon:TSon;
begin
for i := 0 to Amount do
begin
xSon:=TSon.Create; //this is the point, which makes trouble
Add(xSon);
end;
end;
procedure Grandsons...
Thanx...
To answer your question, you could use a metaclass through "class of" if you want to go the route you are going. This block of code demonstrates how you would accomplish that. The hierarchy needs to be cleaned up but you should get the gist of what is going on through this code.
A metaclass is a class whose instances are classes. This allows you to build a more generic framework because you can then use your metaclass to create the classes that you need.
type
TGrandFather = class(TObject)
end;
TStrangeHeirarchyClass = class of TGrandFather;
TFather = class(TGrandFather)
end;
TSon = class(TFather)
end;
TGrandFathers = class(TList)
protected
procedure PopulateInternal(aAmount:Integer; aContainedClass:
TStrangeHeirarchyClass);
public
procedure Populate(Amount:Integer);
end;
TFathers = class (TGrandFathers)
public
procedure Populate(Amount:Integer);
end;
TSons = class (TFathers)
public
procedure Populate(Amount:Integer);
end;
implementation
procedure TGrandFathers.Populate(Amount:Integer);
begin
PopulateInternal(Amount, TGrandFather);
end;
procedure TGrandFathers.PopulateInternal(aAmount:Integer; aContainedClass:
TStrangeHeirarchyClass);
var
i:integer;
xFamilyMember:TGrandFather;
begin
for i := 0 to aAmount do
begin
xFamilyMember := aContainedClass.Create;
Add(xFamilyMember);
end;
end;
procedure TFathers.Populate(Amount:Integer);
begin
PopulateInternal(Amount, TFather);
end;
procedure TSons.Populate(Amount:Integer);
begin
PopulateInternal(Amount, TSon);
end;
The way it works is that the metaclass TStrangeHeirarchyClass, which you can use just like a regular data type, stores the underlying class that you would like to work with. You can pass the type in as a parameter (like I did in the code example above) or store it in the class as a property or field like this:
TGrandFathers = class(TList)
private
FContainedClass: TStrangeHeirarchyClass;
public
procedure Populate(Amount:Integer);
property ContainedClass: TStrangeHeirarchyClass read
FContainedClass write FContainedClass;
end;
Once you have set this property you would then be able to use it to create instances of the class type that it was set to. So, setting the ContainedClass as a TFather would result in calls to ContainedClass.Create creating instances of TFather.
As David indicated in the comments, you will run into problems if you use a metaclass and override the default constructor. Your code in the constructor will never run. You either need to wither use virtual constructors or override the existing AfterConstruction method which is a virtual method that is called by the constructor. Something like this would be an example if you were using AfterConstruction:
TGrandFathers = class(TList)
protected
FContainedClass: TStrangeHeirarchyClass;
public
procedure AfterConstruction; override;
procedure Populate(Amount:Integer);
end;
TFathers = class (TGrandFathers)
public
procedure AfterConstruction; override;
end;
TSons = class (TFathers)
public
procedure AfterConstruction; override;
end;
implementation
procedure TGrandFathers.AfterConstruction;
begin
inherited;
FContainedClass := TGrandFather;
// Other construction code
end;
procedure TGrandFathers.Populate(aAmount:Integer);
var
i:integer;
xFamilyMember:TGrandFather;
begin
for i := 0 to aAmount do
begin
xFamilyMember := FContainedClass.Create;
Add(xFamilyMember);
end;
end;
procedure TFathers.AfterConstruction;
begin
inherited;
FContainedClass := TFather;
// Other construction code
end;
procedure TSons.AfterConstruction;
begin
inherited;
FContainedClass := TSon;
// Other construction code
end;
Your hierarchy looks very strange though. I think something like this would be more appropriate:
type
TRelationType = (ptSon, ptFather, ptGrandfather);
TPerson = class;
TRelation = class(TObject)
strict private
FRelationship: TRelationType;
FRelation: TPerson;
public
property Relation: TPerson read FRelation write FRelation;
property Relationship: TRelationType read FRelationship write FRelationship;
end;
TRelationList = class(TList)
//...
end;
TPerson = class(TObject)
strict private
FPersonName: string;
FRelations: TRelationList;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
property PersonName: string read FPersonName write FPersonName;
property Relations: TRelationList read FRelations;
end;
implementation
procedure TPerson.AfterConstruction;
begin
inherited;
FRelations := TRelationList.Create;
end;
procedure TPerson.BeforeDestruction;
begin
FRelations.Free;
inherited;
end;
This seems to work:
//MMWIN:CLASSCOPY
unit _MM_Copy_Buffer_;
interface
implementation
type
TBaseSelfCreating = class(TObject)
procedure Populate(Amount: Integer);
procedure Add(Obj: TObject);
end;
{TBaseSelfCreating}
procedure TBaseSelfCreating.Add(Obj: TObject);
begin
Assert(Obj is TBaseSelfCreating);
Assert(Obj <> Self);
Obj.Free;
end;
procedure TBaseSelfCreating.Populate(Amount: Integer);
var
i: Integer;
begin
for i := 1 to Amount do Add(Self.ClassType.Create);
end;
end.
Simply use Self.ClassType.Create:
program Project13;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
TFoo1 = class
procedure Boo;
end;
TFoo2 = class(TFoo1)
end;
{ TFoo1 }
procedure TFoo1.Boo;
var
x: TFoo1;
begin
x := Self.ClassType.Create as TFoo1;
write(Cardinal(Self):16, Cardinal(x):16);
Writeln(x.ClassName:16);
end;
begin
try
TFoo1.Create.Boo;
TFoo2.Create.Boo;
Readln;
except
on E:Exception do
Writeln(E.Classname, ': ', E.Message);
end;
end.
If you do not want to use Generics or you are using a version of Delphi without Generics, then this is a way. Yes, I know I can use forward declaration to remove one class, but this is clearer to follow.
Interface
type
TBaseAncestor = class
end;
TBaseClass = class of TBaseAncestor;
TGrandFathers = class (TBaseAncestor)
FClassType : TBaseClass;
constructor Create (AOwner : TControl); reintroduce; virtual;
procedure Populate;
procedure Add (X : TBaseAncestor);
end;
TFathers = class (TGrandFathers)
constructor Create (AOwner : TControl); override;
end;
Implementation
{ TGrandFathers }
constructor TGrandFathers.Create(AOwner: TControl);
begin
inherited Create;
FClassType := TGrandFathers;
end;
procedure TGrandFathers.Add (X : TBaseAncestor);
begin
end;
procedure TGrandFathers.Populate;
const
Amount = 5;
var
I : integer;
x : TBaseAncestor;
begin
for I := 0 to Amount do
begin
x := FClassType.Create;
Add (x);
end;
end;
{ TFathers }
constructor TFathers.Create(AOwner: TControl);
begin
inherited;
FClassType := TFathers;
end;
Each descendant stores its class into the class variable. And Populate uses this for Creation. I have been using this before Generics came along.

Inheritance/polymorphism concept

I have two binary files that contain a similar type of data so I want to create a unified viewer (TViewer) for both files.
Some, methods are common for these two file types, some are not. So I created a base class
TShape, and the from it TCircle and TTriangle.
Pseudo code:
TShape = class(TObject)
function NoOfItems: integer; virtual; abstract;
end;
TCircle = class(TShape)
function NoOfItems: integer; override; <---- The real implementation
end;
TTriangle = class(TShape)
function NoOfItems: integer; override; <---- The real implementation
end;
TViewer = class(TStringGrid)
Container: TShape;
end;
And I use it like this:
Procedure Main;
begin
if FileType= Circle
then (Viewer.Container as TCircle).Load(FileName)
else (Viewer.Container as TTriangle).Load(FileName);
Caption:= Viewer.Container.NoOfItems; <---- it calls TShape which is abstract
end;
When I do this it works:
if Viewer.Container is TTriangle
then Caption:= (Viewer.Container as TTriangle).NoOfItems
else ...
but I want to do it directly like this:
Caption:= Viewer.Container.NoOfItems;
Obviously there is nothing wrong in using is except that I will have to use it in many many places (close to everywhere). There is a nicer way to achieve this unified viewer?
Update:
Actually, it may be also a performance problem. My file has a really big number of items (up to billions) so doing so many 'is/as' tests may actually have a real impact on speed.
You're doing it wrong.
You need to change your code so that the container is not created until you know what type it needs to be, and then create the proper type:
Procedure Main;
begin
if FileType= Circle then
Viewer.Container := TCircle.Create
else
Viewer.Container := TTriangle.Create;
Viewer.Container.Load(FileName);
Caption := IntToStr(Viewer.Container.NoOfItems); <---- it calls proper code
end;
Here's a working example of using inheritance and polymorphism for you:
program InheritancePolymorphismTest;
uses
System.SysUtils;
type
TAnimal=class
public
procedure Sit; virtual;
procedure Speak; virtual;
end;
TDog=class(TAnimal)
public
procedure Sit; override;
procedure Speak; override;
end;
TCat=class(TAnimal)
public
procedure Speak; override;
end;
TAnimalArray = array of TAnimal;
{ TCat }
procedure TCat.Speak;
begin
inherited;
WriteLn('Bah! No way cats speak when told.');
end;
{ TDog }
procedure TDog.Sit;
begin
inherited;
WriteLn('Sitting down.');
end;
procedure TDog.Speak;
begin
inherited;
Writeln('Woof! Woof!');
end;
procedure TAnimal.Sit;
begin
end;
procedure TAnimal.Speak;
begin
end;
var
Animals: TAnimalArray;
i: Integer;
Pet: TAnimal;
{ TAnimal }
const
NumAnimals = 5;
begin
SetLength(Animals, NumAnimals);
for i := 0 to High(Animals) do
begin
if Odd(i) then
Animals[i] := TDog.Create
else
Animals[i] := TCat.Create;
end;
for Pet in Animals do
begin
Pet.Speak;
Pet.Sit;
end;
Writeln('');
Readln;
end.
Real code and real output. Polymorphism still works!
So I think you have missed some important details while declaring and implementing your class hierarchy.
type
TShape = class(TObject)
function IAm: string; virtual; abstract;
end;
TCircle = class(TShape)
function IAm: string; override;
end;
TTriangle = class(TShape)
function IAm: string; override;
end;
{ TCircle }
function TCircle.IAm: string;
begin
Result := 'I am circle'
end;
{ TTriangle }
function TTriangle.IAm: string;
begin
Result := 'I am triangle'
end;
procedure TForm1.Button6Click(Sender: TObject);
var
Shape: TShape;
begin
Shape := TCircle.Create;
Memo1.Lines.Add(Shape.IAm);
Shape.Free;
Shape := TTriangle.Create;
Memo1.Lines.Add(Shape.IAm);
Shape.Free;
end;
output
I am circle
I am triangle

Serializing TObjectList<T> to stream

I'm wondering how I can perform serialization of a generic TObjectList<T> container. Basically, I want to store different objects in that list, but all objects will descend from TSerializable, which is defined as follows:
TSerializable = class abstract(TObject)
public
{ Public declarations }
procedure LoadFromStream(const S: TStream); virtual; abstract;
procedure SaveToStream(const S: TStream); virtual; abstract;
end;
Now, let's say I have these classes defined somewhere in my app:
type
TExampleClass = class(TSerializable)
private
{ Private declarations }
FIntProp: Integer;
public
{ Public declarations }
constructor Create();
procedure LoadFromStream(const S: TStream); override;
procedure SaveToStream(const S: TStream); override;
property IntProp: Integer read FIntProp write FIntProp;
end;
TAnotherExample = class(TSerializable)
private
{ Private declarations }
FStringProp: String;
public
{ Public declarations }
constructor Create();
procedure LoadFromStream(const S: TStream); override;
procedure SaveToStream(const S: TStream); override;
procedure ReverseStringProp();
property StringProp: String read FStringProp write FStringProp;
end;
I'm planning to store such objects in a list:
var
MS: TMemoryStream;
SomeList: TObjectList<TSerializable>;
begin
MS := TMemoryStream.Create();
SomeList := TObjectList<TSerializable>.Create(True);
try
SomeList.Add(TExampleClass.Create());
SomeList.Add(TAnotherClass.Create());
TExampleClass(SomeList[0]).IntProp := 1992;
TAnotherClass(SomeList[1]).StringProp := 'Some value';
// Here, a method to serialize the list...
SerializeList(SomeList, MS);
// Clear the list and reset position in the stream.
SomeList.Clear();
MS.Seek(0, soFromBeginning);
// Unserialize the list.
UnserializeList(SomeList, MS);
// Should display "Some value".
Writeln(TAnotherClass(SomeList[1]).StringProp);
finally
SomeList.Free();
MS.Free();
end;
end;
Now, how could I possibly serialize the whole list to stream and then re-create the list from that stream?
What I was thinking about was:
Iterate through the list.
Write each object's class name to the stream first.
Call SaveToStream() on that object.
But for that approach to work, I would need to create some kind of a class register, which would be some kind of a dictionary to store known classes. It sounds like a good idea, but then I would need to call some RegisterClass() method to add every new class to the dictionary, and I don't like that way too much.
Is there any other way, or should I just do it the way I proposed?
Thanks a bunch.
Thank you guys for tips. I have decided to use my own approach, which is probably not the best one, but suits the needs of my small project.
I thought that someone might be interested in such approach, so I posted it here.
Basically, what I decided on is to have a base class TSerializable:
type
TSerializable = class abstract(TObject)
public
{ Public declarations }
procedure LoadFromStream(const S: TStream); virtual; abstract;
procedure SaveToStream(const S: TStream); virtual; abstract;
end;
Every descendant class needs to implement LoadFromStream() and SaveToStream() and handle saving to stream separately. It would be probably good to write some generic methods, which would load/save all class properties automatically.
Then, I have this small class:
type
TSerializableList = class(TObjectList<TSerializable>)
public
procedure Serialize(const S: TStream);
procedure UnSerialize(const S: TStream);
end;
The code is:
{ TSerializableList }
procedure TSerializableList.Serialize(const S: TStream);
var
CurrentObj: TSerializable;
StrLen, StrSize: Integer;
ClsName: String;
begin
S.Write(Self.Count, SizeOf(Integer));
for CurrentObj in Self do
begin
ClsName := CurrentObj.QualifiedClassName();
StrLen := Length(ClsName);
StrSize := SizeOf(Char) * StrLen;
S.Write(StrLen, SizeOf(Integer));
S.Write(StrSize, SizeOf(Integer));
S.Write(ClsName[1], StrSize);
CurrentObj.SaveToStream(S);
end;
end;
procedure TSerializableList.UnSerialize(const S: TStream);
var
I, NewIdx, TotalCount, Tmp, Tmp2: Integer;
ClsName: String;
Context: TRttiContext;
RttiType: TRttiInstanceType;
begin
Context := TRttiContext.Create();
try
S.Read(TotalCount, SizeOf(Integer));
for I := 0 to TotalCount -1 do
begin
S.Read(Tmp, SizeOf(Integer));
S.Read(Tmp2, SizeOf(Integer));
SetLength(ClsName, Tmp);
S.Read(ClsName[1], Tmp2);
RttiType := (Context.FindType(ClsName) as TRttiInstanceType);
if (RttiType <> nil) then
begin
NewIdx := Self.Add(TSerializable(RttiType.MetaclassType.Create()));
Self[NewIdx].LoadFromStream(S);
end;
end;
finally
Context.Free();
end;
end;
Quick and dirty, but works for what I need.
NOTE
Since the code uses extended RTTI, it won't compile in older Delphi versions. Also, you might need to add {$STRONGLINKTYPES ON} in your DPR file or invent some other mechanism, so that linker doesn't skip your classes (David Heffernan suggest one way here)

Resources