Tuesday, July 21, 2009

How to use INotifyPropertyChanged, the type-safe way (no magic string)

Implementation of the INotifyPropertyChanged interface is quite simple. There is only one event to implement. Take for example the following simple model class:

public class Model : INotifyPropertyChanged
{
    private string _data;

    public string Data
    {
        get { return _data; }
        set
        {
            if (_data == value)
                return;

            _data = value;

            // Type un-safe PropertyChanged raise 
            PropertyChanged(this, new PropertyChangedEventArgs("Data"));
        }
    }

    #region Implementation of INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged = null;

    #endregion
}

This is a pretty standard way to implement a bindable property. The problem here is the “Data” string to specify which property changed. If someone change the name of the property without changing the content of the string, the code will compile fine but won’t work. In a big application with may properties it can be hard to detect and find the problem.

The best solution is to rely on the compiler to warn us. But because the property name is a string it can’t. So let’s change that line with a type-safe one.

public class Model : INotifyPropertyChanged
{
    private string _data;

    public string Data
    {
        get { return _data; }
        set
        {
            if (_data == value)
                return;

            _data = value;

            // Type safe PropertyChanged raise
            PropertyChanged.Raise(() => Data);
        }
    }

    #region Implementation of INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged = null;

    #endregion
}

What is the trick? Raise is an extension method that takes a lambda expression to specify the name of the property in a type safe way. The Raise method resolve this expression to extract the name of the property and pass it to the PropertyChanged event.

public static class PropertyChangedExtensions
{
    public static void Raise(this PropertyChangedEventHandler handler, Expression<Func<object>> propertyExpression)
    {
        if (handler != null)
        {
            // Retreive lambda body
            var body = propertyExpression.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("'propertyExpression' should be a member expression");

            // Extract the right part (after "=>")
            var vmExpression = body.Expression as ConstantExpression;
            if (vmExpression == null)
                throw new ArgumentException("'propertyExpression' body should be a constant expression");

            // Create a reference to the calling object to pass it as the sender
            LambdaExpression vmlambda = Expression.Lambda(vmExpression);
            Delegate vmFunc = vmlambda.Compile();
            object vm = vmFunc.DynamicInvoke();

            // Extract the name of the property to raise a change on
            string propertyName = body.Member.Name;
            var e = new PropertyChangedEventArgs(propertyName);
            handler(vm, e);
        }
    }
}

All you have to do is to put this extension method in your code and the jib is done. Of course at the end a string will be used to raise the PropertyChanged event but because you don’t have to type it, you don’t have to maintain it.

10 comments:

leppie said...

Nice work :)

Does this work in the inherited class case?

Eric De C# said...

Of course it works on inherited classes because it doesn't depend on inheritance.

leppie said...

Maybe you did not understand. Anyways, I tried it, and it does NOT work (as I expected).

What I mean is something like:

public class SpecialModel : Model
{
private string _data2;

public string Data2
{
get { return _data2; }
set
{
_data2 = value;
PropertyChanged.Raise(() => Data2);
}
}
}

The above will not compile. You will get: error CS0070: The event 'Model.PropertyChanged' can only appear on the left hand side of += or -= (except when used from within the type 'Model')

:|

Eric De C# said...

You are right. You need PropertyChanged event defined in every derived classes, but it it's not so good because they will "hide" the base implementation.

Another way to do this would be to define a virtual method on the base class that take a "Expression Of (Func Of object)" and use it to call the PropertyChanged event.

I used to implement my INPC interface like this:
#region INotifyPropertyChanged Members

private string[] GetPropertiesList()
{
Type t = GetType();
PropertyInfo[] properties =
t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);

var propList = new string[properties.Length];
for (int i = 0; i < properties.Length; i++)
propList[i] = properties[i].Name;
return propList;
}

public void RaisePropertyChanged(params string[] properties)
{
if (properties.Length == 0)
properties = GetPropertiesList();

foreach (string property in properties)
OnPropertyChanged(new PropertyChangedEventArgs(property));
}

private void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);

OnPropertyChangedCore(e);
}

protected virtual void OnPropertyChangedCore(PropertyChangedEventArgs args) {}

public event PropertyChangedEventHandler PropertyChanged;

#endregion

Andrew Theken said...

I think that you could just write the extension method for the INotifyPropertyChanged interface, then do:

"this.Raise(()=>Data)" and the inheritance problems go away.

XIU said...

Nope, because you can't raise the event from your extension method.

Eric De C# said...

See my next post to find a solution I think worth trying.

Unknown said...

Hi, great idea.
I got inspired by that and wrote something about it.
Have a look at http://gfoidl.spaces.live.com/blog/cns!36D34E66505CE4AF!201.entry

Altought it's written in German language the code should be readable.
It's very little code needed and shows a variant with reflection where even no arguments are needed.

Andrei Rinea said...
This comment has been removed by the author.
Andrei Rinea said...

screw it... the angled brackets got eaten away.. (I'll try square ones instead) :

public abstract class ViewModelBase[TConcreteType] : INotifyPropertyChanged
{
protected void NotifyPropertyChanged(Expression[Func[TConcreteType, object]] member)
{
if (member == null)
{
throw new ArgumentNullException("member");
}
if (this.PropertyChanged == null)
{
return;
}
this.PropertyChanged(this, new PropertyChangedEventArgs(ExpressionUtil.MemberName[TConcreteType](member)));
}

public event PropertyChangedEventHandler PropertyChanged;

public bool IsPropertyName(Expression[Func[TConcreteType, object]] property, string propertyName)
{
return ExpressionUtil.MemberName[TConcreteType](property) == propertyName;
}
}


and


public static class ExpressionUtil
{
public static string MemberName[T](Expression[Func[T, object]] unaryExpression)
{
var unaryExpr = (UnaryExpression)unaryExpression.Body;
var memberExpr = (MemberExpression)unaryExpr.Operand;
return memberExpr.Member.Name;
}
}