Saturday, April 30, 2011

Data Bindings

I just published a post about my Delphi data binding implementation on Delphi-Praxis.

Since it is german here is the translation:

Those who already worked with .Net most likely know and like them: data bindings
You can bind almost everything. Like showing collections of objects in tree-, list- or gridviews or editing properties of objects in text-, check- or comboboxes. You can even change the visibitiy or the appearance of controls with them.

You can read about how it works exactly on the MSDN.

So what? In Delphi we have dataset, datasource and those db controls to do this!
True, but there is just one catch: you cannot just add a foo object to them. True, there is TSnapObjectDataset or special controls to do that. The problem is that you have to use those special controls or components and you have to inherit your bindable objects from some base class.

So I took a closer look at how .Net does it and implemented it in Delphi with the following goals in mind:
  • standard out of the box components have to be supported (i.e. TEdit)
  • little implementation necessary for bindable objects
  • little source code to actually bind objects

Ok, lets start. What do we need?

A binding always requires a source and a target (both objects) and the names of the properties that are bound.
For our first example we create some simple class that got 2 properties:

type
  TSettings = class
  private
    FCaption: string;
    FColor: TColor;
    procedure SetCaption(const Value: string);
    procedure SetColor(const Value: TColor);
  public
    property Caption: string read FCaption write SetCaption;
    property Color: TColor read FColor write SetColor;
  end;

implementation

procedure TSettings.SetCaption(const Value: string);
begin
  FCaption := Value;
end;

procedure TSettings.SetColor(const Value: TColor);
begin
  FColor := Value;
end;

We already have those explicit setter methods - we will need them later. For a first example we could get rid of them but since the auto completion created them we just let them where they are.

So, what are we doing with out TSettings object? We create 2 bindings to set the caption and the color of our MainForm.

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FSettings := TSettings.Create;
  FSettings.Caption := 'Binding demo';
  FSettings.Color := clGray;
  TBinding.Create(FSettings, 'Caption', MainForm, 'Caption');
  TBinding.Create(FSettings, 'Color', MainForm, 'Color');
end;

It only requires using the System.Bindings unit. Also we do not need to take care about freeing the objects as long as one of the bound objects if inherited from TComponent (in our case that is the MainForm).

Lets start the application and see what happens. Caption and color of the MainForm are set according to the values in our TSettings object. Awesome, no?
Well, setting those values would have done that too... Sure, but that's not the end of the story!

Lets put a button on our form and set the properties of our settings object there.

procedure TMainForm.Button1Click(Sender: TObject);
begin
  FSettings.Caption := 'Current time: ' + DateTimeToStr(Now);
end;

That is not doing anything for now but we are not done yet. Let's go back to out TSettings class and modify it.
We inherit from another class that already implements the INotifyPropertyChanged interface. Nicely taken from .Net of course. Ah, now we need the setter methods for the properties.

So that is how the modified class looks now:

type
  TSettings = class(TPropertyChangedBase)
  private
    FCaption: string;
    FColor: TColor;
    procedure SetCaption(const Value: string);
    procedure SetColor(const Value: TColor);
  public
    property Caption: string read FCaption write SetCaption;
    property Color: TColor read FColor write SetColor;
  end;

implementation

procedure TSettings.SetCaption(const Value: string);
begin
  FCaption := Value;
  DoPropertyChanged('Caption');
end;

procedure TSettings.SetColor(const Value: TColor);
begin
  FColor := Value;
  DoPropertyChanged('Color');
end;

Now if we click the button the caption of the MainForm changes. Nice, but still not rocking that much, isn't it?

Well, ok. Let's throw some edit on our form and bind that to the caption of the MainForm.

TBinding.Create(FSettings, 'Caption', Edit3, 'Text');

Wow, now the caption is displayed in the edit but it's doing nothing when I type.
Correct, we are missing something. The unit System.Bindings.Controls has to be used as the last unit in the interface section (or to be more precisely after the units with the controls we wanna bind to).
This unit contains subclasses to some standard VCL Controls with the same name that implement the INotifyPropertyChanged interface. Using this unit after the other VCL units makes those classes the ones that are used when the controls are created. That means you don't have to place special controls on your form to use data binding. Just the normal out of the box ones.

Let's start the application again and type something into our edit. Awesome, the caption of the form changes!

To make it clear:
  • no special controls needed (only the subclassing of the controls that you want to be bindable to)
  • no special events needed
  • can use TObject descendants for read-once sources (only implementing of the interface if necessary to notify on changes)
  • little source code to actually bind objects, components and controls

What is missing so far is binding collections to other components but this is already in the works. So you will be able to bind your lists and collections to controls like TListBox or even a TVirtualStringTree.

Your feedback is welcome as always.

The sample and all source code can be downloaded here.

5 comments:

  1. If you take a look at the new Delphi XE2 beta you can see that there is a very nice implementation of this coming up, out of the box...

    ReplyDelete
  2. Isn't there some NDA for beta that forbids to talk about the features in it? Unfortunatly I don't have beta access (yet).
    Nice to see they finally implemented this out of the box. I'm very excited seeing it.

    ReplyDelete
  3. I'm very excited to have found this, I think it will be a huge help to me.

    Off to play a bit I think.

    ReplyDelete
  4. Is it possible to use your code with delphi 7 ? I have some problems with Generics.Collections and it seems to be available since delphi 9.

    Is your framework refactorable to use with delphi 7 ?

    ReplyDelete
  5. Hi Sébastien,

    I am indeed making it work on Delphi 7 (maybe even lower but I only have D7 at the moment) and Lazarus soon (tm). There will be some missing features and the interface will look a bit different than the current version which is for Delphi 2010 and higher.

    The main difference will be that it only works with published properties as I have to use the "old" RTTI. Also the unit names will be different as there are some problems with dots in unit names in older Delphi versions and Lazarus/FPC.

    Currently I am refactoring the source to keep it the same as much as I can so I don't have to maintain two versions.

    ReplyDelete