One of the most exciting features of WPF is data binding, which propagates to the
user interface any change you make to domain objects. For this to work, domain objects
must implement the interface INotifyPropertyChanged.
Unfortunately, it's not just about implementing the INotifyPropertyChanged
interface; it's also about modifying every property setter. It's dead simple and
deadly boring. And gets you more than one step away from the idea you have of aesthetical
code.
Before starting to code an implementation pattern into an aspect, it's good to summarize
what's inside the pattern for a given class (say C):
C already implements the pattern:
INotifyPropertyChanged into C
and define the event PropertyChanged, part of the interface.OnPropertyChanged; this method raises
the event PropertyChanged if the event has any client.OnPropertyChanged
method after the property value has been changed (and only if the new value is different
from the old one).C.Here's the full code of the aspect.
/// <summary> /// Aspect that, when apply on a class, fully implements the interface /// <see cref="INotifyPropertyChanged"/> into that class, and overrides all properties to /// that they raise the event <see cref="INotifyPropertyChanged.PropertyChanged"/>. /// </summary> [Serializable] [IntroduceInterface( typeof(INotifyPropertyChanged), OverrideAction = InterfaceOverrideAction.Ignore )] [MulticastAttributeUsage( MulticastTargets.Class, Inheritance = MulticastInheritance.Strict )] public sealed class NotifyPropertyChangedAttribute : InstanceLevelAspect, INotifyPropertyChanged { /// <summary> /// Field bound at runtime to a delegate of the method <c>OnPropertyChanged</c>. /// </summary> [ImportMember( "OnPropertyChanged", IsRequired = false)] public Action<string> OnPropertyChangedMethod; /// <summary> /// Method introduced in the target type (unless it is already present); /// raises the <see cref="PropertyChanged"/> event. /// </summary> /// <param name="propertyName">Name of the property.</param> [IntroduceMember( Visibility = Visibility.Family, IsVirtual = true, OverrideAction = MemberOverrideAction.Ignore )] public void OnPropertyChanged( string propertyName ) { if ( this.PropertyChanged != null ) { this.PropertyChanged( this.Instance, new PropertyChangedEventArgs( propertyName ) ); } } /// <summary> /// Event introduced in the target type (unless it is already present); /// raised whenever a property has changed. /// </summary> [IntroduceMember( OverrideAction = MemberOverrideAction.Ignore )] public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Method intercepting any call to a property setter. /// </summary> /// <param name="args">Aspect arguments.</param> [OnLocationSetValueAdvice, MulticastPointcut( Targets = MulticastTargets.Property, Attributes = MulticastAttributes.Instance)] public void OnPropertySet( LocationInterceptionArgs args ) { // Don't go further if the new value is equal to the old one. // (Possibly use object.Equals here). if ( args.Value == args.GetCurrentValue() ) return; // Actually sets the value. args.ProceedSetValue(); // Invoke method OnPropertyChanged (our, the base one, or the overridden one). this.OnPropertyChangedMethod.Invoke( args.Location.Name ); } }
As you can see, the code of the aspect directly follows the steps of the implementation pattern.
Let's have a look at what the aspect adds to its target classes:
IntroduceInterface present on the top of the class
means that the aspect adds the interface INotifyPropertyChanged (unless
it is already implemented); this interface must be implemented by the aspect itself.IntroduceMember, on method OnPropertyChanged
and event PropertyChanged, means that these members must be added to
the class.
The second requirement, to modify each property setter, is implemented by the method
OnPropertySet. The custom attribute OnLocationSetValueAdvice
means that this method can intercept any attempt to set the value of a property
or field (such a method altering the behavior of other methods is called an advice);
MulticastPointcut specifies that this advice should me applied to all
property setters.
What should we do in method OnPropertySet? First, we have to compare
the new value with the old one. We can retrieve the old value by calling args.GetCurrentValue();
the new value is in args.Value. Then, we have to proceed with the normal
property setter. When it is done, we can invoke the method OnPropertyChanged.
And here's the issue: how do we know which method override we have to invoke? We
can't just invoke the method we're intending to introduce into the type. What if
the method has been defined in a parent type? What if it has been overriden in a
child type?
That's why we have to import the method OnPropertyChanged from
the target class into the current aspect. This is done by defining a public field,
here named OnPropertyChangedMethod, whose type is a delegate of the
same signature of the method to be imported, and annotate this field with a ImportMember
custom attribute. It tells the weaver that the field should be initialized with
a delegate of the right method at runtime.
Finally, let's look at our last requirements: when the aspect is applied to a type,
the same aspect must be applied to all children of this type. This is done by annotating
the aspect type with the custom attribute MulticastAttributeUsage,
with its property Inheritance set to MulticastInheritance.Strict.
So now, you have to apply the custom attribute NotifyPropertyChangedAttribute
only on the root classes of your domain classes.
That's all. We can now use the aspect in our business code:
[NotifyPropertyChanged] public class Shape { public double X { get; set; } public double Y { get; set; } } public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } }