Sunday, October 23, 2011

Better, type-safe dependency properties in C#, WPF

WPF is a great toolkit for graphical user interface programming where you can truly (and easily) separate program logic from user interface.
The dynamic and flexible binding mechanism is powerful, yet easy to use (once you grasp it :D). Because of it's dynamic nature, type safety is often an issue. Even the most fundamental constructs, like declaring a property that you can use as a binding target (called DependencyProperty) requires you to use casts. Here as a simple example that declares a DependencyProperty called Number of type int:

using System.Windows;

class ProgramLogic : DependencyObject {

    public static readonly DependencyProperty NumberProperty = DependencyProperty.Register("Number", typeof(int), typeof(ProgramLogic));

    public int Number {
        get { return (int) GetValue(NumberProperty); }
        set { SetValue(NumberProperty, value); }
    }
}
As you can see, there are some constructs that you would usually avoid, such as using typeof and - the single worst construct in every programming language - a cast to int in the property's getter.
While describing this to a friend lately, we said to ourselfs: "Why did they choose this construct? C# does have Generics, so why didn't they use it? Even if it would be just a helper function or class, it'd be much better than that.". So we came up with a wrapper to DependencyProperty, which is a bit shorter, and, most importantly, does not require a cast.
class DepProp <PropertyType, OwnerType> where OwnerType : DependencyObject {

    public DepProp(string name) {
        property = DependencyProperty.Register(name, typeof(PropertyType), typeof(OwnerType));
    }

    private DependencyProperty property;

    public DependencyProperty Property {
        get { return property; }
    }

    public PropertyType Get(OwnerType owner)
    {
        return (PropertyType) owner.GetValue(property);
    }

    public void Set(OwnerType owner, PropertyType value)
    {
        owner.SetValue(property, value);
    }
}
The class DepProp (pick a better name when you decide to use it, please) uses two generic parameters. The first one is the type of the property, the second one is the owner's type, which must be derived from DependencyObject. IMHO, using generic parameters is much better than using typeof. These generic parameters are used to guarantee that later calls to Get() and Set() are called with - and return - the correct types. This way, we made the DepdencyObject.GetValue() call type-safe. Sure, the implementation still uses a cast, but what I find most important is that there are no casts spread throughout the whole project. A neat side-effect is that using this class requires a tiny little less typing than the original DependencyProperty. Another nice feature is that you don't need to call a static member function to register your dependency property. Instead, you use new, just like you would do with any other object. Here is the implementation of ProgramLogic using the new DepProp:
class ProgramLogic : DependencyObject {

    public static readonly DepProp<int, ProgramLogic> NumberProperty = new DepProp <int, ProgramLogic>("Number");

    public int Number {
        get { return NumberProperty.Get(this); }
        set { NumberProperty.Set(this, value); }
    }
}
As we can see, the required code is a little less. But the real nice thing is that the compiler can help you pointing out refactoring errors. Let's imagine that you would want to change your NumberProperty to be of type string instead of int. When you change the generic parameter of your NumberProperty declaration and initialization, the compiler would spill out an error in the "normal" property's getter and setter, because the property is still of type int.
I hope that you find this (really) small helper class useful and that it inspires you to write more code like this that makes your overall codebase more maintainable and readable.

3 comments:

  1. Hey there,

    really nice little helper class, thanks for posting!

    The blog engine seems to have stripped out the type parameters in the last code block, which left me puzzled for a few minutes :)

    ReplyDelete
  2. Ooops - fixed!
    Thanks for pointing it out.

    ReplyDelete
  3. Works wonderfully. Missing additional arguments, which are often crucial like PropertyMetadata. Thank you for sharing this.

    ReplyDelete