Поделиться через


UI without code or XAML: PropertyGrid, DataForm, etc.

WPF has certainly advanced the state-of-the-art in programming the UI. I personally think that WPF and Silverlight remove all the limits that existed to UI development in the past. The only limit now is your imagination. XAML is there for declarative machine-friendly UI descriptions, data-binding is for easier logic of syncing the UI to the data, automatic layout saves you all the trouble of aligning controls yourself, and so on.

One thing that I notice though is that if your goal is a simple dialog or a wizard page, one is likely to write more code than is absolutely necessary, be it in a .NET language or XAML. For instance, one of the most trendiest patterns nowadays, MVVM, suggests that you have a model, a ViewModel (a UI-aware wrapper around model classes) and a View (that is databound to ViewModel). It is also suggested that you write some XAML to achieve what you need.

My personal impression is that UI development nowadays has become complicated. It's super powerful, yes, but I find it is hard to find a simple solution for a simple scenario.

For the purpose of this article, suppose that we need to implement a person editor dialog, and our business logic describes a person like this:

 public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

One is now likely to open XAML and start adding a Grid or a StackPanel with a couple of TextBoxes and databind them to the properties of the Person class. This approach is fine with me in general (since it decouples the UI from the domain model), however what I find is that it's just complicated and for simple scenarios might be an overkill.

One other approach is often forgotten.

PropertyGrid

WinForms introduced an excellent control which unfortunately is probably less known and less used than it deserves: the PropertyGrid. You just add this control to your form and you just say:

 propertyGrid1.SelectedObject = joe;

and you get:

image

What happens is that property grid reflects over the "editable" properties on the object, and displays each property as a separate row in the grid. Depending on the type of the property, it's value can be editable, additional controls can be displayed, for example, a color picker for colors, a calendar for DateTime, a checkbox for bool, etc. The nice thing about the design of the property grid (and I do think that the WinForms PropertyGrid is a very well designed control) is that if you have your own type, then you can also provide your own editor control for properties of that type.

Also, the property grid provides certain other services - grouping properties into collapsible categories, description for selected property, multi-object selection (which enables setting a property on multiple objects simultaneously), etc.

From the design perspective, what the property grid gives you is the ability to not duplicate the description of the UI. In the XAML example above you essentially describe your UI twice: first in your domain model (Name, Age) and then in your XAML (TextBox, TextBox) and then you bind the two together. With a property grid, on the contrary, you just declare your UI once, in your domain model, and you don't have to write any UI at all - property grid generates the UI for you.

Unfortunately, this promising approach didn't go much further in the WinForms world, probably because of the limitations of the WinForms architecture (you couldn't re-style the property grid editors, rearrange rows or significantly customize its appearance (besides choosing the background and foreground colors and fonts).

Silverlight 3 DataForm

Thankfully, Silverlight 3 starts to fill the gap in the direction of eliminating the UI code by introducing the DataForm control: you can watch a great video by Mike Taulty about the DataForm here: https://beta.silverlight.net/learn/videos/silverlight-videos/dataform-control/ (and by the way, Mike Taulty makes excellent, excellent videos, highly recommended).

Silverlight 3 DataForm comes with the free Silverlight Toolkit (https://silverlight.codeplex.com), is located in the assembly System.Windows.Controls.Data.DataFormToolkit.dll (GAC'ed) and works almost exactly like the WinForms PropertyGrid:

image

which results from this code:

 dataForm.CurrentItem = joe;

I'm not aware of such a control for WPF but I haven't actually looked. All I know that it's a cool and promising approach which makes UI programming simpler, especially if you're designing dialogs, wizard pages or property grids.

My own experiments

Before Silverlight 3 came out, I was looking for an elegant way to implement property editing for the geometric figures in Live Geometry:

image

Since I didn't want to write any XAML for every property of every figure, I decided that the PropertyGrid design was a perfect fit, so I just implemented my own Property Grid (The benefit of having a hobby project is that you can use it as a playground and sandbox for experimenting with new ideas and approaches).

The API is almost the same as the ones mentioned above:

 PropertyGrid.Show(this.Style, this.Drawing.ActionManager);

gives you this:

image

Unlike the previous ones though, apart from the actual object being edited you can pass the second argument - a pointer to the ActionManager from my Undo Framework, which will make sure all the edits are recorded in the Undo history and can be undone.

Metadata

An important decision in the design of my property grid was to abstract away from modeling UI elements as CLR metadata. Both PropertyGrid and DataForm use CLR metadata (properties and their types) to encode what controls do we want to show on the form. Although metadata is very good for this (especially enriched with the .NET attributes as additional declarative mechanism, such as the [Description] attribute), I wanted to abstract away how the UI intent is declared, just like MEF can abstract away how a contract is specified. This was also inspired by the introduction of ICustomTypeDescriptor that allows extensibility of the PropertyGrid along the dimension of the property set. I didn't like metadata as the host language for the declaration of my UI elements, neither I liked XAML, for various reasons. So I came up with a basic interface called IValueProvider that encapsulates any value, be it a value of a CLR property or a value of a method's parameter, or anything else that might come up in the future. Here's a rough sketch (design not finalized yet):

 public interface IValueProvider
{
    event Action ValueChanged;
    void RaiseValueChanged();
    T GetValue<T>();
    bool CanSetValue { get; }
    void SetValue<T>(T value);
    object Parent { get; }
    Type Type { get; }
    string Name { get; }
    string DisplayName { get; }
    T GetAttribute<T>() where T : Attribute;
    IEnumerable<T> GetAttributes<T>() where T : Attribute;
}

This gives me extensibility along the dimension of how UI elements are declared.

Discovery

To solve the task of taking content (think: an object) and extracting a set of editable values from it (think: properties), I introduced a so-called ValueDiscoveryStrategy. IncludeByDefaultValueDiscoveryStrategy will include all the properties unless they're marked with the [Ignore] attribute, and ExcludeByDefaultValueDiscoveryStrategy will exclude all the properties unless they're marked with the [PropertyGridVisible] attribute. Right now the discovery strategies only support CLR metadata (object properties and method parameters), but in the future nothing prevents me from adding more strategies to create editable values off of something else, say columns of a table in a database schema. This gives me the extensibility along the dimension of what properties to show in the grid.

As an example, here's the ShapeStyle class that is being displayed in the screenshot above:

 public class ShapeStyle : LineStyle
{
    public override FrameworkElement GetSampleGlyph() {...}

    Color mFill = Colors.Yellow;
    [PropertyGridVisible]
    public Color Fill
    {
        get
        {
            return mFill;
        }
        set
        {
            mFill = value;
            OnPropertyChanged("Fill");
        }
    }

    public override Style GetWpfStyle() {...}
}

The Fill property comes in here, and Color and Stroke width are inherited from LineStyle.

Editors

An editor is essentially a visual control that can edit an IValueProvider. For different types of values there are different kinds of editors. It's just a textbox for a string property, a checkbox for bools, a color picker for Color, etc. The nice thing (again, borrowed from the WinForms property grid design) is that the pool of editors is extensible. I use homemade dependency injection to discover all types of editors available for a type and instantiating a proper editor for a proper type. This gives me extensibility along the dimension of what types of properties can I edit.

Also at this point kudos go to SilverlightContrib folks for an excellent ColorPicker control that I reused in my app! Great job and many thanks! BTW I fixed two bugs in it which I think I should submit as patches... need to remind myself later...

Anyway, here's all it took to add an editor type for properties of type System.Windows.Color:

 public class ColorEditorFactory 
    : BaseValueEditorFactory<ColorEditor, Color> { }

public class ColorEditor : LabeledValueEditor, IValueEditor
{
    public ColorPicker Picker { get; set; }

    protected override UIElement CreateEditor()
    {
        Picker = new ColorPicker();
        Picker.SelectedColorChanging += ColorChanged;
        return Picker;
    }

    void ColorChanged(object sender, SelectedColorEventArgs e)
    {
        SetValue(e.SelectedColor);
    }

    public override void UpdateEditor()
    {
        Picker.SelectedColor = GetValue<Color>();
        Picker.IsEnabled = Value.CanSetValue;
    }
}

Complex types

There are two approaches for displaying properties of complex types: either expanding them in place just like a tree view item gets expanded (and collapsed), or providing a hyperlink that, when clicked, will display that object's properties in the PropertyGrid, instead of the current one. In the latter case, one will probably want to implement some sort of a back button that will display the original object.

Method Caller Buttons

Finally, why not move it one step further? If we can get and set properties using this property grid, why not support calling methods? Here's a screenshot again of the polygon properties - note the three buttons at the bottom. They're generated too!

image

Here's the full source for the buttons:

 [PropertyGridVisible]
public virtual void Delete() { ... }

As you see, just add the [PropertyGridVisible] on any method and it will appear in the UI. Obviously, pushing the button will call the method.

 [PropertyGridVisible]
[PropertyGridName("Create new style")]
public void CreateNewStyle()
{
    Drawing.ActionManager.SetProperty(this, "Style", 
        Drawing.StyleManager.CreateNewStyle(this));
    EditStyleButton();
}
 [PropertyGridVisible]
[PropertyGridName("Edit style")]
public void EditStyleButton()
{
    if (PropertyGrid != null)
    {
        PropertyGrid.Show(this.Style, this.Drawing.ActionManager);
    }
}

The next logical step is to allow calling methods with arguments. For example, having this method on your type:

 [PropertyGridVisible]
public void AddEntry(Person person, DayOfWeek day, bool check)
{
}

will result in this UI:

image

Clicking the button will call the method and pass the current values as arguments. This button is implemented as a simple button with PropertyGrid content that displays MethodInfo's parameters instead of object's property values (it uses a different ValueDiscoveryStrategy called ParameterDiscoveryStrategy).

Summary

Using this approach we can build a UI library for calling APIs. You design an API and you get a (basic) UI for it for free. This library will allow us to inspect any object at runtime similar to how debugger watch windows allow us to do it. Moreover, we can not only inspect it, but also set the property values and call methods on objects. This might prove useful not only for dialogs, wizard pages, property browsers, but possibly in other scenarios where you're editing a data structure directly and don't really care about customizing the default UI.

I'm currently planning to continue evolving this prototype library as part of the Live Geometry source code, and possibly spawn off a separate project when this library becomes more mature and if there is demand.

For now, if you're interested in the source code, you can find the up-to-date implementation at https://livegeometry.codeplex.com/SourceControl/ListDownloadableCommits.aspx -> Main\DynamicGeometryLibrary\PropertyGrid.

Let me know if you have any questions or feedback. Thanks!

Comments

  • Anonymous
    September 23, 2009
    The comment has been removed
  • Anonymous
    September 25, 2009
    Wow, I didn't know about this: http://www.nakedobjects.org Nice tip John!