Class/Record Helpers – Custom Syntactic Sugar for the Delphi Class Hierarchy

I find that I really like class/record helpers.  In the right situation, they help me just make my code look nicer and more intuitive.  They allow me to bundle useful functions together and scope them to exactly where they are needed, and to crack classes (access protected methods and properties) in an official manner that doesn’t look like a hack 🙂  The code I write that calls class/record helpers ends up looking very clean and intuitive, at least to me.  In other words, class/record helpers allow me to add syntactic sugar to the Delphi class hierarchy.

FMX TTreeView Example

I don’t know about you, but I find it inexplicable some of the missing methods in the FMX class hierarchy code.  For example, it is super annoying that the TTreeView class in FMX doesn’t provide an easy way to create a TTreeViewItem and add it into the tree hierarchy.  Instead, you have to create the TTreeViewItem class, set its properties, and then add it to the tree view hierarchy, e.g.,

var
 Item: TTreeViewItem;
begin
 Item := TTreeViewItem.Create(TreeView1);
 Item.Text := aText;
 Item.Parent := Self; // or TreeView1.AddObject(Item);
end;

After years of the VCL hierarchy, I just don’t find this intuitive.  I want to write:

TreeView1.Add('My First Node');
TreeView1.AddObject('My Second Node', aObject);
TreeView1.Add('My Third Node').Add('And Child');

Class Helpers to the rescue!  By writing two class helpers, one for TCustomTreeView and one for TTreeViewItem, we can add these methods and so much more:

TTreeViewCompareFunction = reference to function (const Item: TTreeViewItem): Boolean;
TTreeViewItemHelper = class helper for TTreeViewItem
 private
 { private declarations }
 protected
 { protected declarations }
 public
 { public declarations }
 function Add( const aText: String ): TTreeViewItem; overload;
 function Add( const aText, aData: String ): TTreeViewItem; overload;
 function AddObject( const aText: String; const aData: TObject ): TTreeViewItem; overload;
 function ItemBy( const Compare: TTreeViewCompareFunction ): TTreeViewItem; overload;
 function ItemByTag( const aText: String ): TTreeViewItem; overload;
 function ItemByTag( const aData: TObject ): TTreeViewItem; overload;
end;

TTreeViewHelper = class helper for TCustomTreeView
 private
 { private declarations }
 protected
 { protected declarations }
 public
 { public declarations }
 function Add( const aText: String ): TTreeViewItem; overload;
 function Add( const aText, aData: String ): TTreeViewItem; overload;
 function AddObject( const aText: String; const aData: TObject ): TTreeViewItem; overload;
 function ItemBy( const Compare: TTreeViewCompareFunction ): TTreeViewItem; overload;
 function ItemByTag( const aText: String ): TTreeViewItem; overload;
 function ItemByTag( const aData: TObject ): TTreeViewItem; overload;
end;

[...]

{ TTreeViewHelper }

function TTreeViewHelper.Add(const aText: String): TTreeViewItem;
begin
 result := TTreeViewItem.Create(Self);
 result.Text := aText;
 result.Parent := Self;
end;

function TTreeViewHelper.Add(const aText, aData: String): TTreeViewItem;
begin
 result := TTreeViewItem.Create(Self);
 result.Text := aText;
 result.TagString := aData;
 result.Parent := Self;
end;

function TTreeViewHelper.AddObject(const aText: String;
 const aData: TObject): TTreeViewItem;
begin
 result := TTreeViewItem.Create(Self);
 result.Text := aText;
 result.TagObject := aData;
 result.Parent := Self;
end;

function TTreeViewHelper.ItemBy(const Compare: TTreeViewCompareFunction): TTreeViewItem;
var
 I: Integer;
begin
 Result := nil;
 for I := 0 to Count-1 do
 begin
  if Compare(Items[I]) then
  begin
   Result := Items[I];
   Break;
  end;
  Result := Items[I].ItemBy(Compare);
  if Result <> nil then Break;
 end;
end;

function TTreeViewHelper.ItemByTag(const aData: TObject): TTreeViewItem;
var
 I: Integer;
begin
 Result := nil;
 for I := 0 to Count-1 do
 begin
  if Items[I].TagObject = aData then
  begin
   Result := Items[I];
   Break;
  end;
  Result := Items[I].ItemByTag(aData);
  if Result <> nil then Break;
 end;
end;

function TTreeViewHelper.ItemByTag(const aText: String): TTreeViewItem;
var
 I: Integer;
begin
 Result := nil;
 for I := 0 to Count-1 do
 begin
  if Items[I].TagString = aText then
  begin
   Result := Items[I];
   Break;
  end;
  Result := Items[I].ItemByTag(aText);
  if Result <> nil then Break;
 end;
end;

{ TTreeViewItemHelper }

function TTreeViewItemHelper.Add(const aText: String): TTreeViewItem;
begin
 result := TTreeViewItem.Create(Self);
 result.Text := aText;
 result.Parent := Self;
end;

function TTreeViewItemHelper.Add(const aText, aData: String): TTreeViewItem;
begin
 result := TTreeViewItem.Create(Self);
 result.Text := aText;
 result.TagString := aData;
 result.Parent := Self;
end;

function TTreeViewItemHelper.AddObject(const aText: String;
 const aData: TObject): TTreeViewItem;
begin
 result := TTreeViewItem.Create(Self);
 result.Text := aText;
 result.TagObject := aData;
 result.Parent := Self;
end;

function TTreeViewItemHelper.ItemBy(const Compare: TTreeViewCompareFunction): TTreeViewItem;
var
 I: Integer;
begin
 Result := nil;
 for I := 0 to Count-1 do
 begin
  if Compare(Items[I]) then
  begin
   Result := Items[I];
   Break;
  end;
  Result := Items[I].ItemBy(Compare);
  if Result <> nil then Break;
 end;
end;

function TTreeViewItemHelper.ItemByTag(const aData: TObject): TTreeViewItem;
var
 I: Integer;
begin
 Result := nil;
 for I := 0 to Count-1 do
 begin
  if Items[I].TagObject = aData then
  begin
   Result := Items[I];
   Break;
  end;
  Result := Items[I].ItemByTag(aData);
  if Result <> nil then Break;
 end;
end;

function TTreeViewItemHelper.ItemByTag(const aText: String): TTreeViewItem;
var
 I: Integer;
begin
 Result := nil;
 for I := 0 to Count-1 do
 begin
  if Items[I].TagString = aText then
  begin
   Result := Items[I];
   Break;
  end;
  Result := Items[I].ItemByTag(aText);
  if Result <> nil then Break;
 end;
end;

Ahhh, much better and so much more useful!  If you noticed, I added bonus methods, including one to search your tree view by anything.  Using an anonymous method, you can easily search for tree view items that start with some text:

procedure TForm1.Edit1ChangeTracking(Sender: TObject);
begin
 TreeView1.Selected := TreeView1.ItemBy(function (const Item: TTreeViewItem): Boolean
                                        begin
                                          result := StartsText((Sender as TEdit).Text, Item.Text);
                                        end);
end;

You can download this file here.

Alternatively, when the RiverSoftAVG SVG Component Library (RSCL) is officially released at the end of this month, you will be able to find this class helper bundled with the RiverSoftAVG Common Classes Library (RCCL).  The RCCL is part of any of our products, such as the RSCL and the free RiverSoftAVG Charting Component Suite.

Streaming Non-Published TPersistent Properties Example

For another example, in a post from December, I discussed using the Delphi streaming classes, TReader and TWriter, to stream non-published TPersistent properties.  I did some judicious hacking of the TWriter class (to call the protected TWriter.WriteValue method with vaCollection) and of the TReader class (to call the protected TReader.ReadProperty method) to stream TPersistent properties.  I wrote two functions, ReadPersistent and WritePersistent, to take a TReader/TWriter and read/write a TPersistent instance to and from a stream.

By using class helpers, we can clean up this code and make its use more intuitive.  For the TWriter, we create a TWriterHelper class helper and refactor the WritePersistent function as a method:

TWriterHelper = class helper for TWriter
 private
 { private declarations }
 protected
 { protected declarations }
 public
 { public declarations }
 procedure WritePersistent( Instance: TPersistent );
end;

[...]

{ TWriterHelper }

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

Similarly, we move the ReadPersistent function and make it a method of a TReaderHelper class helper.

TReaderHelper = class helper for TReader
private
 { private declarations }
protected
 { protected declarations }
public
 { public declarations }
 procedure ReadPersistent( Instance: TPersistent );
end;

[...]

{ TReaderHelper }

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

Much cleaner.  And now it makes the calling code look better too.

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

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

The code for this example is here or is already part of the RiverSoftAVG Common Classes Library.  As a bonus, there are a couple other methods packaged with it.

That’s all for now.  I believe that class/record helpers can really help you write better and more intuitive code, and let you concentrate on writing the hard code instead… a spoonful of syntactic sugar to make the hard, medicinal, code go down, if you will 🙂

Happy CodeSmithing!

 

 

Leave a Reply

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