Click here to Skip to main content
13,762,417 members
Click here to Skip to main content
Add your own
alternative version

Stats

21.3K views
661 downloads
31 bookmarked
Posted 9 Aug 2015
Licenced CPOL

A Code-First User Interface Library

, 9 Aug 2015
Rate this:
Please Sign up or sign in to vote.
Describes a 'Proof of Concept' project for a Code-First User interface library.

Sample Image - maximum width is 600 pixels

Introduction

In this article, I will describe a small proof of concept project for an idea which has been floating around for a long time, but doesn't seem to have a name. I've dubbed it the 'code-first user interface', by analogy with code-first development in Microsoft Entity Framework. Its main goal is to greatly reduce the amount of work required to create a program's user interface.

I need to apologize here for the incompleteness and inconsistencies in the code. I've been working on it for ages during my limited spare time, so I felt I had to get the article out before I died of old age. Also I'd like to get some early feedback, to see if this idea is worth pursuing. Please bear with me.

But I'm going to start with some complaints...

Don't Repeat Yourself

The impetus behind this is simply to reduce the amount of code required to implement a user interface. Currently, UI development seems to entail writing the same thing in multiple ways. For instance, recently I had to create a small test program as part of my professional work. It was a WPF application to display and edit items in a list. I've recreated a simplified version of it here. I started with this class like this:

[Serializable]
public class ProductItem: Notifier
{
    public ProductItem() { }
    public ProductItem(string number, ProductTypeEnum type)
    {
        _productNumber = number;
        _productType = type;
    }
    private string _productNumber;
    public string ProductNumber { get { return _productNumber; } 
	set { SetProperty(ref _productNumber, value); } }
    private ProductTypeEnum _productType;
    public ProductTypeEnum ProductType { get { return _productType; } 
	set { SetProperty(ref _productType, value); } }
    private string _title;
    public string Title { get { return _title; } set { SetProperty(ref _title, value); } }
    private decimal _price;
    public decimal Price { get { return _price; } set { SetProperty(ref _price, value); } }
    private int _stockLevel;
    public int StockLevel { get { return _stockLevel; } set { SetProperty(ref _stockLevel, value); 
    } 
}

I needed to display a list of these, so I had to define a list view in XAML, and bind it to my objects properties:

<listview name="listView1" grid.row="1" itemssource="{Binding}"
mousedoubleclick="listView1_MouseDoubleClick" keyup="listView1_KeyUp" allowdrop="True">
    <listview.view>
        <gridview>
            <gridviewcolumn header="Product Number" displaymemberbinding="{Binding ProductNumber}" />
            <gridviewcolumn header="Type" displaymemberbinding="{Binding ProductType}" />
            <gridviewcolumn header="Title" displaymemberbinding="{Binding Title}" />
            <gridviewcolumn header="Price" displaymemberbinding="{Binding Price}" />
            <gridviewcolumn header="Stock Level" displaymemberbinding="{Binding StockLevel}" />
        </gridview>
    </listview.view>
</listview>

This gives me a form like this:

I also needed a form to create and edit the items:

<window x:class="ProductList.ProductItemForm"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        xmlns:c="clr-namespace:ProductList"
        title="Product Item" width="478.373" height="194.895"
    minwidth="478" sizetocontent="WidthAndHeight">
<window.resources>
    <objectdataprovider methodname="GetValues"
                    objecttype="{x:Type System:Enum}"
                    x:key="ProductTypeEnumValues">
        <objectdataprovider.methodparameters>
            <x:type typename="c:ProductTypeEnum" />
        </objectdataprovider.methodparameters>
    </objectdataprovider>
</window.resources>
<stackpanel name="stackPanel1" minwidth="250">
    <grid minwidth="250" height="115">
        <grid.rowdefinitions>
            <rowdefinition height="27" />
            <rowdefinition height="27" />
            <rowdefinition height="27" />
        </grid.rowdefinitions>
        <grid.columndefinitions>
            <columndefinition width="153" />
            <columndefinition width="370*" minwidth="100" />
        </grid.columndefinitions>
        <label grid.row="0" x:name="label10"
        margin="10,0,38,0" content="Product Number" />
        <textbox grid.row="0" grid.column="1" margin="11,4,19,2"
    x:name="ProductNumTextBox" text="{Binding ProductNumber,
    UpdateSourceTrigger=PropertyChanged}" />
        <label grid.row="1" x:name="EventTypeText"
        margin="10,0,38,0" content="Type" />
        <combobox grid.row="1" margin="9,3,21,3" x:name="ProductType"
    selectionchanged="EventType_SelectionChanged"
    selectedvalue="{Binding ProductType}" grid.column="1"
    minwidth="100" itemssource="{Binding Mode=OneWay,
    Source={StaticResource ProductTypeEnumValues}}" />
        <label grid.row="2" x:name="label7"
        margin="10,0,38,0" content="Title" />
        <textbox grid.row="2" margin="9,1,21,5" x:name="TitleTextBox"
    text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" grid.column="1" />
        <label content="Price" horizontalalignment="Left" margin="10,0,0,0"
    grid.row="3" verticalalignment="Top" width="79" height="27" />
        <textbox grid.row="3" margin="9,0,21,6" x:name="TitleTextBox_Copy"
    text="{Binding Price, UpdateSourceTrigger=PropertyChanged}" grid.column="1" />
        <label content="Stock Level" horizontalalignment="Left"
    margin="10,0,0,0" grid.row="4"
    verticalalignment="Top" width="79" height="27" />
        <textbox grid.row="4" margin="10,0,20,6"
        x:name="TitleTextBox_Copy1"
    text="{Binding StockLevel, UpdateSourceTrigger=PropertyChanged}"
    grid.column="1" rendertransformorigin="0.498,2.857" />
   </grid>
        :

Which looks like this:

I feel this is all a bit mechanical and repetitive. And we are repeating the same information - the names of the properties, the type and so on. This is clear if we need to add a new property to the class - we will have to add it in three places, not just one. We have to add it to the class, the XAML for the list and the XAML for the form. We could almost write an algorithm:

for each public property of the class
    add a column to the list view and bind it to the property
    add a label and field to the form, and bind it to the property

I feel we need to apply two of the fundamental principals of programming here:

  • Don't repeat yourself (DRY)
  • If it's repetitive and mechanical, automate it.

Too Many Files

Another problem I have with current user interface development is the amount of 'cruft' it requires. For example, if you fire up Visual Studio Community and create a sample MVC project, here's what you get:

We end up with 191 files in 33 folders, taking up over 9 MB! But the real essence of this application can be described with just a couple of objects - the TodoItem and the TodoItemList in the Model folder. So my question is why do I need to see all these views, controllers, data transfer objects, JavaScript and CSS files? I feel this is making application development more complex than it should be.

The Code-First User Interface

My proposal is that we simplify user interface code by adopting a method similar to code-first development in Microsoft Entity Framework. In the code-first approach, we can concentrate on the design of our 'domain' entities without having to write separate code to map them to database tables. We just code the classes that describe our application and the mapping is done automaticaly, using sensible conventions:

A mapping layer (the Object-Relational Mapper) is responsible for translating our model classes into relational database tables. [This article is a good introduction to the subject: http://msdn.microsoft.com/en-us/data/jj193542.]

The code-first user interface works the same way, but now a mapping layer (the Object-UI Mapper) creates the user interface using 'reflected' information about the model classes:

So we can just code the model classes, and the OUIM is responsible for creating the user interface. If we modify a model class, no further changes are required - the mapping process will recreate the UI. Furthermore, we could create user interfaces for different UI frameworks without having to rewrite most of our application. We just change the OUIM.

The efficacy of this does rely on a few assumptions, however:

  • There is a one-one mapping between model objects and their views.
  • There is a one-one mapping between properties of model objects and either list columns or form fields.
  • We can infer the appearance of the UI by using the type information of the properties and a small number of attributes.

There will be cases were these assumptions do not hold, but in my experience this covers a large swathe of an application's user interface.

Existing 'Code-First UI' Systems

There are a couple of existing examples of this approach that you may already be familiar with: the Windows Forms Property Grid and Dynamic Data Entities.

Property Grid

Here's what we get if we drop a Windows Forms PropertyGrid onto a form and bind it to a ProductItem object, as defined above:

We have very quickly generated a user interface to edit our object, but I think we could fairly describe it as 'Spartan'. We can edit all the fields, but we are stuck with just a text box for most of them.

Dynamic Data Entities

Dynamic Data Entities for ASP.NET allows us to quickly create a user interface from a data model. We just define this:

public class ProductModel : DbContext
{
    public ProductModel()
        : base("name=ProductModel")
    {
    }
    public virtual DbSet<ProductItem> Products { get; set; }
}

Then, we automatically get a list view and an editing form, like this:

This is exactly the kind of thing we want - the user interface has been generated for us just using the type information in the model. But:

  • It only works for the web
  • We still don't get those up/down controls for the numeric fields.
  • There are still lots of additional files added to the project that we probably don't need to see:

Sample Implementation

To demonstrate the code-first approach I've implemented a small demonstration project, which you can download using the links at the top of the article. In this section, I'll go through some of the major features of the object-UI mapping. For starters though, here's what we get if we use it to display a Product object as defined above. The code required to do this will be trivial:

ProductItem product = new ProductItem();
UI.ShowDialog(product);

This is the result (I've added some more fields in this incarnation):

Hopefully, this would be more acceptable to an end-user than the property grid shown above.

Now I'll present some examples of the code to UI mapping in more detail.

Simple Text Field

The simplest case is just a string property without any additional attributes:

public string simpleTextField { get; set; }

Result:

We get a label and a single-line TextBox. Note that we have not had to specify the label to use for the control - it has been derived from the property name by splitting it on each capital letter. So if our properties follow the 'camelback' naming convention, we will get acceptable labels.

Only Public Properties are Shown

Private and static properties are ignored:

private string privateProperty { get; set; }
public static string staticProperty { get; set; }

Result:

No control is displayed for these properties.

Overriding the Label

We can override the label by using the DiplayName attribute:

[DisplayName("Another text field")]
public string simpleTextField2 { get; set; }

Result:

Masks

We can specify a mask for a text field:

[Mask("(LLL) 000-0000")]
public string maskedTextField { get; set; }

Result:

Numeric Fields

Numeric fields are shown with an up-down control:

public int integerField { get; set; }
public decimal decimalField { get; set; }
public double doubleField { get; set; }

Result:

Selecting the Control

You can override how a property is displayed with the Display attribute:

[Display(ControlType.TextBox)]
public int another_integer { get; set; }

Result:

Setting the Range and Step

You can define the range and increment of a numeric field with attributes:

[CodeFirstUIFramework.Range(40.0, 200.0), CodeFirstUIFramework.Increment(0.1)]
public float beatsPerMinute { get; set; }

Result:

Booleans

Booleans are shown as a check box:

public bool booleanField { get; set; }

Result:

Enums

Enumerations are shown as combo boxes:

public enum TestEnum { Zero, One, Two, Three, Four, Five }
public TestEnum enumField { get; set; }

Result:

Flag Enums

We show a 'flags' field as a set of check boxes:

[Flags]
public enum TestFlags { Olives=1, Pepperoni=2, Capers=4, ExtraCheese=8, Anchovies=16, Chillies=32 }
public TestFlags flagsField { get; set; }

Result:

Date and Time

DateTime fields are shown with a date picker:

public DateTime a_date_time { get; set; }
// We also support a pure Date class:
public Date a_date { get; set; }

Result:

Structs

Given a structure definition like this:

public struct TimeOfDay
{
        public TimeOfDay(UInt16 hours, UInt16 minutes, UInt16 seconds)...
        [Range(0,23)]
        public UInt16 Hours...
        [Range(0, 59)]
        public UInt16 Minutes...
        [Range(0, 59)]
        public UInt16 Seconds...
        public override string ToString()...
}

Then a field of that type will be shown as a text field with a pop-up editor:

public TimeOfDay timeOfDay { get; set; }

Result:

Clicking the button gives us another form:

And here's the field after clicking OK:

We can use attributes to use a group of in-line controls instead:

[Display(LabelOption.Group, ControlType.Inline)]
public TimeOfDay timeOfDay { get; set; }

Which will appear like this:

File Names

Strings with the FileName attribute will display a file picker:

[FileName(Filter = "XML files (*.xml)|*.xml|All files (*.*)|*.*", ForSave = false)]
        public string fileName { get; set; }

Result:

Clicking the button brings up a FilePicker dialog:

After selecting a file and pressing OPEN:

Object References

If there is a list registered for an object, then we will get a list picker:

public Country country { get; set; }

Result:

On clicking the button:

After pressing OK:

Lists

A list of objects will display as an embedded list control:

public List<orderline> _orderLines = new List<orderline>
public List<orderline> OrderLines { get { return _orderLines; } }

Result:

Points of Interest

One tricky point during implementation was getting the semantics right for OK/Cancel. We want the following code to work as expected:

if(  UI.ShowDialog(product) )
{
    //The user clicked OK - product has been updated
}
else
{
    // The user clicked cancel - product is unchanged
}

There are a number of ways we could achieve this:

  • Data is only copied from the controls to the object properties when OK is clicked (effectively not using binding).
  • Bind the controls to the properties, but somehow restore the state of the object on cancel.
  • Create a clone of the object and bind to that. On OK click, copy the clone back to the original object.

We want to use proper dynamic binding, as we want any 'calculated' fields to update as editing progresses.

In the end (after various experiments), I chose the last option. But this presented a problem - there is no simple way to clone an object in C# if you do not know its type. This does not work:

object clone = objectToCopy.MemberwiseClone();

This will not compile, as MemberwiseClone() is a protected member. In the end, I had to resort to this:

public static object CloneObject(object objectToCopy)
{
    //object clone = objectToCopy.MemberwiseClone();        // won't compile
    object returnValue = null;
    Type t = objectToCopy.GetType();
    if (t.IsSerializable)
        returnValue = CopyBySerialization(objectToCopy);
    else
        returnValue = CopyFields(objectToCopy);
    return returnValue;
}

private static object CopyFields(object objectToCopy)
{
    Type t = objectToCopy.GetType();
    object targetObject = Activator.CreateInstance(t);
    // now copy the fields:
    CopyFields(objectToCopy, targetObject);
    return targetObject;
}

private static void CopyFields(object objectToCopy, object targetObject)
{
    Type t = objectToCopy.GetType();
    if (targetObject.GetType() != t)
        throw new ArgumentException("Trying to copy fields of incompatible types");
    var fields = t.GetFields(BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic);
    foreach (FieldInfo info in fields)
    {
        object value = info.GetValue(objectToCopy);
        info.SetValue(targetObject, value);
    }
}

private static object CopyBySerialization(object objectToCopy)
{
    MemoryStream memoryStream = new MemoryStream();
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    binaryFormatter.Serialize(memoryStream, objectToCopy);
    memoryStream.Position = 0;
    object returnValue = binaryFormatter.Deserialize(memoryStream);
    memoryStream.Close();
    memoryStream.Dispose();
    return returnValue;
} 

If anyone knows of a better way, I'd like to hear about it.

Future Development

There are numerous ways this code could be improved and developed. Here are some I've thought of so far:

  • Different UI frameworks. I've implemented for Windows Forms, as that's what I have most familiarity with. But it would be good if we could also create an interface for the web, and for the new Microsoft 'Universal Apps'.
  • Actions. Currently we only support editing data. It would be good to extend the interface generation to support actions. For instance, public methods marked with an [Action] attribute could be used to add a button to the form, which calls the method when clicked.
  • Attributes. At the moment, the code is only interpreting its own attributes and a few from System.ComponentModel. It should be extended to garner useful information from System.ComponentModel.DataAnnotations.
  • Groups. If properties have a group attribute, we could perhaps put them on separate tabs.

History

  • 1.0.0 | Initial Release | 09 Aug 2015

License

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

Share

About the Author

Keith Barrett
Software Developer (Senior) Imagine Communications
United Kingdom United Kingdom
I have been working in IT for 40 years, in various roles from junior programmer to system architect, and with many different languages and platforms. I have written shedloads of code.

I now live in Bedfordshire, England. As well as working full time I am the primary carer for my wife who has MS. I am learning to play the piano. I have three grown up children and a cat.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
PraiseExcellent Work! Pin
onelopez9-Mar-18 6:15
memberonelopez9-Mar-18 6:15 
GeneralMy vote of 5 Pin
Ehsan Sajjad14-Oct-16 11:25
mvpEhsan Sajjad14-Oct-16 11:25 
SuggestionYour question about objects copied members Pin
InvisibleMedia12-Sep-15 5:41
memberInvisibleMedia12-Sep-15 5:41 
QuestionGood work Pin
Santhakumar Munuswamy @ Chennai11-Sep-15 22:55
professionalSanthakumar Munuswamy @ Chennai11-Sep-15 22:55 
QuestionPut this project on GitHub... Pin
Matt Slay10-Aug-15 18:17
memberMatt Slay10-Aug-15 18:17 
AnswerRe: Put this project on GitHub... Pin
Keith Barrett12-Aug-15 7:14
memberKeith Barrett12-Aug-15 7:14 
GeneralMy vote of 5 Pin
Maxwolf Goodliffe10-Aug-15 12:31
memberMaxwolf Goodliffe10-Aug-15 12:31 
QuestionThird Party DataForms Pin
TheDuck6110-Aug-15 8:29
memberTheDuck6110-Aug-15 8:29 
QuestionMessage Closed Pin
10-Aug-15 7:27
memberMarc Greiner at home10-Aug-15 7:27 
AnswerRe: My vote of 5 Pin
Keith Barrett10-Aug-15 8:32
memberKeith Barrett10-Aug-15 8:32 
GeneralMy vote of 5 Pin
Mike Barthold9-Aug-15 10:49
professionalMike Barthold9-Aug-15 10:49 
SuggestionLabel localization Pin
LOST_FREEMAN9-Aug-15 13:29
memberLOST_FREEMAN9-Aug-15 13:29 
GeneralRe: Label localization Pin
Keith Barrett10-Aug-15 3:59
memberKeith Barrett10-Aug-15 3:59 
QuestionSource files cannot be downloaded Pin
fredatcodeproject9-Aug-15 10:01
professionalfredatcodeproject9-Aug-15 10:01 
AnswerRe: Source files cannot be downloaded Pin
Keith Barrett9-Aug-15 10:18
memberKeith Barrett9-Aug-15 10:18 
GeneralRe: Source files cannot be downloaded Pin
fredatcodeproject10-Aug-15 3:47
professionalfredatcodeproject10-Aug-15 3:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.181111.1 | Last Updated 9 Aug 2015
Article Copyright 2015 by Keith Barrett
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid