Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Dynamically Generated User Defined Interfaces (Part 2)

4.83/5 (13 votes)
27 Nov 2008CPOL7 min read 42.6K   489  
Further explanation about how we solved the problem of allowing users to define their own data structures and to define their own user interfaces to edit this data.

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:

Image 1

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:

Image07-ClassDefDiagram1.png

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 PropDefs of the ClassDef configured for MachineType.

Image08-ClassDefDiagram2.png

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:

C#
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 UIFormFields that define our form (see the brief discussion of this near the end of part 1) and link them to the ClassDef object:

Image09-ClassDefDiagram3.png

The ClassDef has a collection of UIForms 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 UIFormFields, and each of these fields correspond to a PropDef (the link being the PropertyName field). These UIFormFields 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:

Image10-ClassDefDiagram4.png

The inclusion of the TypeParameter property on ClassDef allows us to differentiate between the many ClassDefs 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:

C#
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 (BOProps) for this MachineType. In other words, it is a Machine of T!

Continuing with the MachineType class, we move on to the GetMachineClassDef() method:

C#
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:

C#
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.

C#
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 UIFormFields. 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).

C#
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 MachinePropertyDefs. 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.

C#
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.

C#
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.

C#
        }
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)