[PostSharp] [Caliburn] - Automatically call NotifyOfPropertyChange on the view models
By Michael DELVA on Thursday 15 July 2010, 14:42 - C# - Permalink
TweetHere, I won't explain what are Caliburn and PostSharp. Their respective documentations, examples and samples already do that very well. But I will show you how to create an aspect which will be applied by PostSharp on the view models of a WPF assembly.
And as a result, on runtime, each time a public property will be changed, the event PropertyChanged of the interface INotifyPropertyChanged will be fired, and the WPF view will be able to refresh its state.
If you already know PostSharp, or have been on their website, you have certainly noticed that they already propose an aspect which intends to implement the interface INotifyPropertyChanged, and to call PropertyChanged when the value of a property is changed.
And if you already know Caliburn, you know that the classes you must inherit from to implement your view models inherit themselves from an abstract class named PropertyChangedBase, which implements INotifyPropertyChanged, and which proposes a function named NotifyOfPropertyChange, to which you give as a parameter the name of the modified property.
The aspect I'm going to show you will allow us to mix these 2 methods.
One thing you could ask me could be : "Why wouldn't we decorate the view models with the aspect attribute NotifyPropertyChangedAttribute, found on the PostSharp website?"
This is what I've started to do. But the problem is that in the parameters of this attribute, there is the following line, which states to not apply the aspect when the decorated class (or one of its parent class) already implements the interface:
[IntroduceInterface(typeof(INotifyPropertyChanged), OverrideAction = InterfaceOverrideAction.Ignore)]
And as our view-models will inherit from classes of Caliburn, which themselves inherit from PropertyChangedBase which implements INotifyPropertyChanged, the aspect will simply be ignored.
I could have changed the definition of the aspect, but I preferred not to alter it (it already works very well like that), and to create a new aspect. Which is fine, because this was my first aspect, and it was not so complicated at all: we don't have to implement an interface, but only to integrate some code in the accessors of the properties of the view-models. In order to do that, we will take the logic of the sample given on the website of PostSharp, with modifications so that it fits our needs.
Let's begin by defining our aspect:
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)]
public class NotifyOfPropertyChangeAttribute : TypeLevelAspect
{
}
Here, we declare that this aspect will only be applied on classes, and that on classes inheriting from the classes decorated with this attribute. As we saw before, we don't need to add the IntroduceInterface declaration.
We just have to code the part in charge of the call to the function NotifyOfPropertyChange(string propertyName) defined in Caliburn.PresentationFramework.PropertyChangedBase, when the value of a property will be updated:
[OnLocationSetValueAdvice, MulticastPointcut(Targets = MulticastTargets.Property, Attributes = MulticastAttributes.Instance | MulticastAttributes.NonAbstract)]
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();
((PropertyChangedBase) args.Instance).NotifyOfPropertyChange(args.Location.Name);
}
We keep the same declaration OnLocationSetValueAdvice, as the targets of OnPropertySet don't change compared to the initial aspect. We keep the part which verifies the value has been changed too, and the call to args.ProceedSetValue() which will assign the new value.
Then, only the notification of the value change will change. We are going to get the instance of the view-model on which the aspect is applied, with args.Instance. We cast it in PropertyChangedBase (no problems with that, as the view-model inherits from it), to be able to eventually call the function NotifyOfPropertyChange, to which we will give the name of the modified property, as an argument.
Here it is! Nothing too complicated, as I told you :)
But nevertheless, we can add a small improvement. Indeed, nothing forbids us to apply this aspect on any class, including classes which don't inherit from PropertyChangedBase, which would throw an InvalidCastException when we cast the instance object.
To avoid this kind of mistakes, we will override the function CompileTimeValidate like this:
public override bool CompileTimeValidate(Type type)
{
Type baseType = type.BaseType;
while (baseType != typeof(object))
{
if (baseType == typeof(PropertyChangedBase))
return true;
baseType = baseType.BaseType;
}
return false;
}
And from now on, we won't be able to apply this aspect on a "wrong" class.
See you soon !