Wednesday, February 17, 2010

Aspect Based INotifyPropertyChanged Implementation

There are quite a few articles floating around about how to implement an aspect oriented version of INotifyPropertyChanged Interface.
My version is based on an article by Tamir Khason Posted on CodeProject.

The original version had some problems - placing the Attribute on both a Parent and Child class caused an exception to be thrown and because the interface was implemented at compile time by PostSharp* it could not be used without an unsafe cast.

My version requires that the class implement the interface directly, adds an interface that reports which property that also provides the pre-change value (useful when you want to save undo for example) and checks with any implementing class to see if anyone is registered (and skips all other checks if no one is listening...). We then check to see if there's any modification and only fire notification on change - this might not be the required behavior in all cases so the check can be easily removed.

So, performance is probably better, usage is pretty straight forward - implement INotifyPropertyChanged and IFirePropertyChanged and mark your class with the NotifyPropertyChanged attribute.
If this is implemented on a base class all children will also fire modification notification on all properties. I am adding a sample base class implementation as well.
Any class that extends this base class gets a free change notification for all properties.

Example Base Class:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AutoPropertyChangedWiring;
using System.ComponentModel;

namespace AutoPropertyChanged
{
/// <summary>
/// base class for any class that wishes to get INofityPropertyChanged for "free"
/// any class that extends this base class will get a PropertyChanged events, auto-implemented for all properties
/// </summary>
[NotifyPropertyChanged]
public abstract class PropertyChangeNotifier : INotifyPropertyChanged, IFirePropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

public void FirePropertyChanged(string propertyName, object oldValue)
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public bool HasPropertyListeners
{
get
{
if (PropertyChanged == null)
return false;
else
return true;
}
}
}
}



Aspect Implementation and base class:



using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using PostSharp.Extensibility;
using PostSharp.Laos;

namespace AutoPropertyChangedWiring {

public interface IFirePropertyChanged
{
void FirePropertyChanged(String propertyName,Object oldValue);
Boolean HasPropertyListeners{get;}
}
[Serializable, AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = false, Inherited = true),
MulticastAttributeUsage(MulticastTargets.Class, AllowMultiple = false, Inheritance = MulticastInheritance.Strict, AllowExternalAssemblies = true)]
public sealed class NotifyPropertyChangedAttribute : CompoundAspect {
public int AspectPriority { get; set; }

public override void ProvideAspects(object element, LaosReflectionAspectCollection collection) {
Type targetType = (Type)element;
foreach (var info in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(pi => pi.GetSetMethod() != null)) {
collection.AddAspect(info.GetSetMethod(), new NotifyPropertyChangedAspect(info.Name) { AspectPriority = AspectPriority });
}
}
}

[Serializable]
internal sealed class NotifyPropertyChangedAspect : OnMethodBoundaryAspect {
private readonly string _propertyName;
private object oldValue;
private Boolean fireEvent;
public NotifyPropertyChangedAspect(string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException("propertyName");
_propertyName = propertyName;
}

public override void OnEntry(MethodExecutionEventArgs eventArgs) {
var instance = eventArgs.Instance as IFirePropertyChanged;
fireEvent = true;
//check if anyone is listening.
if (!instance.HasPropertyListeners)
{
//no one is listening, no point in moving on.
fireEvent = false;
return; //no need for the other checks
}

var targetType = eventArgs.Instance.GetType();
var setSetMethod = targetType.GetProperty(_propertyName);
if (setSetMethod == null) throw new AccessViolationException();
oldValue = setSetMethod.GetValue(eventArgs.Instance,null);
var newValue = eventArgs.GetReadOnlyArgumentArray()[0];

if (IsEqual(oldValue,newValue)) //nothing changed, nothing to do..
eventArgs.FlowBehavior = FlowBehavior.Return;
}
protected static Boolean IsEqual(Object left, Object right)
{
//both are null == both are equal.
if (left == null && right == null)
return true;

//at least one is not null, if its left - lets use it for the "Equals" method.
if (left != null)
return left.Equals(right);
else //left is null, but both are not null - which means right is not null and they are not equal.
return false;
}
public override void OnSuccess(MethodExecutionEventArgs eventArgs) {
if (!fireEvent)
return; //no need to fire the event.
var instance = eventArgs.Instance as IFirePropertyChanged;
instance.FirePropertyChanged(_propertyName,oldValue);

}
}

}

* - This probably goes without saying - Using aspects requires Postsharp to be installed (v1.5)

No comments: