Monday, September 7, 2009

Binding data with INotifyPropertyChanged

Wpf can only bind to properties, and in order to let it automatically refresh the UI when a bound data item changes value, it must implement the INotifyPropertyChanged property.
This means that the following code does not suffice :
using System;
namespace JiraWpf.DataObjects
{
public class Person
{
public Person()
{
}
public string Name { get; set; }
}
}

So let's implement the interface, this leads us to the following :
using System;
using System.ComponentModel;

namespace JiraWpf.DataObjects
{
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

public Person()
{ }

private string name;
public string Name
{
get { return name; }
set
{
if (value != this.name)
{
this.name = value;
OnPropertyChanged(new PropertyChangedEventArgs("Name"));
}
}
}

private void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, args);
}
}
}

Not a big change, but if you have a lot of properties, this is very bad. We came from a 1 line property implementation to a 13 line !! The lines for the event and raising it are common for all properties, so they are not counted. Every property compares its value with the previous value, and when those 2 are different it raise the change-event. This is functionality that is the same for all properties so it belongs somewhere else. It would be handy if an attribute NotifyOnChange could be set on a property, but this does not exist (as far as I know). Meaning this comparison must be done in a self made base class : NotifyingObject, leading us to the following :
using System;
using System.ComponentModel;
using System.Collections.Generic;

namespace JiraWpf.DataObjects
{
public class NotifyingObject : INotifyPropertyChanged
{
private Dictionary<string, IComparable> props;

public event PropertyChangedEventHandler PropertyChanged;

public NotifyingObject()
{
props = new Dictionary<string, IComparable>();
}

public void SetValue<T>(string name, T value) where T : IComparable
{
if (!props.ContainsKey(name))
{
props.Add(name, value);
}
else
{
if (props[name].CompareTo(value) != 0)
{
props[name] = value;
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
}

public T GetValue<T>(string name) where T : IComparable
{
return (T)props[name];
}

protected void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, args);
}
}
}
using System;
using System.ComponentModel;

namespace JiraWpf.DataObjects
{
public class Person : NotifyingObject
{
public Person()
{ }

public string Name
{
get { return base.GetValue<string>("Name"); }
set { base.SetValue("Name", value); }
}
}
}

Ok, this is already much better, property definitions are back to just 5 lines, but at least it is readable again. The NotifyingObject has a list of the property names and their values, so on a get, we look up the value in the list. On a set we compare with the value in the list, and when the 2 are different, we raise the event.
Now what still bothers me on this is that we type the property name 3 times in the Person class! Once in the property name itself, secondly in the get and another time in the set. This is asking for trouble on refactoring (renaming the property). No tool will change those names in the get and set, they are hardcoded string values ! This leads us to the following implementation of the 2 classes :
using System;
using System.ComponentModel;
using System.Collections.Generic;

namespace JiraWpf.DataObjects
{
public class NotifyingObject : INotifyPropertyChanged
{
private Dictionary<string, IComparable> props;

public event PropertyChangedEventHandler PropertyChanged;

public NotifyingObject()
{
props = new Dictionary<string, IComparable>();
}

public void SetValue<T>(T value) where T : IComparable
{
string name = GetCallerPropertyName();

if (!props.ContainsKey(name))
{
props.Add(name, value);
}
else
{
if (props[name].CompareTo(value) != 0)
{
props[name] = value;
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
}

public T GetValue<T>() where T : IComparable
{
string name = GetCallerPropertyName();
return (T)props[name];
}

protected void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, args);
}

private string GetCallerPropertyName()
{
System.Diagnostics.StackTrace stack = new System.Diagnostics.StackTrace();
System.Diagnostics.StackFrame currentFrame = stack.GetFrame(2);

//strip of the set_ and get_ prefixes of the generated property names, to get the ones you typed
string propname = currentFrame.GetMethod().Name.Substring(4);

return propname;
}
}
}
using System;
using System.ComponentModel;

namespace JiraWpf.DataObjects
{
public class Person : NotifyingObject
{
public Person()
{ }

public string Name
{
get { return base.GetValue<string>(); }
set { base.SetValue(value); }
}
}
}

We let the NotifyingObject look up the property name itself, preventing us from ever typing in a wrong name. This is now safe for refactoring.
Another benefit of the NotifyingObject is that you could foresee a way to postpone the raise event till all properties are set, a BeginUpdate and EndUpdate function could foresee that. Implementing extra logging is now also much easier. Anyway, with this baseclass ready, I can start working on the next part.
Stay tuned ....

No comments:

Post a Comment