Monday, June 13, 2011

The pains of presentation

"It takes one hour of preparation for each minute of presentation time." - Wayne Burgraff  

Did you ever show your data in a virtual treeview? If so, how did you do it? Most likely (at least that's the way I have seen often) you implemented a couple of events for your treeview. Did you have a flat list of objects or did you have some hierarchical structure of data? Most likely this was not the only place you used the treeview to present your data. But you were smart and you did not have the same code for OnInitNode, OnGetText, DoChange or other events all the time, right?

The virtual treeview is one of the most powerful visual components when it comes to presenting data in different ways. And one of the most abused ones when in comes to design and coding principles - at least in my opinion. Some of them are DRY (reimplementing the same events every time), Single responsibility principle and Separation of Concerns (using the treeview as storage and controlling business logic), Dependency inversion principle (gui and business logic are tightly coupled).

So how can we solve these problems? Let's analyze what we got and what we need in common situations. We have a list or hierarchical structure of data and we know what parts of data to show in which column of the tree. So what we need is kind of a presenter. Let me show you what you can do with the DSharp TTreeViewPresenter.

"I am careful with my material and presentation." - Shelley Berman 

The treeview presenter is a component that you can drop on your form and plug the virtual treeview to it. Then you need to set the ItemsSource property of the presenter. You can attach any kind of list that is based on TList<TObject> - due to the lack of covariance in delphi you need a hardcast here. Then you specify the ColumnDefinitions either by using the designtime support or writing some special classes - more on that later.

Ok, we got our treeview, the presenter, a list of customer objects and the column definitions to show firstname and lastname. When we run the application it shows our data in the tree and we can even click on the columns to sort the data.

How does it work? Basically the presenter attaches to the events of the treeview so make sure you don't use those otherwise (well, actually you don't want to since you are using the presenter, don't you?). So this is how the presenter knows about what's going on in the tree. I mentioned writing special classes earlies. The presenter is using the IDataTemplate interface to get information about the items and the structure to display them. For now we just need to know about this function:

function GetText(const Item: TObject; const ColumnIndex: Integer): string;

If you want to write some simple data template and not using the designtime support that's the function you want to override when inheriting from TDataTemplate (the base class for all data templates).

Presenting lists of the same kind of objects is pretty much a no-brainer, isn't it? But what about presenting hierarchical data? Very straight forward as well, let's see.

"The audience only pays attention as long as you know where you are going." - Philip Crosby 

Assuming we want to show folders and files in the tree we need two different data templates one for each class we want to show (actually one for each class that is different, if you have objects inheriting from a base class and you only need data from that base class you only need one template) and register them on the existing data template. Data templates have built-in functionality to recursively look for the best fitting data template. The code for our two templates would look like this:

type
  TFolderTemplate = class(TDataTemplate)
  public
    function GetItem(const Item: TObject; const Index: Integer): TObject; override;
    function GetItemCount(const Item: TObject): Integer; override;
    function GetText(const Item: TObject; const ColumnIndex: Integer): string; override;
    function GetTemplateDataClass: TClass; override;
  end;

  TFileTemplate = class(TDataTemplate)
  public
    function GetText(const Item: TObject; const ColumnIndex: Integer): string; override;
    function GetTemplateDataClass: TClass; override;
  end;

implementation

{ TFolderTemplate }

function TFolderTemplate.GetItem(const Item: TObject;
  const Index: Integer): TObject;
begin
  Result := TFolder(Item).Files[Index];
end;

function TFolderTemplate.GetItemCount(const Item: TObject): Integer;
begin
  Result := TFolder(Item).Files.Count; // containing subfolders in that list as well
end;

function TFolderTemplate.GetTemplateDataClass: TClass;
begin
  Result := TFolder;
end;

function TFolderTemplate.GetText(const Item: TObject;
  const ColumnIndex: Integer): string;
begin
  case ColumnIndex of
    0: Result := TFolder(Item).Name;
  end;
end;

{ TFileTemplate }

function TFileTemplate.GetTemplateDataClass: TClass;
begin
  Result := TFile;
end;

function TFileTemplate.GetText(const Item: TObject;
  const ColumnIndex: Integer): string;
begin
  case ColumnIndex of
    0: Result := TFile(Item).Name;
    1: Result := DateTimeToStr(TFile(Item).ChangeDate);
    2: Result := IntToStr(TFile(Item).Size);
  end;
end;

One thing I am working on is making a generic data template class so you can get rid of the typecasting and defining the GetTemplateDataClass method. You also have to keep in mind that the data template relates to the column definitions because of the column index.

"I am still working on patter and presentation." - Paul Daniels 

When you download the VirtualTreeviewSample from the svn repository you can see more things that are possible with this presenter like data binding, change notification (through TObservableCollection) and using xpath to show xml in the tree in a super easy way. Originally it was designed to be usable in Delphi 7 but in the meanwhile I added a few things that are most likely not compatible (like TList<TObject> which should be easy replaceable). So if anyone likes to use this in an older Delphi version please let me know. I am also refactoring the class to be able to build presenter classes for other controls without having to write everything again from scratch.

As always your feedback is very welcome.

2 comments:

  1. If you can't use a generic TList with a more specific T type than TObject due to lack of covariance support, why not just use a TObjectList? Then you don't lose D7 compatibility.

    ReplyDelete
  2. Valid point but that's not the only thing not being compatible with D7. The presenter uses bindings (rtti) and multicast events (generics and record methods). Also I am thinking about making it compatible with Alex' Collection library (probably by compiler switch like in the yield unit). Also if you have some generic list from elsewhere in your program and you want to attach it to the presenter it would be incompatible with TObjectList while it's compatible with the generic one by hardcast.
    As said, if someone wants to use it that badly in earlier version I could think about cutting some stuff off, but actually I don't care that much for supporting older Delphi versions and putting myself into that backwards compatibility hell.

    ReplyDelete