Windows 窗体:通过 ITypedList 接口进行绑定
一种通过 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>
的类,它继承自 BindingList
和 ITypedList
接口。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]);
}
我们还需要将 WeatherDayViewModel
和 WeatherMonthViewModel
的事件推送到代理对象,以便网格能够消耗它们。这可以通过以下方式完成。我们将处理所有我们感兴趣的事件,并在代理类中使用 RaiseNotifyPropertyChanged
方法。此外,当每日最高或最低温度改变时,我们需要将相应的 MaxTemperature
和 MinTemperature
的变化通知给代理对象。
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");
};
}
}
最后需要启用添加新记录的功能。这需要处理 BindingList
的 AddingNew
事件。实现很简单,我们只需要创建一个新对象。
//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:初始版本。