Home > Delphi, nodeJS, Object Pascal > Writing better TTreeView code

Writing better TTreeView code

December 30, 2014 Leave a comment Go to comments

RAD (rapid application development) has its bright side as well as a dark side. The bright side is that you can assemble and construct the architecture of a program very quickly using components — the dark side is that, over time, RAD programmers tend to lose sight of more direct and elegant approaches.

TTreeView component

TTreeView component

A component that is very much miss-treated by RAD is good old TTreeView. Under vanilla Delphi TTreeView is just a wrapper around the built-in Windows component of the same name, available to all languages who chose to use it. But for some reason people tend to avoid almost 50% of its functionality – opting only for adding nodes, setting image-indexes manually, and using the data property to store a pointer to a record.

While this is all fine and dandy, it does little to help you write clean, object-oriented and maintainable code. I have worked with a ton of codebases over the years, and one of the common clutters you find in all of them – is code to populate, update, search and initialize TTreeView controls.

People use lists to keep track of thumbnail images (glyphs) for TTreeView, they stuff code for that into functions, they spread everything over a gazillion units –before they top it all off with a custom undocumented record pointer in the data property.

A better way

Instead of having X number of methods spread about the main-form, with loads of event handling hooks, typecasts and the spaghetti we are all to acustomed to — why not approach TTreeView like C# does it? With a controller class and proper node classes?

NodeJS designer uses TTreeView as explained

NodeJS designer uses TTreeView as explained

“Wow, that sounds like a lot of coding” I hear you think, but it’s actually not. It’s minimalistic, easy to use, easy to expand and best of all – child’s play to maintain. If you work in a company where more than one person is in contact with your code, or odds are that a new employee will sooner or later maintain your codebase, then doing it like this is a god send. It will save you all so much time, especially when training new staff members.

So what would this look like? Well here is a small example from my upcoming NodeJS service designer:

type

  (* Base TTreeView node class *)
  TInfoNode = Class(TTreeNode)
  private
    FModel:   TNSObject;
  protected
    property  Data;
  public
    property  ModelObject:TNSObject read FModel write FModel;
  end;

  (* Folder TTreeView class *)
  TInfoNodeFolder = Class(TInfoNode)
  public
    constructor Create(AOwner: TTreeNodes); override;
  end;

  TInfoNodeType = Class of TInfoNode; 

  (* Node controller, we pertain to this and it takes
     care of the rest *)
  TInfoNodeController = Class(TObject)
  private
    FParent:    TTreeView;
    FToCreate:  TInfoNodeType;
  protected
    procedure   HandleGetNodeClass(Sender: TCustomTreeView;
                var NodeClass: TTreeNodeClass);virtual;
  public
    Property    Parent:TTreeView read FParent;

    function    AddFolderTo(aParent:TTreeNode;
                Caption:String):TInfoNodeFolder;overload;

    function    AddFolderTo(aParent:TTreeNode;Caption:String;
                aModelObject:TNSObject):TInfoNodeFolder;overload;

    Constructor Create(aList:TTreeView);virtual;
    Destructor  Destroy;Override;
  end;

As you can see, rather than stuffing a record of information into the data property, or defining an enum of various node-types that I assign to each node — I am in fact creating full custom nodes. This allows me to customize each and every node type that my treeview will display.

In my case it will display a project node, which contains X number of service nodes, which in turn contains X number of method nodes. Each node also has a reference to it’s internal object (the object the node actually represents). Rather than stuffing this in a record, like most people do, It’s simply exposed as a normal property, called “ModelObject”.

So everything that has to do with nodes, including the image-index and selected-index properties, are handled and set by the node-class itself, rather than having some large-list of image index numbers you have to keep track of.

If someone wants to alter the glyphs used for a treeview node class, they simply locate the class and alter the numbers in the constructor. Easy, efficient and straight to the point.

The code

This is where things get interesting. Did you know that TTreeView has a special event called “OnCreateNodeClass”? This is called whenever a node is being allocated. What people dont realize is that you can alter this whenever you want, and as such each node can be of a distinctly different class-type. As long as it inherits from TTreeNode, it’s all good.

So the first thing we do in our controller, is to take ownership of that event. Our own “add node” mechanics ensures that the node you want to create (which is reflected by the method name) is returned by OnCreateNodeClass. Voila, we now have an ordinary OOP approach to an otherwise procedural API.


//#############################################################################
// TInfoNodeFolder
//#############################################################################

Constructor TInfoNodeFolder.Create(AOwner: TTreeNodes);
Begin
  inherited Create(AOwner);
  ImageIndex:=0;
  SelectedIndex:=1;
end;

//#############################################################################
// TInfoNodeController
//#############################################################################

Constructor TInfoNodeController.Create(aList:TTreeView);
Begin
  inherited Create;
  FParent:=AList;
  FParent.OnCreateNodeClass:=HandleGetNodeClass;
end;

Destructor TInfoNodeController.Destroy;
Begin
  If assigned(FParent) then
  begin
    if not (csDestroying in FParent.ComponentState) then
    FParent.OnCreateNodeClass:=NIL;
  end;
  inherited;
end;    

function TInfoNodeController.AddFolderTo(aParent:TTreeNode;
         caption:String):TInfoNodeFolder;
begin
  result:=AddFolderTo(aParent,Caption,NIL);
end;    

function TInfoNodeController.AddFolderTo(aParent:TTreeNode;Caption:String;
         aModelObject:TNSObject):TInfoNodeFolder;
var
  mNode:  TTreeNode;
begin
  (* Set class to create *)
  FToCreate:=TInfoNodeFolder;

  (* Create node *)
  mNode:=FParent.Items.AddChildObject(aParent,Caption,aModelObject);

  (* Return node of type *)
  result:=TInfoNodeFolder(mNode);
end;    

procedure TInfoNodeController.HandleGetNodeClass(Sender: TCustomTreeView;
          var NodeClass: TTreeNodeClass);
Begin
  (* Default back to root-node type if none set *)
  if FToCreate=NIL then
  FToCreate:=TInfoNode;
  NodeClass:=FToCreate;
end;                

Using the controller

Naturally you have to expand the controller with as many methods you need, making sure you isolate all the property initialization there. The point is, once you have written the base-classes – extending your treeview with other types is a breeze. And should something need to be adjusted, you know exactly where to look and what to do.

You can now do funny stuff like..

  FFolder:=FController.AddFolderTo(FRootNode,'My methods',mMethodObject);

..and completely forget about glyph indexes while working on model representation. It may take 15 minutes extra to setup, but in a living, breathing product that is constantly under development and evolution, you will earn back those 15 minutes a hundred-fold.

And should your product have several TTreeView’s, it quickly makes sense to compartmentalize and de-couple things like this. The mess of dealing with 3 or 4 TTreeViews of inter-connected objects and data-elements is troublesome to make, and even worse to maintain or understand for new employees. Teaching them to go to a single unit and look for a class of a specific name saves you a lot of time.

Practical use

If you have ever coded a typical file / folder treeview, consider this: Instead of having a function which scans a folder and populate the list-node with the content — you can now isolate this functionality in the folder class instead. So the code to manage and work on the folder data is now a part of the actual folder node. Which is fundamental object orientation thinking.

And once again it means less clutter and greater maintainability.

Expanding the idea

The above code works great and it allows you to de-couple your data from it’s visual representation. But can we make it even better than this? Well it depends. If you shun generics then the above code is as good as it gets (which is more than enough if you ask me), but yes we can indeed expand the controller using <T> types.

So instead of “AddFolderTo” which is exclusively bound to the named task, we could have a more generic:

  FFolder:=FController.Add(<TInfoNodeFolder>,FRootNode,
         'My methods',mMethodObject);

The above idea takes a new parameter, where the node-type is provided first. The rest of the parameters are like before. This is the benefit of isolating all initialization of a node in it’s constructor.

The only downside to this is that you are back doing typecasts for every node (or those nodes you want to adjust after creation). This sort of kills some of the beauty and elegance of the initial solution.

Either way, using a controller class to de-couple your code like this is very helpful! And if you expand the idea to also include TListView (and indeed other components which allows custom child classes) your programming life will become much easier!

Advertisements
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: