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

Implementing the INotifyPropertyChanged interface with the WatchableObject.

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
15 Jun 2014Ms-PL3 min read 9.9K   85   10  
An introduction to the WatchableObject that is a base class to implement the INotifyPropertyChanged interface.

This class is deprecated. Please check the PropertyObservable class.

Introduction

If you are a WPF developer using the MVVM pattern, you may know the importance of the INotifyPropertyChanged interface. INotifyPropertyChanged implementation influences almost all properties in view models. Therefore even a minor improvement of the INotifyPropertyChanged implementation is likely to increase overall coding productivity effectively. The WatchableObject is a pure C# class written for that purpose.

Basics

The INotifyPropertyChanged interface has the following definition:

C#
// Notifies clients that a property value has changed.
public interface INotifyPropertyChanged
{
    // Occurs when a property value changes.
    event PropertyChangedEventHandler PropertyChanged;
}

So if you implement it from scratch, you need to write the PropertyChanged event like this:

C#
public partial class Test : INotifyPropertyChanged
{
    // Occurs when a property value is changed.
    public event PropertyChangedEventHandler PropertyChanged;

    // Raises a PropertyChanged event.
    // An empty or null property name indicates that all of the properties have changed.
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged != null)
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

And you have to raise the event for each property changes you want to notify. The following shows a property named Number that supports change notification:

C#
public partial class Test : INotifyPropertyChanged
{
    int _number;
    
    public int Number
    {
        get { return _number; }
        set
        {
            if (_number == value)
                return;
                
            _number = value;
            OnPropertyChanged("Number");
        }
    }
}

If read-only calculated properties are added, you must raise the event whenever the property it depends on is changed. For instance, this code declares two calculated properties: IsOddNumber, IsEvenNumber. Both properties depend on the Number property. So if the Number property is changed, the code notifies that the IsOddNumber and IsEvenNumber are changed:

C#
public partial class Test : INotifyPropertyChanged
{
    int _number;
    
    public int Number
    {
        get { return _number; }
        set
        {
            if (_number == value)
                return;
                
            _number = value;
            OnPropertyChanged("Number");
            OnPropertyChanged("IsOddNumber");
            OnPropertyChanged("IsEvenNumber");
        }
    }

    public bool IsOddNumber { get { return (Number % 2) != 0; } }
    public bool IsEvenNumber { get { return !IsOddNumber; } }
}

As you can see, if the INotifyPropertyChanged interface is implemented from scratch, many lines of code must be written for a simple property setter. If you implement it with the WatchableObject, it can be written like this:

C#
public class Test : WatchableObject
{
    int _number;

    public int Number
    {
        get { return _number; }
        set { SetProperty("Number", ref _number, value, "IsOddNumber", "IsEvenNumber"); }
    }

    public bool IsOddNumber { get { return (Number % 2) != 0; } }
    public bool IsEvenNumber { get { return !IsOddNumber; } }
}

Auto storage

The backing field _number can be omitted if you use the storage provided by the WatchableObject like this:

C#
public class Test : WatchableObject
{
    public int Number
    {
        get { return GetProperty<int>("Number"); }
        set { SetProperty("Number", value, "IsOddNumber", "IsEvenNumber"); }
    }

    public bool IsOddNumber { get { return (Number % 2) != 0; } }
    public bool IsEvenNumber { get { return !IsOddNumber; } }
}
</int>

If you want to initialize the property, you can use the initializer parameter of the GetProperty method:

C#
public class Test : WatchableObject
{
    const int DefaultNumber = 10;

    public int Number
    {
        get { return GetProperty<int>("Number", () => DefaultNumber); }
        set { SetProperty("Number", value, "IsOddNumber", "IsEvenNumber"); }
    }

    public bool IsOddNumber { get { return (Number % 2) != 0; } }
    public bool IsEvenNumber { get { return !IsOddNumber; } }
}
</int>

You should be careful that the initializer parameter is not executed until the property getter is accessed. It means that if you run the following code:

C#
Test test = new Test();
test.PropertyChanged += (_, e) => Trace.WriteLine(e.PropertyName + " is changed.");
test.Number = 10;

you get the following result:

Number is changed.
IsOddNumber is changed.
IsEvenNumber is changed.

Auto property name

The .NET Framework 4.5 introduces an attribute named CallerMemberName, which allows you to obtain the property name of the caller to the method. By this attribute, the property name can be omitted like this:

C#
public class Test : WatchableObject
{
    const int DefaultNumber = 10;

    public int Number
    {
        get { return GetCallerProperty<int>(() => DefaultNumber); }
        set { SetCallerProperty(value, new string[] { "IsOddNumber", "IsEvenNumber" }); }
    }

    public bool IsOddNumber { get { return (Number % 2) != 0; } }
    public bool IsEvenNumber { get { return !IsOddNumber; } }
}
</int>

Refactorable property name

Property name strings are not refactorable by Visual Studio. If you want to avoid this situation, you can use lambda expression instead of property name string like this:

C#
public class Test : WatchableObject
{
    const int DefaultNumber = 10;

    public int Number
    {
        get { return GetProperty(() => Number, () => DefaultNumber); }
        set { SetProperty(() => Number, value, ToPropertyName(() => IsOddNumber, () => IsEvenNumber)); }
    }

    public bool IsOddNumber { get { return (Number % 2) != 0; } }
    public bool IsEvenNumber { get { return !IsOddNumber; } }
}

You should know that it costs some performance. If the nameof operator of the C# vNext is implemented, you should avoid using lambda expressions.

Running custom code when property changes

The SetProperty method returns true if the property is changed. This allows you to execute custom code when the property is changed:

C#
public class Test : WatchableObject
{
    const int DefaultNumber = 10;

    public int Number
    {
        get { return GetProperty<int>("Number", () => DefaultNumber); }
        set
        {
            if (SetProperty("Number", value, "IsOddNumber", "IsEvenNumber"))
                Trace.WriteLine("The Number property is changed.");
        }
    }

    public bool IsOddNumber { get { return (Number % 2) != 0; } }
    public bool IsEvenNumber { get { return !IsOddNumber; } }
}
</int>

Performance

This is a simple performance test result that shows average ticks of reading and writing properties of 10000 runs. There are four cases:

  • Local Storage: Properties using backing fields for property values.
  • Local Storage with Lambda: Properties using backing fields for property values and lambda expressions for property names.
  • Auto Storage: Properties using the auto storage for property values.
  • Auto Storage with Lambda: Properties using the auto storage for property values and lambda expressions for property names.

Here is the result:

Read/Write Local Storage Local Storage with Lambda Auto Storage Auto Storage with Lambda
6 properties 1 tick 19 ticks 2 ticks 39 ticks
12 properties 1 tick 38 ticks 6 ticks 81 ticks
25 properties 2 tick 80 ticks 15 ticks 181 ticks
50 properties 5 tick 173 ticks 34 ticks 412 ticks
100 properties 11 tick 389 ticks 76 ticks 1003 ticks

Image 1

The "Local Storage with Lambda" and "Auto Storage with Lambda" performance will be much worse if you run it in Win RT. You can download the performance test program at nicenis.codeplex.com

Supported preprocessor symbols

You can use the following preprocessor symbols:

  • NICENIS_RT: Defines this symbol if you want to compile for Win RT.
  • NICENIS_4C: Defines this symbol if you want to compile for .NET Framework 4 Client Profile.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer
Korea (Republic of) Korea (Republic of)
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --