Streaming non-published TPersistent properties

The Delphi streaming system is a marvel of design.  For most cases, streaming components to and from any stream (memory, file, string) just works.  You publish the properties you want to stream when designing your class, and the streaming system takes care of reading and writing your published properties.  Even streaming non-published properties is relatively easy.  However, there is one non-published property type that is much more difficult than it should be: object properties descended from TPersistent.

For the purposes of this blog post, we are going to define a THouse component.  This component has published properties like Address and Price as well as three unpublished properties:

  • NumOffers (simple integer property recording the number of buy offers we have on the house)
  • Comparable (an object property descended from TPersistent that holds the properties of a house that we think is comparable to the current house)
  • PurchaseHistory (a TObjectList property of TTransaction objects that record the purchase history of the house)

Here is the Delphi code for THouse, TComparable, and TTransaction:

uses
  Classes, SysUtils, Generics.Collections;

type
  TTransaction = class(TPersistent)
  private
    { private declarations }
    FDate: TDateTime;
    FPrice: Single;
    FSeller: String;
    FBuyer: String;
  protected
    { protected declarations }
  public
    { public declarations }
    procedure Assign(Source: TPersistent); override;
    constructor Create; overload; virtual;
    constructor Create( aSeller, aBuyer: String; aPrice: Single; aDate: TDate ); overload;
    function Clone: TTransaction; virtual;
  published
    { published declarations }
    property Buyer: String read FBuyer write FBuyer;
    property Seller: String read FSeller write FSeller;
    property Price: Single read FPrice write FPrice;
    property Date: TDateTime read FDate write FDate;
  end;
  TTransactions = TObjectList;

  TComparable = class(TPersistent)
  private
    { private declarations }
    FDate: TDate;
    FPrice: Single;
    FAddress: String;
  protected
    { protected declarations }
  public
    { public declarations }
    procedure Assign(Source: TPersistent); override;
    constructor Create; overload; virtual;
    constructor Create( aAddress: String; aPrice: Single; aDate: TDate ); overload;
  published
    { published declarations }
    property Address: String read FAddress write FAddress;
    property Price: Single read FPrice write FPrice;
    property Date: TDate read FDate write FDate;
  end;

  THouse = class(TComponent)
  private
    { private declarations }
    FPrice: Single;
    FDateBuilt: TDate;
    FBathrooms: Single;
    FPurchaseHistory: TTransactions;
    FComparable: TComparable;
    FNumOffers: Integer;
    FBedrooms: Integer;
    FAddress: String;
    procedure SetComparable(const Value: TComparable);
    procedure SetPurchaseHistory(const Value: TTransactions);
  protected
    { protected declarations }
  public
    { public declarations }
    procedure Assign(Source: TPersistent); override;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property NumOffers: Integer read FNumOffers write FNumOffers;
    property Comparable: TComparable read FComparable write SetComparable;
    property PurchaseHistory: TTransactions read FPurchaseHistory write SetPurchaseHistory;
  published
    { published declarations }
    property Address: String read FAddress write FAddress;
    property Price: Single read FPrice write FPrice;
    property DateBuilt: TDate read FDateBuilt write FDateBuilt;
    property Bathrooms: Single read FBathrooms write FBathrooms;
    property Bedrooms: Integer read FBedrooms write FBedrooms;
  end;

Note that for the streaming system to work, it needs to be able to find the classes on reading and create them.  Therefore, the classes must be registered:

initialization
 RegisterClasses([THouse, TComparable, TTransaction]);
end.

If the THouse component is streamed as is, only the published properties are saved:

object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
end

Streaming non-published properties

To stream non-published properties, you override the TPersistent.DefineProperty method.  From the Delphi help:

By default, writing an object to a stream writes the values of all its published properties, and reading the object in reads those values and assigns them to the properties. Objects can also specify methods that read and write data other than published properties by overriding the DefineProperties method.

This process is fairly straightforward and, in general, the Delphi streaming classes (TFiler, TReader, and TWriter) make this easy to do.  There are 3 steps to defining any property to be published:

  1. Override the protected DefineProperties method and declare your non-published property
  2. Write a WriteXXX( Writer: TWriter ) method for each non-published property and write the property to the stream using TWriter.WriteXXX methods
  3. Write a ReadXXX( Reader: TReader ) method for each non-published property and read the property from the stream using TReader.ReadXXX methods

For the first step, the TFiler class provides two methods for “declaring” your non-published property and defining the reader and writer methods to use: DefineProperty and DefineBinaryProperty.  These methods accept the name of your “fake” property and two methods (one for reading, one for writing).  Finally, there is a parameter to tell the TFiler if the property should be written out.  Use this parameter to not write out properties that equal their defaults.  So, in our example to write out the NumOffers simple integer property, the DefineProperties method might look like this:

procedure THouse.DefineProperties(Filer: TFiler);
begin
  inherited DefineProperties(Filer);
  Filer.DefineProperty('NumOffers', ReadNumOffers, WriteNumOffers, NumOffers <> 0);
end;

Make sure you call the inherited DefineProperties method so that any ancestor classes can write our their fake properties.

Coding the read and write methods is easy for simple property types, the TReader and TWriter class provide many methods for simple types, such as TWriter.WriteInteger, TWriter.WriteBoolean and TWriter.WriteString.  The ReadNumOffers and WriteNumOffers methods look like:

procedure THouse.ReadNumOffers(Reader: TReader);
begin
  FNumOffers := Reader.ReadInteger;
end;

procedure THouse.WriteNumOffers(Writer: TWriter);
begin
  Writer.WriteInteger(NumOffers);
end;

Note that the TFiler class does the work of figuring out when to call your Reader and Writer methods so you don’t have to put in any special checks in your code.  For example, when reading the stream, when the Filer object detects your fake property name in the stream, it will position the stream and then call your Reader method.

Streaming non-published TPersistent properties

The Delphi streaming system starts to fail you when you want to write out non-simple published properties.  Unfortunately, the TWriter class does not have a WritePersistent method or something similar to write out our THouse.Comparable property.  Comparably, the TReader class lacks ReadPersistent method for reading these types back in.  So what are we to do?

If you search the web, you quickly see that there are 2 recommendations:

  1. Write the properties of the TPersistent object yourself (this is the worst way to do it as it is extremely tedious (as you have to write all the properties yourself), error prone (what happens if the class deletes a property at a later date?), and not maintainable (what happens if new properties are added to the class?  They are not streamed out).
  2. Create a TComponent wrapper class that has a published TPersistent property which you set to your TPersistent class and then stream the TComponent using Writer.WriteComponent.

The second method works and something similar is what I used for many years.  The idea is that you create a TComponent descendant whose only purpose is to read and write your TPersistent descendant class.  The Delphi streaming class has TWriter.WriteComponent and TReader.ReadComponent methods, which you can then use to stream your TPersistent property.

  TOuterComponent = class(TComponent)
  { Purpose: Special class for writing out TPersistent objects.  Allows the
    use of WriteComponent.  This component frees itself when it is loaded in order
    to avoid problems with TReader.  For TWriter, you should still free it }
  private
    FInner : TPersistent;
  protected
    procedure Loaded; override;
  public
  published
    property Inner : TPersistent Read FInner Write FInner;
  end;

{ TOuterComponent }

procedure TOuterComponent.Loaded;
begin
 inherited;
 {$IFDEF AUTOREFCOUNT}DisposeOf{$ELSE}Free{$ENDIF};
end;

procedure WritePersistent(Writer: TWriter; Instance: TPersistent);
// Saves a persistent object to a TWriter object
var
 Outer: TOuterComponent;
begin
 Outer := TOuterComponent.Create(nil);
 try
 Outer.Inner := Instance;
 // Write the outer object, it automatically writes the inner object
 Writer.WriteComponent( Outer );
 finally
 Outer.Free;
 end;
end;

function ReadPersistent(Reader: TReader; aObject: TPersistent): TPersistent;
// read a persistent object from a TReader object
var
 Outer: TOuterComponent;
begin
 Outer := TOuterComponent.Create(nil);
 Outer.Inner := aObject;
 // read the outer object, it automatically reads the inner object
 Reader.ReadComponent( Outer );
 result := Outer.Inner;
end;

With the TComponent class above and the two methods, code for writing a TPersistent property looks like this:

procedure THouse.DefineProperties(Filer: TFiler);
begin
 inherited DefineProperties(Filer);
 Filer.DefineProperty('NumOffers', ReadNumOffers, WriteNumOffers, NumOffers <> 0);
 Filer.DefineProperty('Comparable', ReadComparable, WriteComparable, True);
end;

procedure THouse.ReadComparable(Reader: TReader);
begin
 ReadPersistent(Reader, FComparable);
end;

procedure THouse.WriteComparable(Writer: TWriter);
begin
 WritePersistent(Writer, FComparable);
end;

In general, this is decent method for writing object properties.  It writes out all published properties of the TPersistent-descended class (even fake properties!) and does not need to change as your TPersistent class changes.

However, this method can quickly break down.  If you use the ComponentToStringProc and StringToComponentProc methods described in the Delphi Component To String example, it fails.  It can also fail in other specialized cases such as when you start nesting these properties (i.e., List of TPersistent which has TPersistent which as a list, etc).  Finally, quite honestly, it looks ugly to me in streams.  If you write out the stream as a string, the TPersistent-descended property does not look clean like the other properties:

object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
 NumOffers = 2
 TOuterComponent = Null
 Inner.Address = '456 Lonely Way Anywhere, FL 32006'
 Inner.Price = 85000.000000000000000000
 Inner.Date = 42339.000000000000000000
end

Fortunately, there is an even better way.  However, as I seem to be constitutionally incapable of writing short blog posts, I will stop here and leave you a teaser.  In my next blog post, I will detail how to write a TPersistent-like  and TList-like properties “the better way” and which provide clean output that can be passed through the Component To String example:

object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
 NumOffers = 2
 Comparable = <
 item
 Address = '456 Lonely Way Anywhere, FL 32006'
 Price = 85000.000000000000000000
 Date = 42339.000000000000000000
 end>
end

For now, Happy Holidays and Happy CodeSmithing!

Leave a Reply

Your email address will not be published. Required fields are marked *