Sep 14
2009

Introducing PostSharp 2.0: #1 - NotifyPropertyChanged

Posted by: gfraiteur

Tagged in: Untagged 

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? kick it on DotNetKicks.com

Trackback(0)
Comments (26)add comment

Markus Wagner said:

September 14, 2009
Votes: +0

RobertMcCarter said:

RobertMcCarter
This DOES kick ass!
This is absolutely amazing!
So, when can we get our hands on it!?!

PS. I loved "No pedagogical simplification". :-)

 
September 14, 2009
Votes: +0

RobertMcCarter said:

RobertMcCarter
Still amazed
BTW, I've wanted this capability for AGES! Previously it seemed you had to write a low-level PostSharp plug-in to get this kind of functionality. So the fact that you can now add methods, events, properties, etc to a class this easily is excellent. True AOP for .NET.

Do you have a sense of the pricing for v2? How much will this goodness cost? :-)
 
September 14, 2009
Votes: +0

Alex Yakunin said:

0
PostSharp 2.0
New code looks really beautiful smilies/wink.gif
 
September 15, 2009 | url
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
Pricing
For end users, PostSharp Professional Edition will be priced and licensed as Resharper.
 
September 15, 2009
Votes: +0

Hermann said:

0
...
Looks very interesting, I am just worried about performance. Will all this be turned into code during compilation only or is it more like Loas that still does stuff during runtime.
 
September 15, 2009
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
@Hermann
Generally speaking, runtime performance has been greatly improved in PostSharp 2.0, because you now pay for what you consume. Also, PostSharp 2.0 don't use degates to perform interceptions any more, and don't use extensive value boxing to encapsulate parameters.

However, this precise case uses reflection at runtime to get the property name, and this has a performance cost. I'll work on a solution to avoid this.
 
September 15, 2009
Votes: +1

sean kearon said:

0
Looking good
Gael, that really is a whole load easier than the approach in v1 & v1.5. Can't wait to see some more!
 
September 15, 2009 | url
Votes: +0

Shawn Wildermuth said:

0
Silverlight?
I have been looking at some of these aspect scenarios but have been hamstrung by Silverlight's lightweight .NET Framework. Will this work with Silverlight or just the full .NET Framework?
 
September 20, 2009 | url
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
Silverlight!
All of this will work with Silverlight.
 
September 20, 2009
Votes: +0

Andrew Matthews said:

0
Multiple Inheritance in C#
Hi Gael,

Have you fixed the language again? This looks like a nice way to provide a substitute for multiple inheritance. It is reminiscent of Alexandrescu's Policy Based Programming...

Andrew
 
September 25, 2009 | url
Votes: +0

RobertMcCarter said:

RobertMcCarter
Constructors
Can the same aspect know when a property is being changed from within the constructor? I'd like the aspect to avoid raising the property changed event if the property is set within the constructor.
 
October 13, 2009
Votes: +0

Yoooder said:

Yoooder
@RobertMcCarter RE: Constructors
The constructor shouldn't be a concern; the object is still in the process of being created. It's not possible to add handlers to the events until after the constructor is complete.
 
October 13, 2009 | url
Votes: +0

McNaught said:

0
Order??
NotifyPropertyChanged shouldn't fire until _after_ the property has actually changed. Is this supported?
 
October 14, 2009
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
...
@RobertMcCarter RE: Constructors
No, you can't detect where your advice/handler is invoked from unless you use a StackFrame.
 
October 14, 2009
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
@McNaugh RE: Order
I think there is a mistake in the code sample I gave. The property is actually set when you invoke base.OnSetValue(...). You should invoke that before the notification.
 
October 14, 2009
Votes: +0

RobertMcCarter said:

RobertMcCarter
RE: Constructors
I was thinking that you could introduce a boolean, and if you could within the same aspect capture the method entry/exit on the constructor then on entry you could set the boolean to true and on exit false. Then the property changed code within the aspect could simply test the boolean.
 
October 14, 2009
Votes: +0

RobertMcCarter said:

RobertMcCarter
@Yoooder RE: Constructors
That's a good point - my existing aspect has a very complex raise-property implementation with locking (for thread safety) and automatic cascading property changed events (for example if BirthDate changes then the read-only Age property will also "automatically" raise a property changed event).

However, with this new more powerful approach almost all that work (save for a single lock) can be avoided if nobody is listening to the event.

Thanks!
 
October 14, 2009
Votes: +0

Martijn Muurman said:

0
...
OnLocationSetHandler should be OnLocationSetValueAdvice right? Like in the samples.
 
October 20, 2009
Votes: +0

Martijn Muurman said:

0
PropertyChangedAttribute sample in PostSharp does not work?
Using the property changed sample as a basis for an aspect I ran into the problem
it does not work yet:

The following code is from the test Program
c.PropertyA = "Hello";
//this should write "Value:Hello"..instead we see "Value:"
Console.WriteLine("Value: {0}",c.PropertyA);
c.PropertyB = 5;

Addint the following line to OnPropertySet in the attribute seems to fix this problem.

args.ProceedSetValue();

Is this correct? Is there any documentation yet for these classes? It seems a real nice improvement from 1.5 smilies/smiley.gif
 
October 20, 2009
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
@Martijn
Yes, Martijn, you are twice right. Naming was inlined with official AOP terms before release, and I forgot to call base.OnPropertySetValue() in the sample.
 
October 21, 2009
Votes: +0

Martijn Muurman said:

0
@gael
base.OnPropertySetValue() does not work (not in the base-class)

Did you mean args.ProceedSetValue() ?
 
October 21, 2009
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
@Martijn
Yes, you are rigth a third time. Seems I need some rest smilies/cry.gif
 
October 21, 2009
Votes: +0

Lemonhead said:

0
This doesn't work
The minute you put your class that is tagged with [NotifyPropertyChanged] in a different assembly from the actual definition of NotifyPropertyChangedAttribute, this stops working.

??

That's a serious limitation. It means I can't just declare the NotifyPropertyChangedAttribute class once in a shared library, then use it throughout my different projects.
 
October 22, 2009
Votes: +0

Gael Fraiteur said:

Gael Fraiteur
RE: This doesn't work
The bug has been solved. You have to download and install a new version.
 
October 22, 2009
Votes: +0

Sadegh Alavi said:

SadeghAlavi
Problem in Inheritance Hierarchy
This approach does not work properly when we structured project and define inheritance hierarchy.
For instance, when you define a derived class in second assembly from a derived class which inherited from a base class in addition base class tagged with [NotifyPropertyChanged] and both of them are in external assembly (first assembly) , you could not be able to run program or the result not working properly and properties which are defined in second assembly does not work and notified.
I defined this structure with PostSharp 1.5 and tradition inplementation of NotifyPropertyChanged by PostSharp in solution and it works great.
Is there a problem in this structure or lack of support by PostSharp 2.0 ?


Solution Schema:

Assembly 1
-BaseClass [NotifyPropertyChanged]
-P1 (Work)
-P2 (Work)
-DerivedLevel1 : BaseClass
-DP1 (Work)

Assembly 1 : Assembly 2
-DerivedLevel2 : DerivedLevel1
-D2P1 (Does not Work!)
 
November 21, 2009
Votes: +0

Write comment

busy