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! 🙂