Serialization of a TCollection which is not declared in a TComponent?

1k Views Asked by At

Is it possible to serialize a TCollection which is not encapsulated in a TComponent ?

For example, I have a custom TCollection. I can't use TMemoryStream.WriteComponent() on my TCollection descendant. It'll only works if I encapsulate the collection in a TComponent and then if I write this component.

Technically there is no problem but declaring a TComponent which only owns a TCollection seems a bit odd.

TMyCustomCollection = Class(TCollection) // not serializable ?
  //...
End;

TMyCustomCollectionCapsule = Class(TComponent) // serializable !
Private
  FMyCusColl: TMyCustomCollection;
  Procedure SetMyCusColl(Const Data: TMyCustomCollection);
Published
  Property CanBeSerialized: TMyCustomCollection Read FMyCusColl Write SetMyCusColl
End;

Maybe I just miss a feature of the Delphi RTL? Can a TPersistent descendent be streamed without being itself encapsulated in a TComponent ?

2

There are 2 best solutions below

0
On

You can serialize a TCollection not encapsuled within a TComponent by means of another TComponent descendant defined as follows:

type
  TCollectionSerializer = class(TComponent)
  protected
    FCollectionData: string;
    procedure DefineProperties(Filer: TFiler); override;
  public
    procedure WriteData(Stream: TStream);
    procedure ReadData(Stream: TStream);
    //
    procedure LoadFromCollection(ACollection: TCollection);
    procedure SaveToCollection(ACollection: TCollection);
  end;

DefineProperties, WriteData and ReadData implementation details:

procedure TCollectionSerializer.WriteData(Stream: TStream);
var
  StrStream: TStringStream;
begin
  StrStream:=TStringStream.Create;
  try
    StrStream.WriteString(FCollectionData);
    Stream.CopyFrom(StrStream,0);
  finally
    StrStream.Free;
  end;
end;

procedure TCollectionSerializer.ReadData(Stream: TStream);
var
  StrStream: TStringStream;
begin
  StrStream:=TStringStream.Create;
  try
    StrStream.CopyFrom(Stream,0);
    FCollectionData:=StrStream.DataString;
  finally
    StrStream.Free;
  end;
end;

procedure TCollectionSerializer.DefineProperties(Filer: TFiler);
begin
  inherited;
  //
  Filer.DefineBinaryProperty('CollectionData', ReadData, WriteData,True);
end;

Templates of LoadFromCollection and SaveToCollection:

procedure TCollectionSerializer.LoadFromCollection(ACollection: TCollection);
var
  CollectionStream: TStream;
begin
  CollectionStream:= TCollectionStream.Create(ACollection);
  try
    ReadData(CollectionStream)
  finally
    CollectionStream.Free;
  end;
end;

procedure TCollectionSerializer.SaveToCollection(ACollection: TCollection);
var
  CollectionStream: TStream;
begin
  CollectionStream:= TCollectionStream.Create(ACollection);
  try
    WriteData(CollectionStream);
  finally
    CollectionStream.Free;
  end;
end;

About TCollectionStream:

It should be a TStream descendant having a rich creator with a TCollection as a parameter and designed to behave like a TFileStream. You must implement it. Disclaimer: I have never tested that but I can tell that a TFileStream works (for streaming external file).

Conclusion:

This component is inspired by the VCL way to serialize within a DFM an external file under Delphi XE (RCData). It must be registred along with a component editor (that you must also implement based on TComponentEditor) doing the serialization at designtime.

3
On

It only works if you put your collection in a TComponent, because TMemoryStream.WriteComponent (the name itself is a clue!) takes a TComponent as a parameter:

procedure WriteComponent(Instance: TComponent);

and TCollection is as you already discovered not a TComponent descendant. It may seem odd to have a TComponent descendant just to hold your TCollection descendant, but if you want to stream it using the WriteComponent facilities of streams, I don't see any other easy way to do it.


If you want to do this using "just" the RTL/VCL (ie not using a third party library), you would have to write a T(Memory)Stream descendant and add a WritePersistent implementation that takes an Instance: TPersistent parameter.

I haven't delpheddelved into the TStream classes that much, but my guess is that you w/should be able to borrow a lot from the TComponent support. Certainly the class inheritance support.

Having had a cursory look, it seems simple at first as WriteComponent just calls WriteDescendent which instantiates a TWriter and then calls the WriteDescendent method of that writer. And the TWriter already contains methods to write a collection.

However, if you "just" want to stream TPersistent descendants, you will have to do a lot of work in TWriter/TReader as well as they are completely based around TComponent. And it won't be a simple case of just writing a couple of descendant. For one, they TWriter/TReader are not really set up to be derived from. For another: TStream (descendants) instantiate TWriter and TReader directly and these classes do not have virtual constructors. Which makes writing descendants for them fairly futile unless you would like to try your hand at hooking, patching the VMT and more of that interesting stuff.

All in all: the easiest way to stream your custom collection remains to "just" wrap it in a TComponent and live with the "waste" of that.