Search This Blog

Loading...

Tuesday, March 31, 2009

INotifyPropertyChanged made easier using PostSharp

How many times have you written a C# business class like the one below only to realise that if you want the UI to react to changes you need to implement this cumbersome INotifyPropertyChanged interface.

So the above class will provide no change notification, you’ll have to implement the interface after which your code will look something like this (Note that this is .NET 2.0 syntax):

public class Customer : System.ComponentModel.INotifyPropertyChanged 
{
private String _Name;
private DateTime _DOB;

public String Name
{
get
{
return _Name;
}
set
{
_Name = value;
OnPropertyChanged("Name");
}
}

public DateTime DOB
{
get
{
return _DOB;
}
set
{
_DOB = value;
OnPropertyChanged("DOB");
}
}

#region INotifyPropertyChanged Members

public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}

#endregion
}

That class already looks extremely overloaded and ugly…now imagine if you are able to use .NET 3 or 3.5 and you want to utilise it’s extremely cool feature of auto-implemented properties (e.g. public int Age { get; set; }). Guess what…you can’t!!!! Because where would you put your calls to OnPropertyChanged(…)?

This is where PostSharp saves you.

The above class, with auto-implemented properties and the INotifyPropertyChanged interface.

public class Customer : System.ComponentModel.INotifyPropertyChanged 
{
[Notify]
public String Name { get; set; }

[Notify]
public DateTime DOB { get; set; }

#region INotifyPropertyChanged Members

public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}

#endregion
}

That’s cool…from ~50 lines of code to ~25 lines of code. You’ll be wondering, how will the PropertyChanged event fire now? The answer is within the [Notify] attribute. This attribute is a special attribute which inherits from the PostSharp class OnMethodBoundaryAspect. For this you’ll have to add references to the PostSharp DLLs.


image 



The implementation is shown below:

[Serializable]
[global::System.AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class NotifyAttribute : OnMethodBoundaryAspect
{
public override void OnExit(MethodExecutionEventArgs eventArgs)
{
if (eventArgs == null)
return;

//Why are property sets not properties? they are methods?
if ((eventArgs.Method.MemberType & System.Reflection.MemberTypes.Method) == System.Reflection.MemberTypes.Method && eventArgs.Method.Name.StartsWith("set_"))
{
Type theType = eventArgs.Method.ReflectedType;
string propertyName = eventArgs.Method.Name.Substring(4);

// get the field storing the delegate list that are stored by the event.
FieldInfo[] fields = theType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
FieldInfo field = null;
foreach (FieldInfo f in fields)
{
if (f.FieldType == typeof(PropertyChangedEventHandler))
{
field = f;
break;
}
}

if (field != null)
{
// get the value of the field
PropertyChangedEventHandler evHandler = field.GetValue(eventArgs.Instance)as PropertyChangedEventHandler;

// invoke the delegate if it's not null (aka empty)
if (evHandler != null)
{
evHandler.Invoke(eventArgs.Instance, new PropertyChangedEventArgs(propertyName));
}
}
}
}
}

This works perfectly, on every method “exit” the above OnExit(…) method will fire and invoke the PropertyChangedEventHandler for that particular property with it’s property name.


Neat isn’t it? Let us run some tests against it to make sure that it is actually happening. For this we revert to NUnit.


[TestFixture]
public class CustomerTest
{
private Customer _customer;
private String _propertyChanged;

[SetUp]
protected void Setup()
{
_customer = new Customer();
_customer.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Customer_PropertyChanged);
}

[TearDown]
protected void TearDown()
{
_propertyChanged = String.Empty;
}

void Customer_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
_propertyChanged = e.PropertyName;
}

[Test]
public void TestNotifyName()
{
_customer.Name = "Ruskin";
Assert.AreEqual("Name", _propertyChanged);
}

[Test]
public void TestNotifyDOB()
{
_customer.DOB = DateTime.Now;
Assert.AreEqual("DOB", _propertyChanged);
}
}

image 
You can go one step further…ever had calculated properties in your business classes, for e.g. if you have a property for “Age” which gets calculated depending on the DOB, therefore, if DOB changes, Age changes. In our current implementation we can only mark properties to fire PropertyChanged events for themselves, but we can’t make them trigger events for other properties. This can be easily solved by adding another standard .NET attribute shown below:

[Serializable] 
[global::System.AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class NotifyChildrenAttribute : Attribute
{
public NotifyChildrenAttribute(params string[] dependents)
{
Dependents = dependents;
}

public NotifyChildrenAttribute() { }

public string[] Dependents { get; set; }
}

Now we add an “Age” property to our Customer class and let us look at the changed version: 

public class Customer : System.ComponentModel.INotifyPropertyChanged
{
[Notify]
public String Name { get; set; }

[Notify]
[NotifyChildren("Age")]
public DateTime DOB { get; set; }

public int Age { get { return DateTime.Now.Year - DOB.Year; } }

#region INotifyPropertyChanged Members

public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}

#endregion
}

As we can see, we have marked our DOB property to fire a PropertyChanged event not only for itself but also for the Age property. We still need to modify our implementation of the OnExit(…) method slightly to cater for this new attribute. Our implementation will look as follows:

[Serializable]
[global::System.AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class NotifyAttribute : OnMethodBoundaryAspect
{
public override void OnExit(MethodExecutionEventArgs eventArgs)
{
if (eventArgs == null)
return;

//Why are property sets not properties? they are methods?
if ((eventArgs.Method.MemberType & System.Reflection.MemberTypes.Method) == System.Reflection.MemberTypes.Method && eventArgs.Method.Name.StartsWith("set_"))
{
Type theType = eventArgs.Method.ReflectedType;
string propertyName = eventArgs.Method.Name.Substring(4);

List<string> allPropertiesThatNeedNotification = new List<string>();
if (!String.IsNullOrEmpty(propertyName))
{
PropertyInfo pInfo = theType.GetProperty(propertyName);
if (pInfo != null)
{
// get all the properties which may be affected
object[] notifyChildren = pInfo.GetCustomAttributes(typeof(NotifyChildrenAttribute), false);
foreach (var item in notifyChildren)
{
if (item is NotifyChildrenAttribute)
{
NotifyChildrenAttribute att = item as NotifyChildrenAttribute;
allPropertiesThatNeedNotification.AddRange(att.Dependents);
}
}
}
allPropertiesThatNeedNotification.Add(propertyName);
}

// get the field storing the delegate list that are stored by the event.
FieldInfo[] fields = theType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
FieldInfo field = null;
foreach (FieldInfo f in fields)
{
if (f.FieldType == typeof(PropertyChangedEventHandler))
{
field = f;
break;
}
}

if (field != null)
{
// get the value of the field
PropertyChangedEventHandler evHandler = field.GetValue(eventArgs.Instance) as PropertyChangedEventHandler;

// invoke the delegate if it's not null (aka empty)
if (evHandler != null)
{
foreach (var item in allPropertiesThatNeedNotification)
{
evHandler.Invoke(eventArgs.Instance, new PropertyChangedEventArgs(item));
}
}
}
}
}
}

Updating the tests…note that our previously defined tests won’t work now because for every singular property changed there may be a series of PropertyChanged events, we need some kind of collection which we need to test against. This testing is a little bit primitive but this is only for demonstration purposes.

[TestFixture]
public class CustomerTest
{
private Customer _customer;
private List<String> _propertiesChanged;

[SetUp]
protected void Setup()
{
_customer = new Customer();
_customer.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Customer_PropertyChanged);
}

[TearDown]
protected void TearDown()
{
_propertiesChanged = new List<string>();
}

void Customer_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
Assert.IsTrue(_propertiesChanged.Contains(e.PropertyName), String.Format("Property {0} is not contained within the list", e.PropertyName));
}

[Test]
public void TestNotifyName()
{
_propertiesChanged.Add("Name");
_customer.Name = "Ruskin";
}

[Test]
public void TestNotifyDOB()
{
_propertiesChanged.Add("DOB");
_propertiesChanged.Add("Age");
_customer.DOB = DateTime.Now;
}
}

image 


Downloads: PostSharp

5 comments:

  1. This is really handy. Do you have to buy a license for PostSharp to use it?

    ReplyDelete
  2. No postsharp is open source as far as I know

    ReplyDelete
  3. Oh ok, saw a link on their site about buy, it seems you only need it if you need to package build-time components. So should be all good. http://www.postsharp.org/about/license

    ReplyDelete
  4. I have a question regarding the following line

    // get the field storing the delegate list that are stored by the event.
    FieldInfo[] fields = theType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);

    Why do you only include NonPublic fields when searching for the PropertyChanged event handler? It doesn't make sense unless I am missing something.

    ReplyDelete
  5. PostSharp provides their own implementation here: http://www.sharpcrafters.com/solutions/ui#data-binding

    ReplyDelete