65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 窗体:通过 ITypedList 接口进行绑定

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2013年4月11日

CPOL

5分钟阅读

viewsIcon

23700

downloadIcon

423

一种通过 ITypedList 接口绑定 DataGrid 的方法。

引言

很多时候,你需要一个网格来显示某个嵌套的属性或属性,或者某个计算值。通常,你应该创建一个类来绑定网格,并将所有你想绑定的值暴露为属性。你还需要为每个应该更新 UI 的属性实现 INotifyPropertyChanged。这需要大量的额外工作。我将展示如何使用我编写的 BindingProxy 类来简化这项任务。

问题所在 

我们希望在一个 DataGrid 行中显示由 31 个 Weather Day 组成的完整的 Weather Month。每个 Weather Day 有 3 个属性 – 可编辑的最高和最低温度,以及计算出的平均温度。除了这些属性,我们还希望显示整个月的最高和最低温度。当用户编辑值后,这些值应该被计算并更新到网格中。下面的截图说明了我们想要实现的目标:当用户更新 Day 1 的最高温度时,Max 和 Day 1 Avg 应该由应用程序自动重新计算和更新。我们不想使用 DataTable 类;我们想使用我们自己的类:

解决方案 

拥有 31 天和每个天 3 个属性将导致我们需要显示 93 个关于日温度的属性。在一个类中定义 93 个属性并不是极其困难,但绝对是乏味的。由于我们希望在用户更改值时更新 UI,我们至少需要为所有计算出的属性实现 INotifyPropertyChanged。这使得任务更具挑战性。

在谷歌搜索和尝试了不同的方法后,我发现我需要使用 ITypedList 接口,该接口用于控件的绑定。它允许你在运行时定义要绑定的属性。其思想是构造一个对象,该对象将报告实际指向对象嵌套属性的属性列表。诀窍不是创建新的属性描述符(这并非易事),而是使用现有对象的属性描述符。

BindingProxyPropertyDescriptor

本着这个想法,我编写了 BindingPropertyDescriptor 类,它使用原始的属性描述符和访问器来访问我们想要绑定的属性。最棘手的部分是用从真实对象实例读取的属性替换与值相关的属性,该对象实例通过构造函数中提供的访问器进行访问。

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

然后我创建了一个名为 BindingProxyList<T> 的类,它继承自 BindingListITypedList 接口。BindingProxyList 存储 BindingPropertyDescriptor 的集合。要添加新属性,我创建了 AddMember 方法。它创建一个新的属性描述符并将其添加到属性集合中。AddMember 添加一个属性,而 AddMembers 添加所有从传入的对象类型中获取的属性。有几个方法接受不同的参数,下面这个方法最能说明这个想法:

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);
}

属性列表是通过 ITypedList 接口读取的。实现相当简单:

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;
}

此时,我们有了一个可用的 BindingProxyList<T> 类,可以按如下方式使用:我们添加 WeatherMonthViewModel 的所有属性以及每个 WeatherDayViewModel 的所有属性,在后缀中添加日期编号,因此当我们绑定对象时,我们将使用 HighTemperature1、HighTemperature2 等属性进行引用。

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]);
}

然后,列表可以像这样绑定到网格:

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;
}

此时,网格已绑定并正常工作,但它不能正确响应 WeatherDayViewModel 事件。这是因为我们没有将事件从 WeatherDayViewModel 传播到 WeatherMonthViewModel。为了解决这个问题,我创建了第三个类,名为 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);
    }
}

BindingProxy 类的主要任务是通过调用 RaiseNotifyPropertyChanged 来启用 NotifyPropertyChanged 事件的重新引发。

代码使用

现在,BindingProxyList 的使用将如下所示。我们不是直接创建 WeatherMonthViewModel,而是首先创建 WeatherMonthViewModel 类型的 BindingProxy,然后添加 WeatherMonthViewModel 的所有属性,再添加 WeatherDays 集合中每个 WeatherDayViewModel 的所有属性:

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]);
}

我们还需要将 WeatherDayViewModelWeatherMonthViewModel 的事件推送到代理对象,以便网格能够消耗它们。这可以通过以下方式完成。我们将处理所有我们感兴趣的事件,并在代理类中使用 RaiseNotifyPropertyChanged 方法。此外,当每日最高或最低温度改变时,我们需要将相应的 MaxTemperatureMinTemperature 的变化通知给代理对象。

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");
        };
    }
}

最后需要启用添加新记录的功能。这需要处理 BindingListAddingNew 事件。实现很简单,我们只需要创建一个新对象。

//Subscribe adding new event.
WeatherMonthModels.AddingNew += WeatherMonthViewModelsAddingNew;
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;
}

现在网格功能齐全。它响应所有值更改,并且不需要手动编写一百个属性。

另一个快速示例

同样的方​​法可用于绑定来自任何嵌套或独立对象的对象。假设一天被分成几个小时,我们希望在网格的同一行中显示一个月内所有小时的 HighTemperature。那么 WeatherDay 将有一个小时的集合,可以像下面这样绑定。这次我们将使用 AddMember 方法,因为我们只想添加 HighTemperature 属性。这将创建 31(天)x 24(小时)= 744 个可绑定属性,命名为 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);
    }
}

我在我的项目中使用了这种方法,并且到目前为止效果都很好。

完整的源代码已附上。

历史

  • 2013/4/11:初始版本。
© . All rights reserved.