Introducing PostSharp 2.0: #1 - NotifyPropertyChanged

by Gael Fraiteur on 14 Sep 2009

Be seated safely before reading on. This kicks ass.

I have already written enough abstract words in previous posts; let's now introduce PostSharp 2.0 on a real example: the implementation of  NotifyPropertyChanged, one of the most frequently used patterns for all adepts of MVC designs.

It's not just about implementing the INotifyPropertyChanged interface; it's also about modifying every property setter. It's deadly simple and deadly boring. And gets you more than one step away from the idea you have of aesthetical code.

Enough words. See the implementation using PostSharp 2.0. There's a lot of new concepts out there, so let's start easily:

[Serializable]
[IntroduceInterface( typeof(INotifyPropertyChanged))]
public sealed class NotifyPropertyChangedAttribute : 
   InstanceLevelAspect, INotifyPropertyChanged
{
    public void OnPropertyChanged( string propertyName )
    {
        if ( this.PropertyChanged != null )
        {
            this.PropertyChanged( this.Instance, 
               new PropertyChangedEventArgs( propertyName ) );
        }
    }

    [IntroduceMember]
    public event PropertyChangedEventHandler PropertyChanged;

    [OnLocationSetHandler, 
     MulticastSelector( Targets = MulticastTargets.Property )]
    public void OnPropertySet( LocationInterceptionArgs args )
    {
        if ( args.Value == args.GetCurrentValue() ) return;

        this.OnPropertyChanged( args.Location.PropertyInfo.Name );
    }
}

The aspect is used this way:

[NotifyPropertyChanged]
public class TestBaseClass
{
    public string PropertyA { get; set; }
}

Some words of explanation.

First, look at introductions: IntroduceInterface tells that the interface INotifyPropertyChanged must be introduced into the target type. And where is the interface implemented? Well, by the aspect itself! Note that PropertyChanged is itself introduced to the target type as a public member thanks to the IntroduceMember custom attribute (without this attribute, the interface would be implemented explicitely).

The aspect actually becomes an extension of the class; aspect instances have the same lifetime as objects of the class to which the aspect is applied... just because the aspect inherits from InstanceLevelAspect.

Got it? So let's continue to the OnPropertySet method. It is marked by custom attribute OnLocationSetHandler: it makes this method a handler that intercepts all calls to the setter of all target properties. And which are target properties? This is told by custom attribute MulticastSelector: all properties in this case, but we could filter on name, visibility and all features you are used to. We could have used the same handler to handle access to properties (it would have turned the field to a property as a side effect).

Did you get it? You can now catch accesses to properties or fields using the same aspects. Same semantics, same aspect. Simple, powerful.

Now look at the body of method OnPropertySet and see how easy it is to read the current value of the property.

The code above works on an isolated class, but a class is rarely isolated, right? More of the time, it derives from an ancestor and have descendants. What if the ancestor already implements interface INotifyPropertyChanged? The code above would trigger a build time error. But we can improve it. What do we want, by the way? Well, if the class above already implements INotifyPropertyChanged, it must also have implemented a protected method OnPropertyChanged(string), and we have to invoke this method. If not, we define this method ourselves. In both cases, we need invoke this method from all property setters.

Let's turn it into code:

[Serializable]
[IntroduceInterface( typeof(INotifyPropertyChanged), 
                     OverrideAction = InterfaceOverrideAction.Ignore )]
[MulticastAttributeUsage( MulticastTargets.Class, 
                          Inheritance = MulticastInheritance.Strict )]
public sealed class NotifyPropertyChangedAttribute : 
   InstanceLevelAspect, INotifyPropertyChanged
{
   
    [ImportMember( "OnPropertyChanged", IsRequired = false )] 
    public Action<string> BaseOnPropertyChanged;

    [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 ) );
        }
    }

    [IntroduceMember( OverrideAction = MemberOverrideAction.Ignore )]
    public event PropertyChangedEventHandler PropertyChanged;

    [OnLocationSetHandler, 
     MulticastSelector( Targets = MulticastTargets.Property )]
    public void OnPropertySet( LocationInterceptionArgs args )
    {
        if ( args.Value == args.GetCurrentValue() ) return;

        if ( this.BaseOnPropertyChanged != null )
        {
            this.BaseOnPropertyChanged( args.Location.PropertyInfo.Name );
        }
        else
        {
            this.OnPropertyChanged( args.Location.PropertyInfo.Name );
        }
    }
}

This time, the code is complete. No pedagogical simplification. Look at custom attributes IntroduceInterface and IntroduceMember: I have added an OverrideAction; it tells that interface and member introductions will be silently ignored if already implemented above.

Now look at field BasePropertyChanged: its type is Action<string> (a delegate with signature (string):void) and is annotated with custom attribute ImportMember. At runtime, this field with be bound to method OnPropertyChanged of the base type. If there is no such method in the base type, the field will simply be null. So, in method OnPropertySet, we can now choose: if there was already a method OnPropertyChanged, we invoke the existing one. Otherwise, we invoke the one we are introducing.

Thanks to ImportMember, we know how to extend a class that already implement the pattern. But how to make our own implementation extensible by derived classes? We have to introduce the method OnPropertyChanged and make it virtual. It's done, again, by custom attribute IntroduceMember.

That's all. You can now use the code on class hierarchies, like here:

[NotifyPropertyChanged]
public class TestBaseClass
{
    public string PropertyA { get; set; }
}
public class TestDerivedClass : TestBaseClass
{
    public int PropertyB { get; set; }
}
class Program
{
    static void Main(string[] args)
    {
        TestDerivedClass c = new TestDerivedClass();
        Post.Cast<TestDerivedClass, INotifyPropertyChanged>( c ).PropertyChanged += OnPropertyChanged;
        c.PropertyA = "Hello";
        c.PropertyB = 5;
    }

    private static void OnPropertyChanged( object sender, PropertyChangedEventArgs e )
    {
        Console.WriteLine("Property changed: {0}", e.PropertyName);
    }
}

With PostSharp 1.5, you could do implement easy aspects easily. With PostSharp 2.0, you can implement more complex design patterns, and it's still easy.

Happy PostSharping!

-gael

P.S. Kicking?