Introduction
In the previous article, I described the problem I was trying to solve by dynamically generating user interfaces, that of how to allow the user to create their own “types” and how to represent these on the screen. This article focuses more on the concepts and code involved with mapping a dynamic user interface layer onto a domain model, or more specifically, creating a dynamic domain model to go with the dynamic user interface so that the user interface can map easily on to it. I would recommend reading that article before this one, because I will be carrying on from where I left off.
Recall the class diagram contained in the sample project:
At compile time, we have no idea what Machine Types are going to be created by the user and what fields these machine types have. This is why we’ve created this structure, after all. We have the option of modeling our Machine
class’ behaviour just as shown in the diagram, a structure that easily maps to the database, but makes mapping to a user interface and treating a Machine
as a particular Machine Type doable, but a little tricky and quite a bit of work. Conceptually, what I want is for Machine
to be a generic type, and to have types such as Machine
of Roller
and Machine
of Painter
or, more generically, Machine
of T
that I can work with, where Roller
, Painter
, or T
are machine types that are defined by the user at runtime.
In order to get this effect, I need to somehow define a Machine domain type in an object form. One way to model this is as follows:
In this example, for each type in my domain model, I would have one ClassDef
object which consists of one or more PropDef
objects that define the properties of the class. The idea is that when I create a domain model object such as MachineType
, the object gets a collection of properties based on the PropDef
s of the ClassDef
configured for MachineType
.
In our example, MachineType
is a class that inherits from BusinessObject
. Thus, when a MachineType
object is instantiated, it receives a collection of BOProp
objects as defined by MachineType
’s ClassDef
. These then become the true properties of the domain model object. We can write C# properties for those we know of at compile time, as follows:
public class MachineType : BusinessObject {
public virtual String Name
{
get
{
return ((String)(base.GetPropertyValue("Name")));
}
set
{
base.SetPropertyValue("Name", value);
}
}
}
The above C#, strongly typed, property uses the BusinessObject
’s protected GetPropertyValue()
and SetPropertyValue()
methods to retrieve and set the dynamic property values. Having these makes the objects easier to work with (Intellisense!) and behave exactly like normal domain model objects would (i.e., like those without dynamically created properties).
This overall structure gives us the benefit of a dynamically changeable set of properties along with the benefit of type-safety for those that are known at compile time.
Now, in order to create a user interface that maps onto this object, we must create a set of UIFormField
s that define our form (see the brief discussion of this near the end of part 1) and link them to the ClassDef
object:
The ClassDef
has a collection of UIForm
s to allow for different views on the same type – one might be a view that only shows a few properties, another might show all of them. Each UIForm
contains a collection of UIFormField
s, and each of these fields correspond to a PropDef
(the link being the PropertyName
field). These UIFormField
s describe how the instantiated BOProp
will be displayed. Through this structure, it is possible to dynamically create a form for any subtype of BusinessObject
and link the controls to the object underlying them.
However, this does not solve the problem of wanting to have multiple “parametrized” Machine types as there is only one ClassDef
per type. So, what we did was to change the structure from a one to one mapping between ClassDef
and Type
, and replace it with a many to one mapping, as follows:
The inclusion of the TypeParameter
property on ClassDef
allows us to differentiate between the many ClassDef
s for one Type
. Now, in our Machine example, we can effectively get run-time “types” by instantiating Machines with the appropriate ClassDef
object. In the example program, you can select a MachineType
and choose to create a new Machine of that type. At this point, the system asks the MachineType
object for a new Machine
, as follows:
public partial class MachineType
{
public Machine CreateMachine()
{
ClassDef machineClassDef = GetMachineClassDef();
var machine = new Machine(machineClassDef) {MachineType = this};
foreach (MachinePropertyDef machinePropertyDef in MachinePropertyDefs)
{
MachineProperty property = machine.MachineProperties.CreateBusinessObject();
property.MachinePropertyDef = machinePropertyDef;
}
return machine;
}
The first line of CreateMachine()
gets the Machine ClassDef
object corresponding to the MachineType
of the Machine
being created (we’ll look at GetMachineClassDef()
next). The next line creates the Machine
object, giving it the appropriate ClassDef
object so that it gets the correct properties. After this, there is some code used in supporting persistence and loading to a database structure, but that will be discussed in part 3, so I have left it out here. Finally, we return the new Machine
object which has the correct properties (BOProp
s) for this MachineType
. In other words, it is a Machine
of T
!
Continuing with the MachineType
class, we move on to the GetMachineClassDef()
method:
public ClassDef GetMachineClassDef()
{
string machineTypeClassDefName = "Machine_" + Name;
ClassDef machineClassDef;
string machineAssemblyName = "MachineExample.BO";
if (ClassDef.ClassDefs.Contains(machineAssemblyName, machineTypeClassDefName))
{
machineClassDef =
ClassDef.ClassDefs[machineAssemblyName, machineTypeClassDefName];
ClassDef.ClassDefs.Remove(machineClassDef);
}
machineClassDef = CreateNewMachineClassDef();
ClassDef.ClassDefs.Add(machineClassDef);
return machineClassDef;
}
This method retrieves the ClassDef
object corresponding to a Machine
of T
. When searching the ClassDefs
dictionary, we give it a parameterized name such as “Machine_Roller”, or “Machine_Painter”, and the ClassDefs
collection retrieves the ClassDef
object that matches this.
An important consideration to note is that even if it finds the matching ClassDef
, it removes it and creates a new one. This is because the very definition of the ClassDef
might have been altered by an action of the user since the ClassDef
was constructed, for example, by adding or changing a MachinePropertyDef
for that MachineType
. To ensure that the Machine
being created has got the latest definition of its type, we create the ClassDef
every time. Of course, this is the naïve solution – a better one would be to replace or update the ClassDef
when, and only when, a MachineType
is changed, and that is how we have implemented it in production projects.
The final piece to look at is how the ClassDef
is created because it is in this process that the UI form definition is built up too:
private ClassDef CreateNewMachineClassDef()
{
ClassDef baseMachineClassDef = ClassDef.ClassDefs[typeof (Machine)];
ClassDef machineClassDef = baseMachineClassDef.Clone();
machineClassDef.TypeParameter = Name;
Here, the basic Machine ClassDef
is taken, cloned, and a type parameter is applied. If we’re getting the ClassDef
for a MachineType
called “Roller
”, the TypeParameter
property will be set to “Roller”, and conceptually, we now have a ClassDef
for a Machine
of Roller
.
UIDef uiDef = machineClassDef.UIDefCol["default"];
UIGrid uiGrid = uiDef.UIGrid;
UIForm form = uiDef.UIForm;
form.Title = "Add/Edit a Machine";
UIFormTab tab = form[0];
UIFormColumn column = tab[0];
This code simply gets the UIFormColumn
object out that contains all the UIFormField
s. I left the UIFormTab
and UIFormColumn
out of the diagram earlier as they are simply organizational structures used for layout that don’t affect the way we conceptually think of a form as simply containing a set of fields. You’ll see that a UIGrid
is also retrieved – this is so that we can set up the columns for grids showing this Machine Type (grids are modeled in the same way as forms, but with a slightly different structure).
foreach (MachinePropertyDef machinePropertyDef in MachinePropertyDefs)
{
var machinePropertyPropDef =
new PropDef(machinePropertyDef.PropertyName, "System",
machinePropertyDef.PropertyType,
PropReadWriteRule.ReadWrite, "", null,
machinePropertyDef.IsCompulsory.Value, false);
machinePropertyPropDef.Persistable = false;
machineClassDef.PropDefcol.Add(machinePropertyPropDef);
Here, we set up the Machine
of X
ClassDef
with all of X
’s properties as they are defined by the MachineType
’s MachinePropertyDef
s. They’re marked non-persistable as their persistence is dealt with separately via the MachineProperty
objects. For now, I am ignoring persistence – it will be covered in part 3. Note that when the PropDef
object is created, it is given the Machine Property Definition’s PropertyType
and IsCompulsory
values so that they can be validated before saving.
var uiProperty =
new UIFormField(null, machinePropertyDef.PropertyName,
"TextBox", "System.Windows.Forms", "", "",
true, "", new Hashtable(), null);
column.Add(uiProperty);
This adds a form field for the MachinePropertyDef
we’re working with, a field that will show up on the form for a Machine
of T
.
uiGrid.Add(
new UIGridColumn(machinePropertyDef.PropertyName,
machinePropertyDef.PropertyName, "", "",
false, 100, UIGridColumn.PropAlignment.left,
new Hashtable()));
In the same way, we add a grid column. In the sample application, you can see this in action: when you select a different Machine Type in the Data | Machines screen, the grid columns change depending on which one is selected. The grid is configured at runtime with the columns added in this line of code.
}
return machineClassDef;
}
}
Finally, we return the newly created and configured ClassDef
for a Machine
of T
.
These three methods on the MachineType
class have fully configured the dynamic creation, viewing, and editing of any Machine Type you can imagine. No further customisation is required – when a Machine
is edited, the UI generation classes simply use the ClassDef
associated with that Machine
of T
to create the form or grid.
The last item to look at is how the dynamic properties of the Machine
are persisted to and loaded from the database. Look out for part 3 in the next few days!
Links
The Habanero Enterprise Framework was used in the creation of the example code.
History
- 27 Nov 2008: Added the article.