Streaming non-published TPersistent Properties – A Better Way

In my last blog post, I discussed how to stream non-published properties, most importantly how to stream non-published TPersistent descendant properties.  The solution I discussed (using a TComponent intermediary so that you can call Writer.WriteComponent) is what you see most commonly as the solution on the web.  It works for the most part, but, in my opinion, there are several problems with it:

  • Does not work with the ObjectBinaryToText/ObjectTextToBinary methods
  • Inserts a fake path into the property name stream that can cause problems in some edge cases (for example, see the Inner.Address property in the output below)
  • Frankly, the resulting stream is ugly 🙂
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

When I started trying to stream SVG documents into Delphi binary streams for the latest version of the RiverSoftAVG SVG Component Library, I ran hard into the brick wall of those edge cases I just mentioned.  🙁  Fortunately, there is a better way.  After a couple of days, I came up with this method, which I am really happy about.  I could kick myself though for using the old way for over a decade; I was never totally satisfied with it ( and intuitively, I knew it would cause me problems someday) but better late than never. 🙂

With a little spelunking through the TWriter/TReader code, you can see that the Delphi classes almost give us what you need.  There is a TWriter.WriteProperties public method.  There isn’t a TReader.ReadProperties equivalent.  There is a TReader.ReadProperty protected method.  There isn’t a TWriter public method for resetting the private FPropPath field to an empty string (we need this to make the property path name local to our TPersistent object).

However, with a judicious hack of the Delphi streaming system, and accessing some protected methods, we can create a nice, clean streaming system for non-published TPersistent objects.

The trick comes down to making the Delphi streaming system believe we are writing a collection.  When we write a collection to a stream, the TWriter class resets its FPropPath variable and allows us to write out “collection items” the way we want.

So what we end up with in our stream is we write out the vaCollection value type (using the protected TWriter.WriteValue method), and then write out our TPersistent properties inside list begin and end markers.  We need to crack the TReader to get access to the ReadProperty protected method and the TWriter to get access to the WriteValue protected method.  Here is what our new streaming code looks like:

type
 TReaderCrack = class(TReader);
 TWriterCrack = class(TWriter);

function ReadPersistent(Reader: TReader; Instance: TPersistent): TPersistent;
begin
 result := Instance;
 // read vaCollection
 Reader.CheckValue(vaCollection);
 // read list of items, in this case, only one, the top-level instance
 Reader.ReadListBegin;
 while not Reader.EndOfList do
 TReaderCrack(Reader).ReadProperty(Instance);
 Reader.ReadListEnd;
 Reader.ReadListEnd;
end;

procedure WritePersistent(Writer: TWriter; Instance: TPersistent);
begin
 // write out the Instance properties as a collection. This forces the writer
 // to zero out the property path
 TWriterCrack(Writer).WriteValue(vaCollection);
 // collection is a list of items
 // we are writing out a collection of one item, the Comparable object
 Writer.WriteListBegin;
 Writer.WriteProperties(Instance);
 Writer.WriteListEnd;
 Writer.WriteListEnd; // match vaCollection
end;

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;

The same trick can also be used to write out our non-published PurchaseHistory object list.

procedure THouse.ReadPurchaseHistory(Reader: TReader);
var
 Transaction: TTransaction;
begin
 PurchaseHistory.Clear;
 // read vaCollection
 Reader.CheckValue(vaCollection);
 while not Reader.EndOfList do
 begin
 Transaction := TTransaction.Create;
 PurchaseHistory.Add(Transaction);
 Reader.ReadListBegin;
 while not Reader.EndOfList do
 TReaderCrack(Reader).ReadProperty(Transaction);
 Reader.ReadListEnd;
 end;
 Reader.ReadListEnd;
end;

procedure THouse.WritePurchaseHistory(Writer: TWriter);
var
 i: Integer;
begin
 TWriterCrack(Writer).WriteValue(vaCollection);
 for i := 0 to PurchaseHistory.Count - 1 do
 begin
 Writer.WriteListBegin;
 Writer.WriteProperties(PurchaseHistory[i]);
 Writer.WriteListEnd;
 end;
 Writer.WriteListEnd; // match vaCollection
end;

Our final stream works with ObjectBinaryToText and ObjectTextToBinary.  The output of our stream is clean, and everything looks like normal, published properties:

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>
 PurchaseHistory = <
 item
  Buyer = 'Baker Building Co'
  Seller = 'Anderson'
  Price = 45000.000000000000000000
  Date = 28260.000000000000000000
 end
 item
  Buyer = 'Anderson'
  Seller = 'Smith'
  Price = 60000.000000000000000000
  Date = 32654.000000000000000000
 end
 item
  Buyer = 'Smith'
  Seller = 'Johnson'
  Price = 90000.000000000000000000
  Date = 38629.000000000000000000
 end>
end

Writing out a TList in this case is easy as every item in the list is a TTransaction.  For heterogeneous lists (i.e., lists that contain different class types), it is still possible with just a little extra work.  We would write out the item class name first.  On reading the stream, you would read the class name, find the class, create it, and then read its properties.  However, I will leave this as an exercise for the reader or perhaps as a future blog post. 🙂

I hope you found this useful.  I know I did.  The full code, including a test program, is available to download.

Happy Holidays and Merry CodeSmithing! 🙂

Leave a Reply

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