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:
-
Instantiate the ModelMediator instance
-
Tell it what Subject to observe (single or list model)
-
Tell it what published properties to display, using what GUI components.
-
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);
{ Listview component }
FMediator.AddComposite('Caption(150,"Name",<);Age(55,"Age",>);GenderGUI(65,"Gender",|)', lvName1);
{ Listbox component }
FMediator.AddComposite('Name', lstName1);
end;
FMediator.Subject := FPersonList;
FMediator.Active := True;
end;
Lets explain a bit of what this code does.
Here we associate our StringGrid component grdName1 as a composite GUI view to display a list of objects.
| |
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 .
| |
Even a Listbox component lstName1 can be used as a GUI view displaying a list of objecst.
| |
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. | |
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.
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.
|
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.
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 aTLearner
object that represents a student. Override theDoGetFieldBounds()
protected method inTLearner
, and apply your size restrictions (normally dictated by the database field sizes) to each property.AFieldName
is the published property name ofTLearner
.
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.
- 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.