Click here to Skip to main content
15,885,214 members
Articles / Desktop Programming / WPF
Tip/Trick

Concise PropertyChanged Implementation

Rate me:
Please Sign up or sign in to vote.
4.25/5 (7 votes)
29 Jun 2016CPOL1 min read 17.9K   11   16
Save wear and tear on your keyboard: INotifyPropertyChanged with the fewest key-strokes.

I H8 PropertyChanged

...because I got used to auto-properties really quickly...

The most painful part of building entities to interact with the UI, (for me at least) is having to create properties that participate in the INotifyPropertyChanged interface.

You know the ones I mean:

C#
string _backingField;

public string MyProperty  
{
    get { return _backingField; }
    set 
    { 
          if (!string.Equals(value, _backingField)
          {
              _backingField = value;
              OnPropertyChanged("MyProperty");
          }
    }
}

You can improve the rate at which you can add these using a code-snippet, but there are annoying limitations, like having to rename the backing field to match the naming convention, the problem of the name being passed to OnPropertyChanged becoming out-of-date with refactoring *(unless you are using C#6.0/VS2015 and "nameof()") and that it's too verbose, you end up with a bunch of backing fields you don't want, and it's just fugly.

Wouldn't it be nicer if it looked like this?

C#
public string MyProperty
{
    get { return GetValue(() => MyProperty); }
    set { SetValue(() => MyProperty, value); }
}

The above code can be generated easily enough with a snippet, it neatly aligns, requires no backing field, type-inference and generics work together to make it strongly-typed, and refactoring the property-name will not break anything.

What's the Trick?

Expressions! The GetValue and SetValue method's first parameter is an Expression<Func<T>> - this allows you to pass a lambda expression into the method (with the property name) and supplies the generic-type context (T)

The rest of the trick is that the backing field is in fact a Dictionary<string, object> that stores the values for all the properties.

C#
/// <summary>
/// stores the values for the properties
/// </summary>
protected Dictionary<string, object> m_values = new Dictionary<string, object>();

/// <summary>
/// gets the value of the member specified in the member expression.
/// Generic type parameters should be inferred.
/// </summary>
/// <typeparam name="T">the property type</typeparam>
/// <param name="memberExpression">lambda expression
/// referring to the property invoking this method</param>
/// <returns>the value, or default(T)</returns>
public T GetValue<T>(Expression<Func<T>> memberExpression)
{
    var body = memberExpression.Body as MemberExpression;
    if (body != null)
    {
        object value;
        if (m_values.TryGetValue(body.Member.Name, out value))
        {
            // return the value;
            return (T)value;
        }
    }

    // return a default:
    return default(T);
}

/// <summary>
/// sets the value
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="memberExpression"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool SetValue<T>(Expression<Func<T>> memberExpression, T value)
{
    // is the value different from the existing?
    if (EqualityComparer<T>.Default.Equals(value, GetValue(memberExpression)))
    {
        return false;
    }

    // fetch the name of the property:
    var body = memberExpression.Body as MemberExpression;
    if (body != null)
    {
        // set the value:
        m_values[body.Member.Name] = value;

        // raise the property-changed event
        OnPropertyChanged(body.Member.Name);
    }

    // return true for changed:
    return true;
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Decipha
Australia Australia
Wrote his first computer game in Microsoft Basic, on a Dragon 32 at age 7. It wasn't very good.
Has been working as a consultant and developer for the last 15 years,
Discovered C# shortly after it was created, and hasn't looked back.
Feels weird talking about himself in the third person.

Comments and Discussions

 
QuestionThread safety and performance Pin
tom-englert1-Jul-16 2:51
tom-englert1-Jul-16 2:51 
AnswerRe: Thread safety and performance Pin
georani1-Jul-16 6:19
georani1-Jul-16 6:19 
GeneralRe: Thread safety and performance Pin
Simon Bridge1-Jul-16 12:51
Simon Bridge1-Jul-16 12:51 
GeneralRe: Thread safety and performance Pin
georani1-Jul-16 14:18
georani1-Jul-16 14:18 
GeneralRe: Thread safety and performance Pin
Simon Bridge3-Jul-16 14:27
Simon Bridge3-Jul-16 14:27 
GeneralRe: Thread safety and performance Pin
georani4-Jul-16 3:08
georani4-Jul-16 3:08 
GeneralRe: Thread safety and performance Pin
Simon Bridge4-Jul-16 14:46
Simon Bridge4-Jul-16 14:46 
AnswerRe: Thread safety and performance Pin
hooodaticus1-Jul-16 7:04
hooodaticus1-Jul-16 7:04 
GeneralRe: Thread safety and performance Pin
tom-englert1-Jul-16 20:46
tom-englert1-Jul-16 20:46 
GeneralRe: Thread safety and performance Pin
Simon Bridge3-Jul-16 14:37
Simon Bridge3-Jul-16 14:37 
GeneralRe: Thread safety and performance Pin
hooodaticus7-Sep-16 10:22
hooodaticus7-Sep-16 10:22 
AnswerRe: Thread safety and performance Pin
Simon Bridge1-Jul-16 12:47
Simon Bridge1-Jul-16 12:47 
AnswerRe: Thread safety and performance Pin
Simon Bridge3-Jul-16 16:43
Simon Bridge3-Jul-16 16:43 
I forgot about [CallerMemberName]

I don't know how fast it is... I assume it would have to access the call-stack to read the name of the calling member? whereas nameof() would be embedded at compile-time, and member name expressions are quite expensive (they access the reflection api), but all this is irrelevant for UI code since the user can't tell if the property was read in 100 or 5000 microseconds, and there is no bulk reading/writing of properties through the UI objects (that would be silly, transfers like that would hit the model directly).

One thing that it doesn't remove the need for is a backing field, and it doesn't provide the generic-type-context, meaning that, unfortunately I couldn't reduce the footprint of the code any further with something like this:

C#
get { return GetValue(); }


But I could do this, since the set-value doesn't need a type-context:

C#
set { SetValue(value); }


BTW:

We use GetValue/SetValue methods with a dictionary backing store for ViewModels as our ViewModels all derive from DynamicObject and the same method is used in TrySetMember/TryGetMember so that we can use completely dynamic properties at the same time. Development time and error rates have significantly been reduced since we adopted this model, which translates to lots more $$$ for us, and we haven't had any complaints about performance (and, we would hear about it, believe me.)

We also use this technique to pass-through values from the Model, storing compiled expression delegates in the dictionary that read/write from the 'Model' directly instead of the values themselves, eg:

C#
/// <summary>
/// sets a value on the model, raising <see cref="PropertyChangedBase.PropertyChanged"/> if the value is changing
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="memberExpression"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool SetModelValue<T>(Expression<Func<T>> memberExpression, T value)
{
    var getter = GetGetter(memberExpression);
    var setter = GetSetter(memberExpression);

    if (EqualityComparer<T>.Default.Equals(value, getter.Invoke(Model)))
    {
        return false;
    }

    var body = memberExpression.Body as MemberExpression;
    if (body != null)
    {
        // set the value on the model
        setter.Invoke(Model, value);

        // raise the event:
        OnPropertyChanged(body.Member.Name);
    }

    return true;
}

/// <summary>
/// caches and returns a compiled getter to retrieve the property of the <see cref="Model"/> as specified by the member expression
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="member"></param>
/// <returns></returns>
Func<TModel, T> GetGetter<T>(Expression<Func<T>> member)
{
    // the compiled delegate:
    Func<TModel, T> func = null;

    // get the name of the property to retrieve from the model
    var name = ((MemberExpression)member.Body).Member.Name;

    // define a key to use for this getter in the dictionary
    var key  = $"get_{name}";

    // check the cache: is the getter already compiled?
    object f;
    if (m_values.TryGetValue(key, out f))
    {
        // retrieve the existing getter
        func = f as Func<TModel, T>;
    }
    if (func == null)
    {
        // compile a new getter:
        func = typeof(TModel).GetProperty(name).CompileGetter<TModel, T>();
        if (func != null)
        {
            // add into the dictionary:
            m_values[key] = func;
        }
    }

    // return the getter:
    return func;

}

SuggestionGreat Pin
Kim Nordmo30-Jun-16 4:40
Kim Nordmo30-Jun-16 4:40 
QuestionGreat! Pin
Aladár Horváth29-Jun-16 21:06
professionalAladár Horváth29-Jun-16 21:06 
AnswerRe: Great! Pin
Simon Bridge30-Jun-16 14:36
Simon Bridge30-Jun-16 14:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.