Wednesday, July 22, 2009

How to use INotifyPropertyChanged, the type-safe way (no magic string)

Implementation of the INotifyPropertyChanged interface is quite simple. There is only one event to implement. Take for example the following simple model class:

public class Model : INotifyPropertyChanged
{
    private string _data;

    public string Data
    {
        get { return _data; }
        set
        {
            if (_data == value)
                return;

            _data = value;

            // Type un-safe PropertyChanged raise 
            PropertyChanged(this, new PropertyChangedEventArgs("Data"));
        }
    }

    #region Implementation of INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged = null;

    #endregion
}

This is a pretty standard way to implement a bindable property. The problem here is the “Data” string to specify which property changed. If someone change the name of the property without changing the content of the string, the code will compile fine but won’t work. In a big application with may properties it can be hard to detect and find the problem.

The best solution is to rely on the compiler to warn us. But because the property name is a string it can’t. So let’s change that line with a type-safe one.

public class Model : INotifyPropertyChanged
{
    private string _data;

    public string Data
    {
        get { return _data; }
        set
        {
            if (_data == value)
                return;

            _data = value;

            // Type safe PropertyChanged raise
            PropertyChanged.Raise(() => Data);
        }
    }

    #region Implementation of INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged = null;

    #endregion
}

What is the trick? Raise is an extension method that takes a lambda expression to specify the name of the property in a type safe way. The Raise method resolve this expression to extract the name of the property and pass it to the PropertyChanged event.

public static class PropertyChangedExtensions
{
    public static void Raise(this PropertyChangedEventHandler handler, Expression<Func<object>> propertyExpression)
    {
        if (handler != null)
        {
            // Retreive lambda body
            var body = propertyExpression.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("'propertyExpression' should be a member expression");

            // Extract the right part (after "=>")
            var vmExpression = body.Expression as ConstantExpression;
            if (vmExpression == null)
                throw new ArgumentException("'propertyExpression' body should be a constant expression");

            // Create a reference to the calling object to pass it as the sender
            LambdaExpression vmlambda = Expression.Lambda(vmExpression);
            Delegate vmFunc = vmlambda.Compile();
            object vm = vmFunc.DynamicInvoke();

            // Extract the name of the property to raise a change on
            string propertyName = body.Member.Name;
            var e = new PropertyChangedEventArgs(propertyName);
            handler(vm, e);
        }
    }
}

All you have to do is to put this extension method in your code and the jib is done. Of course at the end a string will be used to raise the PropertyChanged event but because you don’t have to type it, you don’t have to maintain it.

5 comments:

pieterbreed said...

This is a very neat trick, rename refactor will probably catch this as well?

I'm worried about the reflection cost in vmlambda.Compile(). Will caching work here?

Eric De C# said...

Maybe this can be improve to keep a cache but in most enterprise application the cost is negligible.

Of course most refactoring tool will catch it, but what if someone change a property name without refactoring? It will compile but won't work.

Stefan Lange said...

Very good idea! After I tried it out I have two hints:

If the property is a value type you need an additional step to unbox the expression body:

((MemberExpression)((UnaryExpression)propertyExpression.Body).Operand).Member.Name

The only reason why you must compile and execute the expression is to retrieve the sender. I would avoid this completely by simply adding the sender to the parameters:

PropertyChanged.Raise(this, () => Data);

I think adding the sender is better than creating code to retrieve it.

anup said...

Superb article. I really expect more from you.

tomsundev said...

Unfortunatly use of expression tree's is very time expensive. Here's a solution without the use of expression trees. In the reference post you'll find a performance comparsion. http://tomsundev.wordpress.com/category/articles/update-property-change-notification-without-strings/