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:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
So if you implement it from scratch, you need to write the PropertyChanged event like this:
public partial class Test : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
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:
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:
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:
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:
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:
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:
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:
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:
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:
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 |
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.