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 !