Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C# 5.0

A Code-First User Interface Library

Rate me:
Please Sign up or sign in to vote.
4.84/5 (33 votes)
9 Aug 2015CPOL9 min read 32.9K   894   31   17
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:

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

XML
<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:

Image 2

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

XML
<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:

Image 3

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:

Image 4

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:

Image 5

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:

Image 6

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:

Image 7

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:

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

Image 8

Image 9

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:

Image 10

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:

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

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

Image 11

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:

C#
public string simpleTextField { get; set; }

Result:

Image 12

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:

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

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

Result:

Image 13

Masks

We can specify a mask for a text field:

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

Result:

Image 14

Image 15

Numeric Fields

Numeric fields are shown with an up-down control:

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

Result:

Image 16

Selecting the Control

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

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

Result:

Image 17

Setting the Range and Step

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

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

Result:

Image 18

Booleans

Booleans are shown as a check box:

C#
public bool booleanField { get; set; }

Result:

Image 19

Enums

Enumerations are shown as combo boxes:

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

Result:

Image 20

Image 21

Flag Enums

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

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

Result:

Image 22

Image 23

Date and Time

DateTime fields are shown with a date picker:

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

Result:

Image 24

Structs

Given a structure definition like this:

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

C#
public TimeOfDay timeOfDay { get; set; }

Result:

Image 25

Clicking the button gives us another form:

Image 26

And here's the field after clicking OK:

Image 27

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

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

Which will appear like this:

Image 28

File Names

Strings with the FileName attribute will display a file picker:

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

Result:

Image 29

Clicking the button brings up a FilePicker dialog:

Image 30

After selecting a file and pressing OPEN:

Image 31

Object References

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

C#
public Country country { get; set; }

Result:

Image 32

On clicking the button:

Image 33

After pressing OK:

Image 34

Lists

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

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

Result:

Image 35

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:

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

C#
object clone = objectToCopy.MemberwiseClone();

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

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


Written By
Software Developer (Senior) Imagine Communications
United Kingdom United Kingdom
I have been working in IT since 1975, 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.

Comments and Discussions

 
PraiseExcellent Work! Pin
onelopez9-Mar-18 5:15
onelopez9-Mar-18 5:15 
GeneralMy vote of 5 Pin
Ehsan Sajjad14-Oct-16 10:25
professionalEhsan Sajjad14-Oct-16 10:25 
SuggestionYour question about objects copied members Pin
InvisibleMedia12-Sep-15 4:41
professionalInvisibleMedia12-Sep-15 4:41 
QuestionGood work Pin
Santhakumar Munuswamy @ Chennai11-Sep-15 21:55
professionalSanthakumar Munuswamy @ Chennai11-Sep-15 21:55 
QuestionPut this project on GitHub... Pin
Matt Slay10-Aug-15 17:17
Matt Slay10-Aug-15 17:17 
AnswerRe: Put this project on GitHub... Pin
Keith Barrett12-Aug-15 6:14
Keith Barrett12-Aug-15 6:14 
GeneralMy vote of 5 Pin
Maxwolf Goodliffe10-Aug-15 11:31
Maxwolf Goodliffe10-Aug-15 11:31 
QuestionThird Party DataForms Pin
TheDuck6110-Aug-15 7:29
TheDuck6110-Aug-15 7:29 
QuestionMessage Closed Pin
10-Aug-15 6:27
Marc Greiner at home10-Aug-15 6:27 
AnswerRe: My vote of 5 Pin
Keith Barrett10-Aug-15 7:32
Keith Barrett10-Aug-15 7:32 
GeneralMy vote of 5 Pin
Mike (Prof. Chuck)9-Aug-15 9:49
professionalMike (Prof. Chuck)9-Aug-15 9:49 
SuggestionLabel localization Pin
LOST_FREEMAN9-Aug-15 12:29
LOST_FREEMAN9-Aug-15 12:29 
GeneralRe: Label localization Pin
Keith Barrett10-Aug-15 2:59
Keith Barrett10-Aug-15 2:59 
QuestionSource files cannot be downloaded Pin
fredatcodeproject9-Aug-15 9:01
professionalfredatcodeproject9-Aug-15 9:01 
AnswerRe: Source files cannot be downloaded Pin
Keith Barrett9-Aug-15 9:18
Keith Barrett9-Aug-15 9:18 
GeneralRe: Source files cannot be downloaded Pin
fredatcodeproject10-Aug-15 2:47
professionalfredatcodeproject10-Aug-15 2: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.