Saturday, December 05, 2009

Yet another INotifyPropertyChanged with Expression Trees

There are dozens of examples out there showing you how to avoid having to refer to method names as strings when implementing INotifyPropertyChanged. The most important reason why you don't want to have to do this is because method names can get refactored but the hardcoded strings might be forgotten. No-one wants to end up getting a Changed notification for a property which doesn't exist.

My issue with all these examples is that none of them thought far enough ahead. Fine, they all show you how refer to properties without using hardcoded strings but they still require you to write lots of boilerplate code to raise the PropertyChanged event - boilerplate you have to write for every property. What I want is to be able to declare all my properties like:

public string Title {
get { return title; }
set { title = value; }
}

and yet still get my property change notifications. I also want this method to be reasonably high performance. I don't want every property change to have extra memory or CPU overhead as every developer expects that changing the value of a property will not do any complex calculations. So how can I accomplish this?

To start off with, we can all tell that it's impossible to achieve the required behaviour using just the snippet above. We're going to have to add (at least) one additional level of indirection. That means I should be able to implement my requirements using code like:

public string Title {
get { return title.Value; }
set { title.Value = value; }
}

The object 'title' must then contain all the logic required to raise the property changed notification. So what might this magical object look like?

public class ChangeNotifier<TValue>
{
Action<string> notifyHandler;
string propertyName;
TValue value;

public TValue Value {
get { return value; }
set {
if (!EqualityComparer<TValue>.Default.Equals(this.value, value)) {
this.value = value;
notifyHandler(propertyName);
}
}
}


public ChangeNotifier(Expression<Func<TValue>> expression, Action<string> notifyHandler)
{
if (expression.NodeType != ExpressionType.Lambda)
throw new ArgumentException("Value must be a lamda expression", "expression");
if (!(expression.Body is MemberExpression))
throw new ArgumentException("The body of the expression must be a memberref", "expression");

MemberExpression m = (MemberExpression)expression.Body;
this.propertyName = m.Member.Name;
this.notifyHandler = notifyHandler;
}
}

You're probably looking at this thinking "What the hell is this Expression<Func<TValue>> ? How do I even use that monstrosity?". Well... simples!

public class Book : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

ChangeNotifier<string> author;
ChangeNotifier<decimal> price;
ChangeNotifier<int> quantity;
ChangeNotifier<string> title;

public string Author {
get { return author.Value; }
set { author.Value = value; }
}
public decimal Price {
get { return price.Value; }
set { price.Value = value; }
}
public int Quantity {
get { return quantity.Value; }
set { quantity.Value = value; }
}
public string Title {
get { return title.Value; }
set { title.Value = value; }
}

public Book()
{
Action<string> notify = (propertyName) => {
var h = PropertyChanged;
if (h != null)
h(this, new PropertyChangedEventArgs(propertyName));
};

author = new ChangeNotifier<string> (() => Author, notify);
price = new ChangeNotifier<decimal> (() => Price, notify);
quantity = new ChangeNotifier<int> (() => Quantity, notify);
title = new ChangeNotifier<string> (() => Title, notify);
}
}

All that happens here is that when constructing the ChangeNotifier object, an Expression referencing the required Property is passed into the constructor, along with a delegate which will raise the PropertyChanged event. We parse that expression tree to retrieve the method name and store it. After that everything Just Works (tm) with little to no performance penalty. The days of writing boilerplate code for INotifyPropertyChanged are gone! You also have the benefit that you can't make a mistake writing the boilerplate code because you don't write it anymore!

17 comments:

Andy Pook said...

Nice.
But I have a suggestion. How about another constructor that takes the PropertyChanged event. Then you can bury the "notify" action inside the ChangeNotifier.
As the notify action looks like boiler plate, it should make using it simpler.

This blog won't accept my code snippet (the lambda defs look too much like html) so I've switched the offending chars to ^. But I'm sure you'll understand after you change them and re-indent.

public ChangeNotifier(Expression^Func^TValue^^ expression, PropertyChangedEventHandler handler)
: this(
expression,
(propertyName) =^
{
var h = handler;
if (h != null)
h(this, new PropertyChangedEventArgs(propertyName));
}
) { }

Marek Safar said...

2 suggestions

Add an user operator and you can remove ugly foo.Value

public static implicit operator TValue (ChangeNotifier < TValue > v)
{
return v.value;
}

Hide ChangeNotifier ctor via a factory method so type inference can work

Lukáš Čenovský said...

If you are interested how this can be done in dynamic language (IronPython), see my blog post.

Anonymous said...

Sry, premature optimization.

First, it has at least a 4 times higher memory footprint per property (the pointer to the change notifier instance plus the three fields in that instance) than a regular one.

Second, what's with:

public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName { get { return String.Concat(LastName, ", ", FirstName); }

Now when FirstName changes, obviously FullName has to send notification as well. No easy way to extend things here to enable that quite common scenario.

Best way I figured still is using T4 to create metadata classes and put boilerplate functionality in base classes or helper classes, so you can write:

private string _firstName;
public string FirstName { get { return _firstName; } set { base.Set(ref _firstName, value, Props.FirstName, Props.FullName); }

private string _lastName;
public string LastName { get { return _lastName; } set { base.Set(ref _lastName, value, Props.LastName, Props.FullName); }

public string FullName { get { return String.Concat(LastName, ", ", FirstName); }

If someone complains that this requires using non-POCO, then I reply, well we shouldn't be using POCOs anyway, we should be using POIs (plain old interfaces). ;-)

Gena said...

Another way to implement it is using AOP, e.g. http://thetreeknowseverything.net/2009/01/21/auto-implement-inotifypropertychanged-with-aspects/

Then you don't need to type code at all except decorating you class (or properties) by an attribute.

Kha said...

To get rid of the remaining boilerplate code, simply add another level of indirection - don't care about INotify in your entity at all, but let the properties implement it themselves!
That's what Ayende's Observable does: http://ayende.com/Blog/archive/2009/08/08/an-easier-way-to-manage-inotifypropertychanged.aspx

No expression trees, no AOP/T4, may easily be extended with support for interdependent properties.

public Observable< string> FirstName { get; private set; }
public Observable< string> LastName { get; private set; }
public Observable< string> FullName { get; private set; }

.ctor
{
FirstName = new Observable< string>();
LastName = new Observable< string>();
FullName = Observable.From(() => FirstName.Value + LastName.Value, FirstName, LastName);
}

Blake said...

Aren't Microsoft's dependency properties relevant here? http://msdn.microsoft.com/en-us/library/ms752914.aspx

Damon Carr said...

Not really as it is a common mistake to make a view model inherit from DependencyObject (to get the plumbing for DPs) when all that is needed is the INotifyPropertyChanged.

After all a view model should be just a POCO with that exception as in my and many other's opinion it is not desirable to dump it into the visual tree and any dependency property mandates typically correlated back to 'animate this' or 'get value from data binding'.

Of course if you need something to participate visually it is often simply a binding off the view model however one must ask why a data template driven solution is not possible.

In other words, the view is 'projected on', the view model exposes the contextual data needed to project and the data templates represent the visual elements created via the view model data binding they have *see DataTemplateSelector and ItemContainerGenerator for more.

Yet another way... DPs in the view model is often a sign there is a design problem the above are vastly more reusable, flexible and extensible via decoupling. A DP couples your view technology in many cases with an otherwise reusable item.

Damon Carr

Alan said...

@Andy Pook: I'll do another blogpost on your idea and go into some details.

@Marek: The factory method is definitely a good idea, though I'm not keen on the implicit conversion.

@anonymous: Sure, you have '4 times the memory usage per property', but it's a static bloat in the region of a dozen bytes. There are zero extra runtime allocations, which is the more important part.

@kha: An apples and oranges comparison ;) My aim was to give standard INotifyPropertyChanged with no boilerplate while still offering standard property getters/setters. It is still trivial to do dependent properties with my implementation, I'll do that in the next blogpost.

@blake: INotifyPropertyChanged is definitely a poor mans version of a DependencyProperty in my eyes. Though there are places where it's a lot more useful than having to subclass from DependencyObject.

herzmeister der welten said...

I don't like the

public Observable< string> FirstName { get; private set; }

because when I model my little world, the entities should be agnostic about if a property is observable or not. Ideally, it should be configurable.

To that DependencyProperty proposal I want to add that those are also not the most performant things on earth. ;-)

Alan said...

Actually one very good argument against the Observable pattern is that it makes read-only properties impossible to enforce at compile time. You convert what would be a compile time error to a runtime error, never a good thing.

For example assume that FirstName is supposed to be read only:
public Observable< string> FirstName { get; private set; }

FirstName.Value = "Bob";

You've just allowed the user to compile code which normally they couldn't. There are no benefits to the Observable method that I can think of, only downsides.

Another downside is that in the PropertyChanged event, you lose the 'sender' argument - so you no longer know which object actually changed.

Anonymous said...

酒店兼職 酒店打工 打工兼差 台北酒店 酒店兼差 酒店經紀 禮服酒店 酒店工作 酒店上班 兼差 酒店應徵 酒店 打工兼職 打工

cc22 said...

情趣用品,情趣,
角色扮演,吊帶襪,丁字褲,飛機杯,
按摩棒,跳蛋,G點,
自慰套,
情趣內衣,
情趣,情趣用品,
SM,G點,按摩棒,
飛機杯,充氣娃娃,
自慰套,情趣用具,

gaohui said...

Have you noticed ed hardy Clothing that she is spending time with ed hardy sale one person in particular ed hardy and they seemed to come from ed hardy UK nowhere. When you ask how she ed hardy cheap knows them she becomes aloof and ed hardy Clothes disinterested. Is there someone's house ed hardy store she seems to be always going to? This edhardy.com could spell something is wrong with the christian audigier sale relationship. Is she taking trips, possibly day ed hardy dresses trips or small vacations without you? If ed hardy Polos she was doing this before you even ed hardy sandals got married or dated, then it may be okay, but if it is a recent ed hardy Jackets development then you may have problems.

ugg boots sale said...

It is far from only most respected to be particularly significant in top, but highly durable as effectively.ugg classic cardy. The GHD straighteners uses the ionic tourmaline technology; which also goes for each and every other item getting constructed by the business. ghd straighteners uk.For people in the hunt for high quality straighteners, this flat iron produces superior results for all hair textures; regardless of what the ailment is.pandora beads.A negative been making use of recognized quality GHD straighteners supplements have spoken of how sturdy they may be. Examples of the favorite characteristics of any of their brands contain; bad ion therapy, earthernware removing iron, distinction heat range control, strong ceramic heater, and other.ghd straighteners uk. You should buy single and commence to appreciate an array of superior attributes becoming savored by other users more or less anywhere.pandora. The selling prices are cost-effective, and it is particularly possible to get some special discounts in most stores. Decide to purchase 1 these days and see points for oneself!

wedding flowers said...

To select from is indeed significantly wide that you will find elaborate jewelry designs in addition to elegant and straightforward designs. pandora.Buying clothes for comfort can also be yet another practical factor. However, we have a pre-conceived notion that comfortable clothes are not usually stylish and cause you to be appear cool. pandora uk.Positive thing there exists Ed Hardy Clothing. These clothes, although stylish, come in lightweight and comfortable material - permitting individuals to move freely. pandora. A person word of advice-I would prevent acquiring any specific silver jewelry that's not sterling. Pandora Jewelry. Repelling any terrible ingredient during the heavy snow as well as nippy wind, it keeps toes warm along with dry anytime all through winter. ghd sale.Necklaces who have similar sized beads work effectively on tall women and chokers help decrease each side height. ghd straighteners.It truly is accurate cheap Ugg Boots provide method aficionados terrific in addition to broad possibilities to spice up their shows. rrncluding a white and pink compact mirror.ugg boots sale.Longer necklaces aid lengthen each side round or square faces.wedding rings Furthermore they add length when worn beneath the bust-line but above the waist.

bilalpress said...

Latest cars and vehicles, Latest Mazda Models, Racing Cars, International Sport Cars, Concept Cars, PS-Pod, Strange Vehicles, Nissan, Royce Corniche, Ford Concept Cars, Strange Vehicles, Mercedes and More Sport Cars and Vehicles with Pictures and Info
WorldLatestVehicles.com

Hit Counter