Tuesday, 8 June 2010

Automatic INotifyPropertyChanged with DynamicObject

Implementing the INotifyPropertyChanged interface is Microsoft’s recommended mechanism for providing notifications to update WPF data bindings.

Normally, I like to use my code snippets to implement INotifyPropertyChanged on classes I’m writing, but what if I want to use data binding on a class that I didn’t write and which doesn’t support INotifyPropertyChanged?

In this post, I show how to use the DynamicObject class introduced in .Net 4 to create a proxy that wraps access to another object’s properties while adding automatic support for INotifyPropertyChanged.

My class NotifyPropertyChangedProxy is derived from DynamicObject and overrides three methods to dynamically provide access to the properties of a wrapped object.

I think this example shows that DynamicObject has huge potential for dynamically generating many different types of proxies.

There is a caveat when implementing INotifyPropertyChanged using a proxy like this. The PropertyChanged event will only be raised if a property is set through the proxy. Setting the property using the wrapped object directly won’t produce any notification event.

Download the source code here: NotifyPropertyChangedProxy.cs

Using NotifyPropertyChangedProxy

As an example, let’s take a simple class with automatically implemented properties:

public class Person
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public int Age { get; set; }
}

To create a proxy for this class, simply construct an instance of NotifyPropertyChangedProxy, passing the constructor a reference to the Person instance to be wrapped:

dynamic myPerson = new NotifyPropertyChangedProxy( new Person() );

The most likely use for NotifyPropertyChangedProxy is as a data binding source:

personGrid.DataContext = myPerson;

Having set the proxy object as the DataContext, we can create bindings using the property names of the wrapped object:

<TextBox Text="{Binding Path=FirstName}"/>

Using the new dynamic C# keyword (see the declaration of myPerson above), we can also set the properties in code via the proxy:

myPerson.FirstName = "John-Martin";
myPerson.LastName = "Malone";
myPerson.Age = 39;

These property changes will cause the PropertyChanged event to be raised, and the data bound UI elements will be updated.

A walk through NotifyPropertyChangedProxy


public class NotifyPropertyChangedProxy : DynamicObject, INotifyPropertyChanged
{

    public object WrappedObject { get; private set; }

    public NotifyPropertyChangedProxy(object wrappedObject)
    {
        if (wrappedObject == null)
            throw new ArgumentNullException("wrappedObject");

        WrappedObject = wrappedObject;
    }

The class inherits from DynamicObject and implements INotifyPropertyChanged.

The WrappedObject property is automatically implemented and has a private setter which is used in the constructor.

#region INotifyPropertyChanged support

protected virtual void OnPropertyChanged(string propertyName)
{
    var handler = PropertyChanged;
    if (handler != null)
    {
        handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public event PropertyChangedEventHandler PropertyChanged;

#endregion

The boilerplate OnPropertyChanged method and PropertyChanged event were generated with my code snippets.

public override IEnumerable<string> GetDynamicMemberNames()
{
    return from f in WrappedObject.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)
        select f.Name;
}

The override of GetDynamicMemberNames is called by the Visual Studio debugger. The implementation calls the GetProperties method on the wrapped object’s Type and uses a straightforward LINQ query to supply the member names.

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    // Locate property by name
    var propertyInfo = WrappedObject.GetType().GetProperty(binder.Name, BindingFlags.Instance | BindingFlags.Public | (binder.IgnoreCase ? BindingFlags.IgnoreCase : 0));
    if (propertyInfo == null || !propertyInfo.CanRead)
    {
        result = null;
        return false;
    }

    result = propertyInfo.GetValue(WrappedObject, null);
    return true;
}

The TryGetMember override calls the GetProperty method on the wrapped object’s Type. If no property is found with a matching name, or if the property doesn’t have a get accessor, the parameter ‘result’  is set to null, and the method returns false. If an appropriate PropertyInfo object is found, its GetValue method is used to set result and the method returns true.

public override bool TrySetMember(SetMemberBinder binder, object value)
{
    // Locate property by name
    var propertyInfo = WrappedObject.GetType().GetProperty(binder.Name, BindingFlags.Instance | BindingFlags.Public | (binder.IgnoreCase ? BindingFlags.IgnoreCase : 0));
    if (propertyInfo == null || !propertyInfo.CanWrite)
        return false;

    object newValue = value;
    // Check the types are compatible
    Type propertyType = propertyInfo.PropertyType;
    if (!propertyType.IsAssignableFrom(value.GetType()))
    {
        newValue = Convert.ChangeType(value, propertyType);
    }

    propertyInfo.SetValue(WrappedObject, newValue, null);
    OnPropertyChanged(binder.Name);
    return true;
}

The TrySetMember override starts in a similar way to TryGetMember, returning false if a PropertyInfo object cannot be retrieved for the specified name, or if the property found doesn’t have a set accessor. Next, we check whether the Type of the value parameter is compatible with the Type of the property. If not, Convert.ChangeType is used to convert to the required type. Finally, PropertyInfo.SetValue is used to apply the new value, OnPropertyChanged is called to fire the PropertyChanged event, and the method returns true.

Convert.ChangeType will throw an exception if the conversion cannot be performed. The documentation for DynamicObject.TrySetMember states that the method should return false if the operation is unsuccessful, so my first draft of this override caught these exceptions and returned false from within the exception handler. With this approach, WPF’s data binding reports (via the Visual Studio debug output):

Property path is not valid. 'System.Dynamic.DynamicObject+MetaDynamic' does not have a public property named 'Age'.

That’s not a very helpful message, since the property does exist. The code shown above allows any exceptions thrown by ChangeType to be handled further up the stack, in which case a more useful message appears in the debugger.

2 comments:

  1. I did something very much the way you have implemented, I recommend do a cache of the public properties of the wrapped object to increase performance.

    ReplyDelete
  2. https://gist.github.com/2730528

    ReplyDelete