Click here to Skip to main content
15,886,067 members
Articles / Programming Languages / C#

Windows Forms: Binding through ITypedList interface

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
11 Apr 2013CPOL5 min read 22.7K   384   18   4
An approach to bind datagrids through ITypedList interface.

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:   

Image 1

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 BindingProp<code>ertyDescriptor 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.

C#
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: 

C#
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:

C#
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
    // Return properties in sort order.
    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:

C#
WeatherMonthModels = new BindingProxyList<WeatherMonthViewModel>();
 
//Add properties from WeatherMonthViewModel.
WeatherMonthModels.AddMembers();

//Add properties from each WeatherDayViewModel.
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: 

C#
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

C#
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: 

C#
WeatherMonthModels = new BindingProxyList<BindingProxy<WeatherMonthViewModel>>();

//Add properties from WeatherMonthViewModel.
WeatherMonthModels.AddMembers(x => x.Item);

//Add properties from each WeatherDayViewModel.
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:

C#
private void HandleEvents(BindingProxy<WeatherMonthViewModel> proxy)
{
    //Handle PropertyChanged of WeatherMonthViewModel.
    proxy.Item.PropertyChanged += (o, e) => proxy.RaiseNotifyPropertyChanged(e.PropertyName);
    //Handle PropertyChanged of each WeatherDayViewModel.
    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:

C#
//Subscribe adding new event.
WeatherMonthModels.AddingNew += WeatherMonthViewModelsAddingNew;
C#
private void WeatherMonthViewModelsAddingNew(object sender, AddingNewEventArgs e)
{
    //Create new WeatherMonthViewModel.
    var weatherMonthModel = new WeatherMonthViewModel {Year = 0, Month = 0, Location = "New Location"};
    //Create binding proxy.
    var proxy = new BindingProxy<WeatherMonthViewModel>(weatherMonthModel);
    //Handle the events.
    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:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Latvia Latvia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionTried with this code , eventhough its not calling GetItemProperties, so the value is not updating in the UI. Pin
ravit235-May-20 0:03
ravit235-May-20 0:03 
AnswerRe: Tried with this code , eventhough its not calling GetItemProperties, so the value is not updating in the UI. Pin
xllance6-Dec-20 2:04
xllance6-Dec-20 2:04 
QuestionGetItemProperties method is not calling Pin
ravit234-May-20 20:23
ravit234-May-20 20:23 
QuestionInteresting solution Pin
rkorchagin11-Apr-13 20:25
rkorchagin11-Apr-13 20:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.