Introduction
Very often, you need a grid to display some nested property or properties, or some calculated value. Normally, you should create a class that you will use to bind a grid, and expose all values that you want to bind to as properties. You also need to implement
INotifyPropertyChanged
for each property that should update the UI. This is a lot of extra work. I will show how to use a
BindingProxy
class I wrote to simplify this task.
The Problem
We want to display the full Weather Month composed of 31 Weather Days in one row of a DataGrid. Each Weather Day has 3 properties – editable High and Low Temperatures,
and a calculated Average Temperature. In addition to those properties, we want to display Max and Min Temperature for the whole month.
The values should be calculated and updated in the grid after the user edits a value. The screenshot below is illustrating the goal we want
to achieve: when Day 1 Hi temperature is updated by user, the Max and Day 1 Avg should be automatically recalculated and updated by the application.
We don’t want to use DataTable
classes; we want to use our own classes:
The Solution
Having 31 days and 3 properties per day will result in 93 properties that we want to display for daily temperatures. Defining 93 properties in a class is not extremely hard,
but is definitely tedious. Since we want to update the UI when the user changes the values, we need to implement
INotifyPropertyChanged
for all calculated properties
at least. This makes the task even more challenging.
After googling the web and trying different approaches, I found that I need to use
ITypedList
interface,
which is used for binding by controls. It allows you to define properties you want to bind to at runtime.
The idea is to construct an object that will report the list of properties which will actually point to nested properties of the object. The trick is not to create new property
descriptors (which is not trivial task), but to use the property descriptors from the existing objects.
BindingProxyPropertyDescriptor
In pursuit of this idea, I wrote the BindingPrope
rtyDescriptor
class that uses the original property descriptor and assessor to access the property we want to bind.
The trickiest part is substituting value-related properties with properties read from the real object instance, which is accessed through an accessor supplied in the constructor.
public class BindingPropxyPropertyDescriptor<T> : PropertyDescriptor
{
private readonly Func<T, object> _getter;
private readonly PropertyDescriptor _source;
public BindingPropxyPropertyDescriptor(string name)
: base(name, null)
{
}
public BindingPropxyPropertyDescriptor(string name,
PropertyDescriptor source, Func<T, object> getter)
: base(name, null)
{
_source = source;
_getter = getter;
}
public override Type ComponentType
{
get { return _source.ComponentType; }
}
public override Type PropertyType
{
get { return _source.PropertyType; }
}
public override bool IsReadOnly
{
get { return _source.IsReadOnly; }
}
public override bool SupportsChangeEvents
{
get { return _source.SupportsChangeEvents; }
}
private object GetRealInstance(object component)
{
return _getter == null ? component : _getter((T)component);
}
public override bool CanResetValue(object component)
{
return _source.CanResetValue(GetRealInstance(component));
}
public override object GetValue(object component)
{
return _source.GetValue(GetRealInstance(component));
}
public override void ResetValue(object component)
{
_source.ResetValue(GetRealInstance(component));
}
public override void SetValue(object component, object value)
{
_source.SetValue(GetRealInstance(component), value);
}
public override bool ShouldSerializeValue(object component)
{
return _source.ShouldSerializeValue(GetRealInstance(component));
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
_source.RemoveValueChanged(GetRealInstance(component), handler);
}
public override void AddValueChanged(object component, EventHandler handler)
{
_source.AddValueChanged(GetRealInstance(component), handler);
}
}
BindingProxyList
Then I created a class BindingProxyList<T>
inherited from
BindingList
and ITypedList
interface. BindingProxyList
stores
a collection of BindingPropertyDescriptor
. To add a new property I created
AddMember
method. It creates a new property descriptor and adds it to the properties collection.
AddMemember
adds one property, and AddMemembers
adds all properties from the passed object type. There are several methods that take different parameters, here is the one that describes the idea the best:
public void AddMember<TObject, TProperty>(string name, Expression<Func<T, TObject>> propertyObjectSelector,
Expression<Func<TObject, TProperty>> propertySelector)
{
var propertyInfo = BindingHelpers.GetPropertyInfo(propertySelector);
var propertyDescriptor = TypeDescriptor.GetProperties(propertyInfo.DeclaringType)[propertyInfo.Name];
var getter = BindingHelpers.CastToObject(propertyObjectSelector).Compile();
var proxyPropertyDescriptor = new BindingPropxyPropertyDescriptor(name, propertyDescriptor, getter);
_properties.Add(name, proxyPropertyDescriptor);
}
The list of properties is read through ITypedList
interface. The implementation is fairly simple:
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
var values = _properties.Values.Cast<PropertyDescriptor>().ToArray();
var properties = new PropertyDescriptorCollection(values);
return properties;
}
public string GetListName(PropertyDescriptor[] listAccessors)
{
return null;
}
At this point, we have a working BindingProxyList<T>
class which can be used in the following way: We add all properties from
WeatherMonthViewModel
and all properties from each WeatherDayViewModel
, adding day number in the suffix, so when we
will bind the object, we will refer the properties like HighTemperature1, HighTemperature2 and so on:
WeatherMonthModels = new BindingProxyList<WeatherMonthViewModel>();
WeatherMonthModels.AddMembers();
for (int day = 1; day <= 31; day++)
{
var dayLocal = day;
WeatherMonthModels.AddMembers("", day.ToString(CultureInfo.InvariantCulture) ,
x=> x.WeatherDays[dayLocal - 1]);
}
Then this list could be bound to the grid as follows:
private void BindGrid()
{
dataGridView1.AutoGenerateColumns = false;
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn {DataPropertyName = "Location", HeaderText = "Location"});
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn {DataPropertyName = "Year", HeaderText = "Year"});
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn {DataPropertyName = "Month", HeaderText = "Month"});
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn {DataPropertyName = "MaxTemperature", HeaderText = "Max"});
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn {DataPropertyName = "MinTemperature", HeaderText = "Min"});
for (int day = 1; day <= 31; day++)
{
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn
{DataPropertyName = string.Format("LowTemperature{0}", day), HeaderText
= string.Format("Day {0} Lo ", day)});
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn
{DataPropertyName = string.Format("HighTemperature{0}", day),
HeaderText = string.Format("Day {0} Hi ", day)});
dataGridView1.Columns.Add(new DataGridViewTextBoxColumn
{DataPropertyName = string.Format("AverageTemperature{0}", day),
HeaderText = string.Format("Day {0} Avg ", day)});
}
dataGridView1.DataSource = _model.WeatherMonthModels;
}
At this point, the grid is bound and working, but it does not respond to
WeatherDayViewModel
events properly. This is because we aren’t propagating events from
WeatherDayViewModel
to WeatherMonthViewModel
. To solve this, I created a third class called
BindingProxy
.
BindingProxy
public class BindingProxy<T> : INotifyPropertyChanged
where T: class
{
public event PropertyChangedEventHandler PropertyChanged;
public BindingProxy(T item)
{
if(item == null)
throw new ArgumentNullException("item");
Item = item;
}
public T Item { get; private set; }
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public void RaiseNotifyPropertyChanged(string propertyName)
{
OnPropertyChanged(propertyName);
}
}
The main task of the BindingProxy
class is to enable NotifyPropertyChanged
event re-raising by calling
RaiseNotifyPropertyChanged
.
Code Usage
Now the usage of
BindinProxyList
will look as shown. Instead of creating WeatherMonthViewModel
directly, we first create
BindingProxy
of WeatherMonthViewModel
type, and add all properties from
WeatherMonthViewModel
and then add all properties from each WeatherDayViewModel
in the WeatherDays
collection:
WeatherMonthModels = new BindingProxyList<BindingProxy<WeatherMonthViewModel>>();
WeatherMonthModels.AddMembers(x => x.Item);
for (int day = 1; day <= 31; day++)
{
var dayLocal = day;
WeatherMonthModels.AddMembers("", day.ToString(CultureInfo.InvariantCulture) , x=> x.Item.WeatherDays[dayLocal - 1]);
}
We also need to push the events from WeatherDayViewModel
and
WeatherMonthViewModel
to the proxy object to make the grid consume it. This can be done as follows. We will handle all the events that we are interested in and use the
RaiseNotifyPropertyChanged
method in the proxy class. In addition, we need to notify the proxy object about corresponding
MaxTemperature
and MinTemperature
changes when the daily high or low is changed:
private void HandleEvents(BindingProxy<WeatherMonthViewModel> proxy)
{
proxy.Item.PropertyChanged += (o, e) => proxy.RaiseNotifyPropertyChanged(e.PropertyName);
for (int day = 1; day <= 31; day++)
{
var dayLocal = day;
var weatherDayModel = proxy.Item.WeatherDays[dayLocal-1];
weatherDayModel.PropertyChanged += (o, e) =>
{
proxy.RaiseNotifyPropertyChanged(string.Format("{0}{1}", e.PropertyName, dayLocal));
proxy.RaiseNotifyPropertyChanged("MaxTemperature");
proxy.RaiseNotifyPropertyChanged("MinTemperature");
};
}
}
The last thing required is to enable the addition of a new record. This requires handling of the
AddingNew
event of BindingList
.
The implementation is simple; we just need to create a new object:
WeatherMonthModels.AddingNew += WeatherMonthViewModelsAddingNew;
private void WeatherMonthViewModelsAddingNew(object sender, AddingNewEventArgs e)
{
var weatherMonthModel = new WeatherMonthViewModel {Year = 0, Month = 0, Location = "New Location"};
var proxy = new BindingProxy<WeatherMonthViewModel>(weatherMonthModel);
HandleEvents(proxy);
e.NewObject = proxy;
}
Now the grid is fully functional. It responds to all value changes, and it didn’t require coding one hundred properties manually.
Another quick sample
The same approach can be used to bind the objects from any nested or separate objects. Suppose that day is split by hours, and we want to display
HighTemperature
for all hours in the month in one row of the grid. Then
WeatherDay
would have a collection of hours, which could be bound like shown below. This time we will use
AddMemeber
method, since we want to add only HighTemperature
property. This would create 31 (days) x 24 (hours) = 744 bindable properties named HighTemperature_1_1…HighTemperature_31_24:
for( day = 1; day <= 31; day++)
{
var dayLocal = day;
for (int hour = 1; hour <= 24; hour++)
{
var hourLocal = hour;
WeatherMonthModels.AddMember(
string.Format("HighTemperature_{0}_{1}", dayLocal, hourLocal) ,
x => x.WeatherDays[dayLocal - 1].Hours[hourLocal-1], x=>x.HighTemperature);
}
}
I have used this approach in my projects, and it has worked well so far.
The full source code is attached.
History
- 4/11/2013: Initial version.