Posts Tagged business-objects

Inducing The Great Divide

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:

image

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

image

2. Click Load Form.

image

3. Mess around with the values and click Save to Object.

image

4. Click Clear Form.

image

5. And finally, load again.

image

Comments (22)