Have you ever been annoyed with having to implement the cumbersome plumbing required for INotifyPropertyChanged ? Well, I have. So I tried to find a way to make authoring bindable objects better.
The typical example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Contact : INotifyPropertyChanged { private string _firstName; public string FirstName { get { return _firstName; } set { if (!Equals(_firstName, value)) { _firstName = value; OnPropertyChanged("FirstName"); } } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } |
As you can see, it is quite verbose. The event and OnPropertyChanged() method need to be implemented for each class and it’s easy to get the implementation of OnPropertyChanged() wrong (typically introducing a race condition). Moreover, it’s 10 lines of code for each property. I hate that. The more you have to write, the bigger a surface for bugs to appear. A property should just boil down to public string FirstName { get; set; } .
That won’t be possible of course. The default setters and getters just handle the assignment and loading of a compiler-generated backing field. So we somehow need to add the code for each property.
Enter Bindable objects
Similar to class NotificationObject, Bindable is here to help implementing INotifyPropertyChanged. The NotificationObject class only implements RaisePropertyChanged() but does not help with the implementation of the properties. Here is what you can do with Bindable:
1 2 3 4 5 6 |
public class Contact : Bindable { public string FirstName { get { return Get<string>(); } set { Set(value); } } } |
Noticeably shorter, isn’t it ?
Behind the scene, Bindable uses a dictionary to store the property values. You probably have noticed that the property name is not given to Bindable.Get() or Bindable.Set(). Bindable leverages the compiler to provide the value automatically:
1 2 |
protected T Get<T>([CallerMemberName] string name = null) { } protected void Set<T>(T value, [CallerMemberName] string name = null) { } |
CallerMemberNameAttribute when applied on an optional parameter instructs the compiler to pass a string whose value is the name of the calling member. So when property FirstName calls Get<string>() , the compiler generates code for Get<string>("FirstName") .
Here is the actual code for class Bindable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; namespace TiMoch.Framework { /// <summary> /// Base class to implement object that can be bound to /// </summary> public class Bindable : INotifyPropertyChanged { private Dictionary<string, object> _properties = new Dictionary<string, object>(); /// <summary> /// Gets the value of a property /// </summary> /// <typeparam name="T"></typeparam> /// <param name="name"></param> /// <returns></returns> protected T Get<T>([CallerMemberName] string name = null) { Debug.Assert(name != null, "name != null"); object value = null; if (_properties.TryGetValue(name, out value)) return value == null ? default(T) : (T)value; return default(T); } /// <summary> /// Sets the value of a property /// </summary> /// <typeparam name="T"></typeparam> /// <param name="value"></param> /// <param name="name"></param> protected void Set<T>(T value, [CallerMemberName] string name = null) { Debug.Assert(name != null, "name != null"); if (Equals(value, Get<T>(name))) return; _properties[name] = value; OnPropertyChanged(name); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } } |
Some aspects can be improved. For example, enabling subclasses to provide their own backing field. This is left as an exercise to the reader
I use this class a lot when implementing MVVM either in Wpf or Winforms.
What do you think ?
Edit: fixed code snippets as per Krumelur’s comments. Thanks Krumelur.