Thursday, April 1, 2010

Intercepting INotifyPropertyChanged


Using custom attributes and binding to hook into another object's property change events.


I have a love / hate relationship with INotifyPropertyChanged (INPC). I love how it makes binding in Silverlight and WPF work so well. I hate how I have to dirty up my beautiful automatic properties. I spent many hours trying to make INPC work with automatic properties and I did succeed using PostSharp but I stopped using it for various reasons. I am not going to talk about that today but something else which is (in some respects) much cooler. [Check out this article for more info on PostSharp and INPC.]


I have been writing frameworks for years and last year I got started with the latest for Silverlight and RIA services. Our devs are doing MVVM and often they will include a RIA object in their ViewModels:

public class CustomerViewModel: ViewModel
{
  private Customer _Customer;
  public Customer Customer
  {
    get { return _Customer; }

    set { _Customer = value; NotifyPropertyChanged(() => Customer); }
  }
}

The problem with this approach is that although Customer implements IPropertyNotifyChanged there is no way to hook into it. Let's say that Customer has FirstName and LastName properties but we need a FullName property in our ViewModel to display in our view. This is a common scenario but very hard to accomplish. As you can see out CustomerViewModel has a FullName but if Customer.FirstName or Customer.LastName changes it will not be reflected on the UI.

public class CustomerViewModel: ViewModel
{
  private Customer _Customer;
  public Customer Customer
  {
   get { return _Customer; }
   set { _Customer = value; NotifyPropertyChanged(() => Customer); }
  }
 
  public string FullName
  {
    get { return Customer.FirstName + " " + Customer.LastName; }
  }
}

What we want to do is somehow intercept the IPNC from Customer and respond to it. We can't do it by hooking into the PropertyNotifyChanged event of Customer since that is not available to us.

How cool would be to be able to this?


public class CustomerViewModel: ViewModel
{
  private Customer _Customer;
  public Customer Customer
  {
   get { return _Customer; }
   set { _Customer = value; NotifyPropertyChanged(() => Customer); }
  }

  [NotifyWhenChanged("Customer.FirstName")]
  [NotifyWhenChanged("Customer.LastName")]
  public string FullName
  {
   get { return Customer.FirstName + " " + Customer.LastName; }
  }

  [CallWhenPropertyChanged("Preferences.State")]
  public void StateChanged(object sender, PropertyChangedEventArgs e)
  {
    Counties = CountyList.GetCountyNames(Preferences.State);
  }
}
The code below does just that. I created two attributes [NotifyPropertyChanged] and [CallWhenPropertyChanged]. The first is for properties and the second one is used on methods. CallWhenPropertyChanged
is great when you have some common code (like calculations) that need to be executed each time a property changes.

namespace Nowcom.Quicksilver
{
  using System;
 
  [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
  public class CallWhenPropertyChangedAttribute : Attribute
  {
    public string PropertyName { get; set; }

    public CallWhenPropertyChangedAttribute(string propertyName)
    {
     PropertyName = propertyName;
      }
  }
} 
namespace Nowcom.Quicksilver
{
  using System;

  [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
  public class NotifyWhenChangedAttribute : Attribute
  {
    public string PropertyName { get; set; }

    public NotifyWhenChangedAttribute(string propertyName)
    {
      PropertyName = propertyName;
    }
   }
}


Now that we have our attributes we need to create a Notification class with a dependency property to hold on to the property we for which we want to get notifications. This is the intercepting engine when given a property will set up a Binding and then call an action.

namespace Nowcom.Quicksilver
{
  using System;
  using System.Windows;
  using System.Windows.Data;

   public class Notification : FrameworkElement
  {
    public Action Action { get; set; }
    public Notification(object owner, string propertyName, Action action)
    {
      Action = action;
      Binding binding = new Binding(propertyName) { Source = owner };

      SetBinding(Notification.PropertyToMonitorProperty, binding);
    }
   
    public object PropertyToMonitor
    {
      get { return GetValue(PropertyToMonitorProperty); }
      set { SetValue(PropertyToMonitorProperty, value); }
    }

    public static readonly DependencyProperty PropertyToMonitorProperty =
      DependencyProperty.RegisterAttached(
       "PropertyToMonitor",
       typeof(object),
       typeof(Notification),
       new PropertyMetadata(OnPropertyToMonitorChanged));

    private static void OnPropertyToMonitorChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
      var notification = dependencyObject as Notification;

      if (notification == null)
        return;

      if (notification.Action != null)
        notification.Action();
    }
  }
}

In our ViewModel we iterate through all of the properties and methods and look for the [NotifyPropertyChanged] and [CallWhenPropertyChanged] and create a Notification which will call our NotifyPropertyChanged or invoke a method.


namespace Nowcom.Quicksilver
{
  using System.Collections.Generic;
  using System.ComponentModel;
  using System.Linq;

  public class ViewModel : PropertyChangedBase
  {
    private IList<Notification> _Notifications = new List<Notification>();
    public ViewModel()
    {
      CreateNotifications();
    }
   
    private void CreateNotifications()
    {
      GetType().GetProperties().ToList().ForEach(property=>
        {
            property.GetCustomAttributes(typeof(NotifyWhenChangedAttribute), false)
        .OfType<NotifyWhenChangedAttribute>()
        .ToList()
        .ForEach(attribute =>
        {
          _Notifications.Add(new Notification(this, attribute.PropertyName, () =>
          {
              NotifyPropertyChanged(property.Name);
          }));
        });
      });
    
      GetType().GetMethods().ToList().ForEach(method =>
      {
        method.GetCustomAttributes(typeof(CallWhenPropertyChangedAttribute), false)
         .OfType<CallWhenPropertyChangedAttribute>()
         .ToList()
         .ForEach(attribute =>
         {
           _Notifications.Add(new Notification(this, attribute.PropertyName, () =>
           {
             method.Invoke(this, new object[] { this, new PropertyChangedEventArg(attribute.PropertyName)
                });
         }));
       });
     });
   }
  }
}


This is the base to the ViewModel which allows you to use a lambda instead of just a string for the parameter for NotifyPropertyChanged which is checked at compile time. Misspelling a property name is an error which is really really hard to find. So I included this base but it is not required to use the Notifications I showed above.

namespace Nowcom.Quicksilver
{
  using System.ComponentModel;
  using System.Linq.Expressions;
  using System;

  public abstract class PropertyChangedBase : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged(string propertyName)
    {
      var handler = PropertyChanged;
      if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
    }
   
    public void NotifyPropertyChanged<TProperty>Expression<Func<TProperty>> property)
    {
      var lambda = (LambdaExpression)property;
      MemberExpression memberExpression;
      if (lambda.Body is UnaryExpression)
      {
        var unaryExpression = (UnaryExpression)lambda.Body;
        memberExpression = (MemberExpression)unaryExpression.Operand;
      }
      else
           memberExpression = (MemberExpression)lambda.Body;
     
      NotifyPropertyChanged(memberExpression.Member.Name);
    }
  }
}


So that's it. We use this in our current project since September and it works really well. I would like to thank Vaibhav Deshpande, one of our developers, who works with me on our framework. This is his idea to replace the PostSharp implementation we had earlier.

1 comment:

  1. There is a missing "(". Here is the correct line: public void NotifyPropertyChanged(Expression> property)

    Nice post anyway. Thanks!

    ReplyDelete