This guide explains how to use the tiOPF’s mediators to build the GUI layer of your application. In short, the tiOPF GUI Mediators are a custom implementation of the Model-GUI-Mediator design pattern. It enables standard Delphi VCL, Lazarus LCL or fpGUI Toolkit components to become "object aware" and have bi-directional updates to/from your business objects, all without having to create custom GUI components or using DB-aware components.

Introduction

In this guide we will build a few screens based on tiOPF’s Demo 21 (Address Book). In the same light, we will reuse the existing business objects as is already defined in Demo 21. There will be another guide on faster ways of building a set of model classes using tiOPFMapper (the timap utility).

The idea of using Model-GUI-Mediator is to have a clear separation of your User Interface layer and you Business Object layer. So no business logic lives inside your user interface. This is very much the opposite of what Delphi’s RAD style application development popularised, by using DB-aware GUI components and then creating event handlers is your forms with all your business logic. The latter is very hard to maintain and writing unit tests for.

As mentioned earlier, tiOPF Mediatators is an implementation of the Model-GUI-Mediator (MGM) design pattern. Model-GUI-Mediator actually uses two classic design patterns together to accomplish it’s task. The first of the two patterns is the Mediator Pattern.
[Design Patterns (aka the Gang-of-Four book): Mediator page 273]
The MGM pattern creates a mediating view class that relays all communication between the GUI component and the business object. The second design pattern is the Observer Pattern.
[Design Patterns (aka the Gang-of-Four book): Observer page 293]
The business object communicates any changes to the mediating view class via observer. This gives it the ability to do bi-directional updates between the business objects and user interface.

The Model classes

The business object model we are going to use is the same model as defined in the unit <fpgui>/Demos/fpGUI/Demo_21_AdrsBook_MGM/model.pas, which is part of Demo 21 (Address Book). The following classes are defined:

  • TCountry

  • TCountryList

  • TCity

  • TCityList

  • TAddressType

  • TAddressTypeList

  • TAddress

  • TAddressList

  • TContactMemento

  • TContact

  • TContactList

The names should be pretty self explanatory. For the sake of keeping things simple, the demo project defined all model classes in a single model.pas unit. In a real world application you would not do that, and rather spread the model classes over several related units. But that is beyond the scope of this guide.

Mediators explained

If you look at the list of model classes, you will notice that there are classes that define a single element, and classes that define lists of those elements.

For that reason, we have two types of mediators. Mediators that manage a single element (eg. TContact), and composite mediators that manage a list of elements (eg. TContactList). We will look at how to handle both types.

Mediators can be instantiated manually and destroyed manually when the form is created or destroyed. That is a lot of boilerplate code. So tiOPF introduced the concept of a TtiModelMediator which will handle all that boilerplate code setting up and destroying of mediators. In turn, it greatly reduces the effort required to define your user interface.

TtiModelMediator

The TtiModelMediator was previously known as the FormMediator. The idea was that each form in your application will display a list of objects (eg. TContactList), or the details of a single object (eg. TContact). It was quickly realised that form designs can become more complex, and for that reason the class was renamed to TtiModelMediator. For now, we will keep the examples nice and simple - one ModelMediater per form.

The steps we will follow are as follows:

  1. Instantiate the ModelMediator instance

  2. Tell it what Subject to observe (single or list model)

  3. Tell it what published properties to display, using what GUI components.

  4. Activate the ModelMediator

With that said, it would mean that our form will have one TtiModelMediator instance. As TtiModelMediator is a TComponent descendant, you can assign its constructor Owner parameter as the form itself, which means the form with automatically destroy the ModelMediator instance when the form is destroyed.

The job of the ModelMediater is to manage lots of user interface controls/widgets, and their specific mediators for us. As we tell the ModelMediator what published properties we want to display on our form, internally there are a lot of things happening. For every property, the ModelMediator determines the data type of that property, and looks at what GUI component you told it to use. It then determines what the best mediator class would be, and instantiates an instance of it. The ModelMediator fully manages the lifespan of those instances.

For the ModelMediator to do all that "magic", you need to help it by defining the mapping between a data type and a GUI component to display that data. Luckily, tiOPF has already created a huge list of the most common mappings for you. You simply need to initialise them. That is done by calling the folling two methods. The code below is normally placed in the main form unit, but could be placed anywhere early in your application’s start-up sequence.

initialization
  RegisterFallBackMediators;
  RegisterFallbackListMediators;

If you do implement your own mediators [more on this later], you will have to register your custom data type to mediator mapping, in addition to the default ones. That would be done using a gMediatorManager.RegisterMediator(...) call.

ModelMediator class and single

Single Object Mediators

  • Basic properties of the object is easy

  • A object type property (eg: AddressType)

ModelMediator class and List Mediators

Here is some example code on setting up the model mediator instance and adding three composite [list] mediators. This is usually called from the Form’s OnCreate or OnShow event handlers.

procedure TMainForm.SetupMediators;
begin
  if not Assigned(FMediator) then
  begin
    FMediator := TtiModelMediator.Create(self);
    FMediator.Name := 'DemoFormMediator';
    { StringGrid component }
    FMediator.AddComposite('Caption(150,"Name",<);Age(50,"Age",>);GenderGUI(80,"Gender",|)', grdName1); 1
    { Listview component }
    FMediator.AddComposite('Caption(150,"Name",<);Age(55,"Age",>);GenderGUI(65,"Gender",|)', lvName1);  2
    { Listbox component }
    FMediator.AddComposite('Name', lstName1);        3
  end;
  FMediator.Subject := FPersonList;                  4
  FMediator.Active := True;                          5
end;

Lets explain a bit of what this code does.

1 Here we associate our StringGrid component grdName1 as a composite GUI view to display a list of objects.
2 Normally the model mediator will only be associated with a single GUI view, but here we show that multilpe view can observe the same list object. In this case our second GUI view is a Listview component lvName1.
3 Even a Listbox component lstName1 can be used as a GUI view displaying a list of objecst.
4 As MGM is based on the Observer pattern, here is where we assign the object the model mediator will observe. Any changes in this object and it will automatically update our GUI views for us.
5 To optimise rendering performance, by default the model mediator is inactive. Useful while it is being configured. Here we active the model mediator, and it will start doing its job, and all the hard work for us.

We associated three different GUI components to observer the same data, but as you have noticed, the way we set up the components is identical. One of many benefits in using the TtiModelMediator.

To give a more real world example of why you would have multiple GUI components observe the same data. Image your model holds data similar to a spreadsheet full of numbers. We could have two or more Graph GUI components as views. One displaying a Pie chart of the data, and another one displaying a Line graph of the same data.

When you design your form, you did not have to define the StringGrid or ListView properties in the Object Inspector of your IDE. You did nott have to define row count, column definitions, listview style etc. The TtiModelMediator took care of all that for you. All you as a developer had to do, was place the GUI view components in the locations you wanted it on the form. Demo 20 (List mediators) actually shows such a form design and the above code in action.

Three list views
Figure 1. Three list views observing the same data

So how does the GUI components know what to display and what component configuration to use? That’s where the .AddComposite(...) method call comes in, as well as the underlying view mediator classes.

tiOPF implements the most common properties for you, via the view classes. This has been done for all the most common standard VCL, LCL and fpGUI components. So the only time you would need your own custom view classes, is if you want some other custom behaviour, and even then, the custom view classes only need to override what they want changed - everything else can be left as defaults.

The first parameter of AddComposite() is a list of publised property names you want to display as columns, delimeted by a semi-colon.

A basic example can be something as simple as:

.AddComposite('Name;Age;GenderGUI', StringGrid1);

That means the StringGrid will be initialised with 3 columns of equal width. One for Name, Age and GenderGUI. All data will be left aligned and the column header names will be the same name of the published property names. This doesn’t always yield the desired results. For example, we would prefer the columns to have different widths, and the Age column to be right aligned. Also GenderGUI is not a very user friendly column header name, and we would like the gender data to be centre aligned too.

So each column definition can use the extended syntax which has this format:

property_name(col_width,col_header,alignment)

which has the following meaning:

property_name

The published property name to display

col_width

The width of the column in pixels

col_header

The column header caption

alignment

Indicates the alignment of the data displayed in the cells.

  • < meaning left aligned

  • | meaning centre aligned

  • > meaning right aligned

Here is an example using extended syntax:

.AddComposite('Caption(150,"Name",<);Age(50,"Age",>);GenderGUI(80,"Gender",|)', StringGrid1);

Main Form

The Address Book demo’s main form is very simple. We want to display the list of contacts (TContactList) in a grid of listview of sorts. I’ll demo with to show how little effort it is to switch between the two.

Formatting data in StringGrid cells

Why not use the Observer pattern that already exists in the base classes! We have created a set of "DisplayObject" classes that simply contains a bunch of string properties and returns the formatting for a specific object. So if you have a TTransaction object, you create a "gui display" class for example TTransactionDisplay for that object.

So how do does the Observer tie all this together? Quite easy!

Before we used to let the List Mediators (StringGrid or ListView) observe the Object List (TTransactionList) directly as shown below.

  TtiObjectList
       |
 TTransactionList  <-- List Mediators
                       like StringGrid
                       or ListView.

With our new "invention" if you will, we introduced the intermediate class which does the actual "gui" formatting. As show below.

  TtiObjectList            TtiObjectList
       |                         |
 TTransactionList  <-- TTransactionDisplayList  <-- List Mediators
                                                    like StringGrid
                                                    or ListView.

So the TTransactionDisplayList observes the TTransactionList. And the actual composite list mediator observers the TTransactionDisplayList.

This works really well so far. Also the "DisplayObject" classes are quite simple and quick to create. Here is an example of a actual class. All the formatting work gets done in the GetDisplay() method. This class could even be extended to support sorting etc and keeping all the "visual" stuff out of the actual BOM class.

TTransactionDisplay = class(TBaseDisplayObject)
private
  FTransaction: TTransaction;
  function    GetDisplay(AIndex: integer): string;
  procedure   SetTransaction(const AValue: TTransaction);
protected
  function    CheckTransaction: Boolean;
public
  constructor CreateCustom(const ATransaction: TTransaction);
  destructor  Destroy; override;
property    Transaction: TTransaction read FTransaction write SetTransaction;
published
  property    AccountNo: string index 0 read GetDisplay;
  property    TransDate: string index 1 read GetDisplay;
  property    TransDesc: string index 2 read GetDisplay;
  property    TaxAmount: string index 3 read GetDisplay;
  property    TransAmount: string index 4 read GetDisplay;
  property    TransType: string index 5 read GetDisplay;
  property    TransCode: string index 6 read GetDisplay;
end;

Demo 21 (Address Book) uses such a class to help explain how it works. Here is a Sequence Diagram to help visualise the interaction when data gets added or removed from the TTransactionList class.

Sequence Diagram
Figure 2. Display data sequence diagram

Questions and Answers

Q: Is there any way to set the maxlength of text edit components via MGM, or should I manually set it on each control?

You do it in the model class (eg: TLearner), the mediators will then automatically detect such restrictions and apply them automatically to the edit components.
eg:
We have a TLearner object that represents a student. Override the DoGetFieldBounds() protected method in TLearner, and apply your size restrictions (normally dictated by the database field sizes) to each property. AFieldName is the published property name of TLearner.

procedure TLearner.DoGetFieldBounds(const AFieldName: String;
   var MinValue, MaxValue: Integer; var HasBounds: Boolean);
begin
  if AFieldName = 'Name' then
  begin
    HasBounds := True;
    MinValue := 1;
    MaxValue := 30;
  end
  else if AFieldName = 'Surname' then
  begin
    HasBounds := True;
    MinValue := 1;
    MaxValue := 35;
  end
  { add other TLearner property bound rules here }
  else
    inherited DoGetFieldBounds(AFieldName, MinValue, MaxValue, HasBounds);
end;
Q: Is there a way to implement validation with feedback in the user interface?

There definitely is. Simply implement the IsValid() method in your business objects. Here is an example:

function TPerson.IsValid(const AErrors: TtiObjectErrors): boolean;
var
  lMsg: string;
begin
  Result := inherited IsValid(AErrors);
  if not Result then
    Exit;

  if Age < 18 then
  begin
    lMsg := ValidatorStringClass.CreateGreaterOrEqualValidatorMsg(self, 'Age', Age);
    AErrors.AddError(lMsg);
  end;

  if FirstName = '' then
  begin
    lMsg := ValidatorStringClass.CreateRequiredValidatorMsg(self, 'FirstName');
    AErrors.AddError(lMsg);
  end;

  Result := AErrors.Count = 0;
end;

And here is how it would look at runtime.

Validate with UI feedback
Figure 3. Object validation with UI feedback. Colour and tooltip.
Q: What if we use 3rd party components in our Delphi project? For example DevExpress.

It is extremely easy to implement the mediating view classes for any components. Somebody has already created DevExpress mediators, and they are available in the <tiopf>/Quarantine/DevExpressMediators/ directory.