Best implementation of INotifyPropertyChange ever

Back in May 2008, I found a way to use Linq expression to fire notification change, and just after that on September 2008, changing the first implementation to benefit extension methods, things looked a lot easier and more C# 3.0 style. But now to think of it, the best implementation of INotifyPropertyChange would be not to implement it at all! Is this possible?

If you're a WPF or Silverlight programmer, you probably aready know that to make data binding work on simple POCO objects, you need to implement INotifyPropertyChange interface on your POCO class. This is a trivial interface which has a public event. When property value changes, it is your job to notify interested parties about this change and WPF / SL engine is one of the interested parties and will update the UIElement that is bound to that property automatically. How to do that is actually easy too, but a lot of repetitive boiler-plate code should be written:

public class Customer
{
}

public class CustomerViewModel : INotifyPropertyChanged
{
    private Customer _currentCustomer;
    public virtual Customer CurrentCustomer
    {
        get { return _currentCustomer; }
        set
        {
            _currentCustomer = value;
            RaisePropertyChanged("CurrentCustomer");
        }
    }

    private void RaisePropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if(handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Yuck! Even using LINQ and Extension methods just changes the untyped property name to its typed equivalent but the amount of code written hardly changes.

Let's see how we can just create an automatic property and make it notify the property change event, without doing all these stuff. In short, let us see how to make this red test turn green:

[TestFixture]
public class AutomaticNotificationFixture
{
    [Test]
    public void Notification_Change_Is_Added_Automatically()
    {
        var vm = new CustomerViewModel();

        var notification = vm as INotifyPropertyChanged;

        Assert.That(notification, Is.Not.Null);
    }

    [Test]
    public void Can_Notify_Automatically()
    {
        var vm = new CustomerViewModel();
        string changedProperty = null;

        ((INotifyPropertyChanged) vm).PropertyChanged += (s, e) => changedProperty = e.PropertyName;
        vm.CurrentCustomer = new Customer();

        Assert.That(changedProperty, Is.Not.Null);
        Assert.That(changedProperty, Is.EqualTo("CurrentCustomer"));
    }

    public class Customer
    {
    }

    public class CustomerViewModel
    {
        public virtual Customer CurrentCustomer
        {
            get; set;
        }
    }
}

Not possible at first look, eh? If you're familiar with DynamicProxy tools, you can well see the perfect usage here. In short, using a dynamic proxy library, we could implement an interface on our type at runtime, so we could inject the INotifyPropertyChange behavior into our ViewModel here. This is a pretty powerful toy which allows you any kind of behavior into your objects with minimal effort.

Let's create a ViewModelFactory for ourselves that just does that:

public class ViewModelFactory
{
    private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();

    public static T Create<T>()
    {
        return (T)Create(typeof(T));
    }

    public static object Create(Type type)
    {
        return ProxyGenerator.CreateClassProxy(type, new[]
        {
            typeof(INotifyPropertyChanged), 
        }, 
        new NotifyPropertyChangedInterceptor());
    }
}

We need to create an Interceptor for our behavior too. Interceptor as the name implies, will be called on our class at runtime and gives us a chance to run a piece of code before and / or after a call is made on our object.

public class NotifyPropertyChangedInterceptor : IInterceptor
{
    private PropertyChangedEventHandler _subscribers = delegate { };

    public void Intercept(IInvocation invocation)
    {
        if (invocation.Method.DeclaringType == typeof(INotifyPropertyChanged))
        {
            HandleSubscription(invocation);
            return;
        }

        invocation.Proceed();

        if (invocation.Method.Name.StartsWith("set_"))
        {
            FireNotificationChanged(invocation);
        }
    }

    private void HandleSubscription(IInvocation invocation)
    {
        var handler = (PropertyChangedEventHandler)invocation.Arguments[0];

        if (invocation.Method.Name.StartsWith("add_"))
        {
            _subscribers += handler;
        }
        else
        {
            _subscribers -= handler;
        }
    }

    private void FireNotificationChanged(IInvocation invocation)
    {
        var propertyName = invocation.Method.Name.Substring(4);
        _subscribers(invocation.InvocationTarget, new PropertyChangedEventArgs(propertyName));
    }
}

As you can see, the IInvocation interface contains the method that is being executed so we can see if it is an event subscription, a property getter/setter or a method call and act respectivly.

Now we only need to create our ViewModel classes via our factory so let's refactor our test code to this and see the tests pass.

var vm = ViewModelFactory.Create<CustomerViewModel>();

How does this fit to your application? It goes without saying that you can not create instances of your ViewModels and need to get new instances via this factory, but if you're using a presentation framework like Caluburn, or using dependency injection containers like Windsor to resolve your VMs, you'll feel right at home.

So, what's the catch? There's always a catch, you might ask? There's no catch seriously. Well, now that you insist, there is just one point. To be able to do so, you need to make your properties "virtual" as the sample above. I don't consider this a "catch" since making your public API virtual is considered a good thing for many reasons and you probably do that anyway.

Comments

comments powered by Disqus