One of the most important principles in building complex software systems, is detaching the business logic from the screens that allow users to view and edit information.
You’ll notice I said “most important” and not “most popular.” Sad but true – all modern IDEs make it so easy for a programmer to simply whack together a bunch of user interface controls, that there are probably far more forms with embedded business logic than without.
The fact is that for simple systems, this can often be convenient and harmless. But for more complex ones, it nearly always spells disaster (or at least chronic pain). The gold standard for these kinds of systems is to have a set of business objects and a set of screens that merely displays and edits them.
Building a proper business object framework is a complex topic all of its own, but Delphi (and all the other modern languages) give us more than enough riches in the object model to implement what we need in this regard.
What environments like Delphi don’t do very well, is give developers the tools to build a form that can capably display and edit business objects without knowing too much about them. And very generic entry forms only work well for very generic data. Invariably, specialised data require specialised forms that give the user appropriate functionality and a sensible layout.
So you end up with lots and lots of code like this:
procedure TCustomerForm.LoadForm(Customer: TCustomer); begin NameEdit.Text := Customer.Name; EmailEdit.Text := Customer.Email; end; procedure TCustomerForm.PostForm(Customer: TCustomer); begin Customer.Name := NameEdit.Text; Customer.Email := EmailEdit.Text; end;And this kind of boiler-plate code is yet another reason while lots of programmers throw structure to the hounds and just integrate the business logic into the form.
Well, no need.
Delphi’s RTTI framework has always allowed one to access and manipulate all the published properties and fields on an object and I have been using a framework based on that for over ten years now. Simply publish all the properties you want to represent on the form and you can match object properties to screen controls by name and type.
Delphi 2010 has of course overhauled the RTTI framework completely and I quickly adapted my code to use that instead. Truth be told, the new RTTI is a little finicky and brittle, but it is also far richer in functionality and I found it well worth the effort to overhaul my classes.
What I am about to present here is not based in any way on the framework in our system, but illustrates very nicely how that works. Having said that, what I will show here is also simplified to the point that you will definitely want to extend it a little before you go gold with it.
Exploring The Cliff Faces
There is no real limit to the data types that you can have on your business object, but VCL controls almost all use simpler types to represent their values. So the first thing you’ll need is some sort of mapping of how you will want to represent information.
Here is a quick table of reasonable mappings:
Business Object Property | Possbile VCL Controls |
string, TObject | TEdit, TComboBox, TLabel |
Boolean | TCheckBox |
Integer | TEdit, TUpDown, TTrackBar |
Double | TEdit |
TDateTime | TDateTimePicker |
Enumerated type | TComboBox, TListBox |
Set type | TCheckListBox |
I’m sure you can think of a few more and probably disagree with me on a few, but you get to pick your own.
The plan is to have a completely autonomous business object and a very simple screen that can display and modify the business object without knowing much about it.
Here is the business object I am going to use:
TPerson = class private FName: string; FAge: Integer; FOccupation: string; public property Name: string read FName write FName; property Age: Integer read FAge write FAge; property Occupation: string read FOccupation write FOccupation; end;And my form is as simple as they get too:
The load button predictably loads the content of a TPerson instance into the form, the Save button does the reverse and Clear Form… well OK.
Here is the code for the Load and Save buttons:
procedure TCustomerForm.LoadButtonClick(Sender: TObject); begin Binding.Load; end; procedure TCustomerForm.SaveButtonClick(Sender: TObject); begin Binding.Save; end;That is about as simple as it gets, right? An obvious enhancement is to make the form validate the values before assigning them to the form, but let’s not get ahead of ourselves.
The Binding variable above is of the TObjectBinding class which links together any TWinControl and any object. Before I show that – here are a few things you probably know, but let’s cover them anyway:
- All controls placed on a form at design time get their own published fields. This lets the streaming system know what should go into the form file and what shouldn’t.
- Not all public properties on your business object necessarily need to be displayed on the form. And none of those properties that you want to display will be published unless you made them so.
- Properties have names. So do screen controls. They may even match, but lots of programmers prefer to have their controls named something like AgeEdit or EdtAge instead of just Age.
- Not all properties are writeable. In fact, you could (probably shouldn’t) even create properties that can be written to but not read.
Clearly then, we can find the relevant controls by using Delphi’s new RTTI framework to iterate the published fields that are controls. Finding the relevant business object fields can be as simple as grabbing all the public and published properties or it can involve checking attributes. Or only properties with an even-length name. My class has a method to pick out valid fields and another to pick out valid properties. Modify these to your heart’s content.
And in production code you may want to pick up some of the form’s properties as well. Works well when you need to override some of the basic functionality.
Building The Narrow Bridge
Here is the class declaration – I’ll discuss the methods as we go along:
TObjectBinding = class private // Quick way to get from the object // field to its screen control. PropFieldMapping: TDictionary<TRttiProperty, TRttiField>; // Needed for RTTI. Context: TRttiContext; ControlType: TRttiType; ObjType: TRttiType; // The control (normally form) and // the object it represents. Control: TWinControl; Obj: TObject; // Finds the object properties that have corresponding // fields and stores them in the dictionary. procedure CreateMappings; function FindField(Prop: TRttiProperty; out Field: TRttiField): Boolean; function FieldClass(Field: TRttiField): TClass; // Modify these to change the rules about what // should be matched. function IsValidField(Field: TRttiField): Boolean; function IsValidProp(Prop: TRttiProperty): Boolean; // Modify these to change the mappings of property type // to VCL control class. procedure AssignField(Prop: TRttiProperty; Field: TRttiField); procedure AssignProp(Prop: TRttiProperty; Field: TRttiField); // Used from AssignField/AssignProp. Extend these to // support a wider range of properties. function GetPropText(Prop: TRttiProperty): string; procedure SetPropText(Prop: TRttiProperty; const Text: string); public procedure Load; procedure Save; constructor Create(Control: TWinControl; Obj: TObject); destructor Destroy; override; end;Load and Save can easily be supplemented here with Validate, which will use attributes specified on the business object properties to decide whether the current screen values are valid. If not, we don’t call Save at all.
The constructor and destructor do what you expect. They create and destroy the objects held in those private fields. The constructor also calls CreateMappings, which is where a lot of the fun stuff happens.
constructor TObjectBinding.Create(Control: TWinControl; Obj: TObject); begin inherited Create; Self.Control := Control; Self.Obj := Obj; Context := TRttiContext.Create; ControlType := Context.GetType(Control.ClassInfo); ObjType := Context.GetType(Obj.ClassInfo); PropFieldMapping := TDictionary<TRttiProperty, TRttiField>.Create; CreateMappings; end; destructor TObjectBinding.Destroy; begin PropFieldMapping.Free; ObjType.Free; ControlType.Free; Context.Free; inherited; end; procedure TObjectBinding.CreateMappings; var Props: TArray<TRttiProperty>; Prop: TRttiProperty; Field: TRttiField; begin Props := ObjType.GetProperties; for Prop in Props do if IsValidProp(Prop) and FindField(Prop, Field) then PropFieldMapping.Add(Prop, Field); end;CreateMappings scans through all the properties on the business object and attempts to find fields on the form to match them to. If a match is found, they are stored in the PropFieldMapping dictionary for future reference.
Notice the IsValidProp function. That is where you should impose any additional rules about whether or not a property should be taken into account. The default implementation simply grabs all the public and published properties.
function TObjectBinding.IsValidProp(Prop: TRttiProperty): Boolean; begin Result := Prop.Visibility >= mvPublic; end;Also, there is a FindField function. This little guy does a bit more than just grab all the published fields. Again, there is an IsValidField function for you to toy with. But it first attempts to find a control with a name that matches exactly with that of an approved property. If not, it also checks for a few common variants of the name (AgeEdit and EdtAge as mentioned before). This list will obviously need to grow if you support many more controls.
function TObjectBinding.FieldClass(Field: TRttiField): TClass; begin Result := GetTypeData(Field.FieldType.Handle).ClassType; end; function TObjectBinding.IsValidField(Field: TRttiField): Boolean; begin Result := (Field <> nil) and (Field.Visibility = mvPublished) and (Field.FieldType.TypeKind = tkClass) and (FieldClass(Field).InheritsFrom(TControl)); end; function TObjectBinding.FindField(Prop: TRttiProperty; out Field: TRttiField): Boolean; const Embelishments: array [0..5] of string = ('Edt', 'Edit', 'Combo', 'ComboBox', 'Lookup', 'Lkp'); var Emb: string; begin Field := ControlType.GetField(Prop.Name); if IsValidField(Field) then Exit(True) else for Emb in Embelishments do begin Field := ControlType.GetField(Prop.Name + Emb); if not IsValidField(Field) then Field := ControlType.GetField(Emb + Prop.Name); if IsValidField(Field) then Exit(True); end; Result := False; end;Now that we have a dictionary of business object properties and the screen fields that we want to link them to, we can take a look at the very similar-looking Load and Save methods.
procedure TObjectBinding.Load; var Prop: TRttiProperty; begin for Prop in PropFieldMapping.Keys do AssignField(Prop, PropFieldMapping[Prop]); end; procedure TObjectBinding.Save; var Prop: TRttiProperty; begin for Prop in PropFieldMapping.Keys do AssignProp(Prop, PropFieldMapping[Prop]); end;AssignProp and AssignField are probably the two most difficult functions to write in this entire class. The problem is that Delphi’s TValue structure used in the new RTTI framework makes no attempt whatsoever to convert data. Instead, you need to do any conversions yourself and assign to the exact type you need. That isn’t too much of a problem, but it means lots of individually-crafted conversion blocks.
To keep my example manageable, I opted to convert everything to strings and back using GetPropText and SetPropText. And I only support string, Integer, Double and TDateTime. I didn’t use Double and TDateTime on my TPerson business object, but they do require a special trick that I wanted to show.
procedure TObjectBinding.AssignField(Prop: TRttiProperty; Field: TRttiField); var NestedControl: TControl; PropText: string; begin NestedControl := Field.GetValue(Control).AsObject as TControl; PropText := GetPropText(Prop); if NestedControl is TCustomEdit then TCustomEdit(NestedControl).Text := PropText else if NestedControl is TCustomComboBox then TComboBox(NestedControl).Text := PropText; end; procedure TObjectBinding.AssignProp(Prop: TRttiProperty; Field: TRttiField); var NestedControl: TControl; FieldText: string; begin NestedControl := Field.GetValue(Control).AsObject as TControl; if NestedControl is TCustomEdit then FieldText := TCustomEdit(NestedControl).Text else if NestedControl is TCustomComboBox then FieldText := TComboBox(NestedControl).Text else FieldText := ''; SetPropText(Prop, FieldText); end; procedure TObjectBinding.SetPropText(Prop: TRttiProperty; const Text: string); var V: TValue; begin case Prop.PropertyType.TypeKind of tkInteger: V := StrToIntDef(Text, 0); tkFloat: if Prop.PropertyType.Handle = TypeInfo(TDateTime) then V := StrToDateDef(Text, 0) else V := StrToFloatDef(Text, 0); tkUString: V := Text; // And other types handled in similar ways else Exit; // Or some reasonable default action end; Prop.SetValue(Obj, V); end; function TObjectBinding.GetPropText(Prop: TRttiProperty): string; var V: TValue; begin V := Prop.GetValue(Obj); case Prop.PropertyType.TypeKind of tkInteger: Result := IntToStr(V.AsInteger); tkFloat: if Prop.PropertyType.Handle = TypeInfo(TDateTime) then Result := DateToStr(V.AsType<TDateTime>) else Result := FloatToStr(V.AsType<Double>); tkUString: Result := V.AsString; // And other types handled in similar ways else Result := ''; // Or some reasonable default end; end;Firstly, the AssignProp and AssignField functions each has a check for every supported VCL control base class. So to support TCheckBox or TCheckListBox this is where you would add the additional support code.
Then take a look at GetPropText and SetPropText. They mirror one another of course, so we can learn what we need by inspecting GetPropText.
First thing to notice is that string properties use the tkUString type and not tkString as in pre-Unicode versions of Delphi. Also, we can simply read TValue.AsInteger when reading an integer value.
The trouble arises when we look at Double and TDateTime. TDateTime is actually a type alias for Double and they both show up as tkFloat. This is why I compare the type info from my property with the type info for TDateTime. The alternative would be to check the type name, but that thought makes me queasy. The same kind of thing will be needed to tell Boolean from other enumerated types.
Also, both Double and TDateTime are incompatible with TValue.AsExtended, which is of course a floating point type of a different size. So taking care of all these type conversions become really cumbersome, but of course you only need to write the code once.
And that’s it
Hooking all of this together, you can now have business objects that are completely detached from their editing screens. You’ll find that the screens become really trivial to develop – most cases involve little more than slapping on the controls, naming them and calling Load and Save. And that last bit can be done is a base class.
You also get to take advantage of all the other advantages that come with a proper split. You will find it easier to modify your business rules, easily use the same business logic in other contexts (say a bulk import versus the capture screen) and also be able to validate your business rules using automated tests. Good idea!
I can now show you my super-complex test app in action. Note that the first screen shot shows the names of both edit boxes and the combo box. Despite the different naming conventions, they all link up automatically to the correct properties.
1. Starting up.
2. Click Load Form.
3. Mess around with the values and click Save to Object.
4. Click Clear Form.
5. And finally, load again.
Mason Wheeler said
Very nice system! But how do you implement lookups? You put “Lookup” and “Lkp” in your list of embellishments, but how do you actually create a lookup system (I’m thinking of my old friend, the TDBLookupComboBox, here) without a dataset?
Cobus Kruger said
OK, the Lkp and Lookup are actually user interface controls I use often at work. In that case the lookup actually queries the database correctly, which works OK with much less work involved than hooking up lookup datasets. That control is quite old though and with Delphi’s new attribute support, I would probably lean toward a system where you can mark the property with an attribute that specifies range. Food for though for a follow-up post.
Jolyon Smith said
Yep, an impressive demo of RTTI. I don’t see it scaling very well beyond such a trivially simple example though.
e.g. consider a TCompany with a Contacts property of type TContactList… what do you bind that to? TListBox? Maybe… but then in some areas of your application perhaps you instead want a TListView into that contact list.
Or maybe a combo box to allow one contact to be selected from those in the company. Or maybe an edit box with a picker button.
There is no 1:1 binding that says “this property (SomeCompany.Contacts) is visualised in a control of type X”. The visualisation depends not just on the business object but on the application context.
I don’t mean to disparage your efforts, but what you have shown above is really not much more than good old Data Controls, just not as feature complte and with the binding mechanism working differently.
Data Controls also used to make for great demos but hog tied any application built using them into following rigid rules in the construction of the user interface and prevented the creation of truly rich user-experiences.
There’s also the question of performance of code that reliance on attributes and RTTI brings.
edCustomerName.Text := aCustomer.Name;
Has zero infrastructural overhead. The control is given it’s data from the appropriate source. An approach based on RTTI and attributes (which I’m sure is where you are heading) is encumbered by the overhead of having to do constant referrals to infrastructure code to discover things that are actually largely fixed.
Much as we think “it would save us hours, days and weeks of time if controls could be bound to business object data without code, so that if we changed the business object the UI code doesn’t need to be updated” the truth is that 80-90% of business/UI code doesn’t ever actually change at all.
So when we find ourselves having to fix up a control here or there we mentally extrapolate those few minutes into hours of potential saving, when in fact the saving is likely to be nothing like that great.
The work itself is of course also tedious, so those few minutes perhaps even FEEL like hours.
ime.
ymmv.
But I’m still interested to see where you take this.
Cobus Kruger said
You’re right, Jolyon – there is no 1:1 mapping of business properties to VCL controls. But that is in part what I’m getting to here. The class supports loading the property into various types of controls and the one that you place on the screen is the one that will be filled in. So one list of contacts can be loaded into a combobox and another into a list view and another into a list box – depending on what the screen was designed to use.
There are two basic reasons that data controls didn’t work out for me. First, to get them to work you first need to get your business object loaded into a dataset or wrapped by one. The first is exactly what I don’t want and the latter is decidedly non-trivial – I wrote a database engine that we use instead of DBExpress and getting the dataset decendents to work in all instances was a nightmare. The second place where DB controls fall down is that they are a separate little group by themselves. If you mean to get a sophisticated interface going, DB controls almost certainly don’t have what you need.
And finally, I’ve been using it for years and it scales very nicely. I didn’t really plan to follow up on this post, but perhaps I should to show how this works on a larger scale.
Jolyon Smith said
w.r.t lists of contacts in a combo box, there are two bindings into the one control required: 1 for the list, the other for the currently selected item in that list.
i.e. from this list of Company.Contacts, the selected one should be Company.PrimaryContact, or possibly the member in the Contacts list that has the IsPrimary property = true.
In contemplating the “divide”, I find it interesting that such frameworks tend to impose concerns onto the business objects that they really shouldn’t need to worry about.
In such cases it is likely that the framework will decide that they need to be handled in a certain way (e.g. the combo box “binding” will support selection of an item in it’s list based on some value of an identified property of a member of the list), and that then constrains the implementation of the business object, compelling it to comply with the model expected by the generic GUI framework.
i.e. the Company object cannot have a PrimaryContact property, because this won’t work with combo-box bindings (or at the very least, if it *does* have a PrimaryContact property, then the Contact object must also have an IsPrimary property.
And just to clarify, my reference to scalability was only partially related to volumes of data or numbers of controls on forms. it was more concerned with scalability from simple data entry forms up to far more complex and rich UI’s (which is the trend ime).
Deivid said
Hi, very nice you post.
I’m very interested in new features of Delphi 2010 in its new rtti.
I’ll post on my blog a reference to your post that I found very interesting, if you do not agree please let me know that I will delete immediately.
Paul said
Is it possible to make the source code available?
A follow up on scaling would be very interesting.
Birger Jansen said
And this is exactly where you can use a framework like tiOPF (http://tiopf.sourceforge.net/). It allows you to connect your business objects (TPerson) to source of data (SQL database, xml, text, soap call) and the interface by using mediators (called bindings in your example).
We have written mediators for the DevExpress framework, so all our business objects are nicely bound to the DevExpress controls. This includes grids (with in-place editing), lookups, etc.
I can really recommend tiOPF to anybody that wants to experiment with ‘the great divide’!
Leonardo M. Ramé said
Cobus, I enjoyed reading your post, it did remember when I started thinking about a solution to “the great divide”, who took me to the creation of simple a Model-to-View framework based on RTTI.
I posted an article in my blog as a response to the comments I’d read here.
Leonardo.
HMcG said
Interesting post. But does this approach tend to lead to bad GUI design? If the GUI is built without thought as to the actual content? I realise that this is a simple demo, but, as a software user, statements like “most cases involve little more than slapping on the controls, naming them and calling Load and Save” remind me far to well of all the crappy forms I have had the misfortune of having to use.
I appreciate that there are potential benefits, but there are also pitfalls…….
HMcG
Cobus Kruger said
I doesn’t need to. The “slapping controls on” comment is probably spot-on for simple data entry forms but you’re right in that sophisticated forms will need more. The form is supposed to contain code to display and manipulate content and it needs to do that in a way that is logical, functional and pleasing to use. It should not, however perform a tax calculation by reading column 7 from a string grid or passing the TEdit representing the amount. The “Great Divide” in my post refers to spliting screen functionality from business logic. That doesn’t mean the form becomes less important; in fact when the form can focus only on its own functionality, you can often use simpler code to achieve sophisticated functionality.
Javier Santo Domingo said
I use this kind of approach too, and my forms are really complex sometimes, i mean, with controls that hold complex data inside, and that means sometimes you have to validate with a large amount of code. The key is to abstract the loading/saving tasks as much as possible, even from the validation code. That gives you a clean idea of your code, making it easier to improve adding/modifying/removing functionality. Of course you have to use frames and datamodules as much as possible to avoid loading too much your forms with unnecessary stuff.
In my case, I have developed a whole base business objects system that I reuse all the time, and it shows to be a really reliable design practice, even at the form level. Just put everything at its very own abstraction level: following the Single Responsability Principle makes your life easier.
Laurent PIERRE said
@Javier Santo Domingo: Hello Javier, I’m working on this kind of dephi code so, I would love to see what is (or has been) your approach on this part of code. You would be ready to share this code ?
@Cobus : Thank you to share this really nice code. I like this programming approach.
Cameron said
If you are going through the effort of writing a system to hydrate, why not just go one more big step. By changing the type for each property of a data object, you could easily define the form on the fly. You could then create an object to allow a generated form to be changed at runtime and preserved so customizing the form would be easy. Throw in an application level skin and your entire application would be data classes and the links to them. Why go through the effort of creating the data object, creating the form and creating the hydration all manually for the simple data entry forms? Quick example:
IFormRender = Interface(IInterface)
function GetFormShowstate: String;
procedure SetFormShowState(Value: String);
property FormShowState: String read GetFormShowState write SetFormShowState;
procedure Show;
end;
TDataObject = TInterfacedObject;
TPerson = class(TDataObject, IFormRender)
private
FName: FormString;
FAge: FormInteger;
FOccupation: FormString;
FID: HiddenString;
public
property Name: FormString read FName write FName;
property Age: FormInteger read FAge write FAge;
property Occupation: FormString read FOccupation write FOccupation;
property ID: HiddenString read FID write FID;
end;
Michael Justin said
Mixing business classes with presentation logic? Sounds like the return of the spaghetti-code monster is near 🙂
Anthony Frazier said
Why do I get the feeling that there’s a way to use Attributes to define the GUI mappings in the form definition instead of having rules about how components must be named?
I just don’t have the time to flesh out the particulars right now…
Lots of Little Bits « Source Code Adventures said
[…] last post, Inducing The Great Divide, got quite a reaction. I didn’t really expect the approach to be particularly controversial, but […]
Douglas said
Nice implementation of RTTI’s feature!
I have a question, could you tell me how is the instantiation of the class ObjectBinding, the parameters that are passed in the constructor of the class.
Thank you.
Robert Love said
I just found your post, very nice indeed.
I just started working on on a similar solution, can I use your code as a basis?
I was using Attributes to define custom bindings.
Willing to share the result.
Cobus Kruger said
Oh absolutely; use, reuse, abuse or discard any or all of it, in any way you choose. I’m actually working on a more complete system that I intend to make public domain, but long hours at the office have slowed me considerably. I’d love to see your implementation, so I’ll be keeping a close eye on your blog.
La gran división… | Delphi básico: Lo más básico de Delphi said
[…] Inducing The Great Divide […]
Salvador Jover said
Hi Cobus:
I enjoyed reading your post.
I think its interesting for spanish delphi developers and I’m going to comment about it in my blog.. (If you dont have problem)
Thanks
http://sjover.com/delphi/?p=1457