承载简单数据绑定






4.88/5 (6投票s)
展示了如何同时作为数据源和绑定主机参与数据绑定
引言
数据绑定是 Windows Forms 和 WPF 中存在的一种机制。本文的主题——简单数据绑定——是指托管对象上的属性与数据源对象上的属性之间建立的活动连接。托管对象通常是用户界面 Control
,而数据源对象经常是用户界面正在操作的业务对象。这种连接确保一个对象中的更改能够反映在另一个对象中的更改。
Control
对象提供的数据绑定非常有用:它避免了编写代码来从业务对象填充 Control
对象的内容,以及在用户与 Control
对象交互时更新业务对象。然而,用户界面并非唯一可以从数据绑定中受益的领域。业务对象本身常常是其他最终对象的便捷表示,例如数据库中的行。在这种情况下,业务对象必须提供代码来从数据库填充业务对象,并在业务对象更改时更新数据库。让业务对象本身托管数据绑定可以避免提供此类代码的需求。
本文将展示任何类别的对象如何托管数据绑定,并提供一个包含所需基础架构的 abstract
基类。一个简单的应用程序通过实现一个相互更新的四个对象的链来演示基类的使用。
背景
简单数据绑定将 *托管* 对象(通常是 System.Windows.Forms.Control
类的实例)的指定属性与 *数据源* 对象的指定属性连接起来。通过将 System.Windows.Forms.Binding
对象添加到托管对象维护的绑定集合 System.Windows.Forms.ControlBindingsCollection
中来创建数据绑定。对于 Control
对象,此集合由 DataBindings
属性返回。Binding
对象通常是通过 ControlBindingsCollection
对象提供的便捷方法创建的,而不是使用 new
运算符。典型的 Binding
创建可能如下所示:
NameTextbox.DataBindings.Add ("Text", _customer, "Value");
这会将对象 NameTextbox
的 Text
属性绑定到对象 _customer
的 Value
属性。在此绑定中,NameTextbox
是托管者,_customer
是数据源。
当绑定首次建立时,数据源对象的指定属性将复制到托管对象的指定属性。此后,对象的指定属性可以自动或手动更新。
- 绑定对象的
ControlUpdateMode
属性决定了托管对象何时自动更新。值为ControlUpdateMode.Never
表示托管对象不应自动更新。值为ControlUpdateMode.OnPropertyChanged
(默认值)表示每当数据源对象为绑定属性发布属性更改事件时,都会更新托管对象。 - 绑定对象的
DataSourceUpdateMode
属性决定了数据源对象何时自动更新。值为DataSourceUpdateMode.Never
表示数据源对象不应自动更新。值为DataSourceUpdateMode.OnValidation
(默认值)指定每当托管对象发布Validated
事件时,都会更新数据源对象。值为DataSourceUpdateMode.OnPropertyChanged
指定每当托管对象为绑定属性发布属性更改事件时,都会更新数据源对象。 - 绑定对象的
ReadValue
方法从数据源对象更新托管对象。 - 绑定对象的
WriteValue
方法从托管对象更新数据源对象。
绑定对象可以解释两种类型的属性更改事件:由 System.ComponentModel.INotifyPropertyChanged
接口定义的 PropertyChanged
事件,或者属性特定的 *propertyNameChanged
事件,例如 Control
对象实现的事件。PropertyChanged
事件是首选。
参与数据绑定
数据绑定中有两种可用角色:数据源和绑定主机。本节讨论支持这两种角色所需的代码。
作为数据源参与
对象可以通过发布属性更改事件来简单地成为数据源。System.ComponentModel.INotifyPropertyChanged
接口是发布属性更改事件的首选机制。此接口定义了 PropertyChanged
事件。订阅 PropertyChanged
事件需要 PropertyChangedEventHandler
委托,该委托又使用 PropertyChangedEventArgs
事件参数。PropertyChangedEventArgs
类定义了一个 string
类型的 PropertyName
属性,其中包含已更改属性的名称。PropertyName
属性可以为 null
或 string.Empty
,表示发布对象的全部属性已更改。
属性更改事件只能在属性值实际更改时发布,而不是仅仅在属性被赋值时发布。一个典型的不可绑定属性可能实现如下:
public string Value { get; set; }
这会指示编译器生成后备存储和简单的 get
和 set
访问器。
同一个属性的“工业级”可绑定版本可能如下所示:
/// <summary>
/// The name of the Value property.
/// </summary>
public static readonly string ValuePropertyName = "Value";
/// <summary>
/// Backing storage for the Value property.
/// </summary>
protected string _value;
/// <summary>
/// Gets or sets the string value of the object.
/// </summary>
public string Value
{
get
{
EnsureNotDisposed ();
return _value;
}
set
{
EnsureNotDisposed ();
bool change = !string.Equals (_value, value);
if (change)
{
_value = value;
PublishPropertyChangedEvent (ValuePropertyName);
}
}
}
此版本的属性解决了许多问题。
- 创建绑定时,属性名称以文本字符串形式指定。错误只能在运行时捕获,此时绑定会引发异常。
ValuePropertyName
成员提供一个用于属性名称的string
,可用于创建绑定。这可以防止在绑定站点或属性更改发布站点出现拼写错误。 - 对
EnsureNotDisposed()
的调用是处置模式的一部分。通常,业务对象在其继承链中拥有IDisposable
接口,以及一个提供处置、最终化等钩子的基类,包括一个确定对象是否已被处置的检查。此处假定确定对象是否已被处置的检查是EnsureNotDisposed()
。 change
标志指示属性赋值是否是属性值的更改。此处将赋给属性的值与后备存储_value
进行比较。由于此属性不拒绝null
值,因此使用静态string.Equals
方法允许_string
或赋给value
的值均为null
。单独计算change
标志的模式便于引入更复杂的更改确定。例如,业务对象可能希望将null
和string.Empty
视为等效。- 最后,如果属性赋值导致属性值发生更改,则会更新后备存储并发布属性更改事件。此处,发布委托给一个名为
PublishPropertyChangedEvent
的方法,该方法接受发生值更改的属性的名称。
事件发布最佳实践需要几个不那么明显的步骤。不能调用 null
委托,因此必须在检查之前复制 EventHandler
订阅者列表null
null
和委托调用之间发生可能出现的竞用条件,这种情况可能发生在最后一个订阅者取消订阅事件时。EventChangedEventArgs
对象仅在非 null
订阅者列表存在时才需要构造。订阅者列表必须扩展为单独的委托,并单独调用每个委托,捕获并丢弃异常:这可以防止有缺陷的事件处理程序过早地终止事件处理程序的调用。将实际属性更改事件的发布委托给 PublishPropertyChangedEvent
方法,可以遵循这些最佳实践,而不会弄乱各个属性。
作为绑定主机参与
对象可以通过实现 System.Windows.Forms.IBindableComponent
接口来成为绑定主机。此接口定义了两个属性:BindingContext
和 DataBindings
。实现类只需初始化这些属性;此后,无需与它们进行任何交互。从基类继承这些属性是一个非常有吸引力的选择。
BindingContext
属性的类型为 BindingContext
。BindingContext
对象是 BindingManagerBase
对象的集合。BindingManagerBase
对象是 CurrencyManager
对象或 PropertyManager
对象。每个 CurrencyManager
对象管理一个单独的对象列表。当需要将多个控件显示同一列表元素的不同属性时,这一点很重要。然而,对于本文讨论的简单数据绑定,只涉及 PropertyManager
对象。由于 PropertyManager
对象是可互换的,因此每个绑定主机的 BindingContext
属性可以是一个新分配的 BindingContext
对象。在绑定主机对象集合之间共享单个 BindingContext
集合只会影响存储消耗。本文附带的代码允许这种共享,方法是实现 BindingContext
属性为读/写。
DataBindings
属性的类型为 ControlBindingsCollection
。它必须初始化为一个新创建的 ControlBindingsCollection
对象。ControlBindingsCollection
构造函数需要对绑定主机的引用。
数据绑定对它们的托管对象没有特殊的访问权限或了解。因此,为了让绑定能够感知到托管对象的变化,托管对象必须发布属性更改事件。绑定会像订阅数据源对象的属性更改事件一样订阅其托管对象的属性更改事件。因此,托管对象应该同时实现 INotifyPropertyChanged
接口和 IBindableComponent
接口,并为所有感兴趣的属性实现 PropertyChanged
通知,以便它们可以成为数据源。
顾名思义,IBindableComponent
接口继承自 System.ComponentModel.IComponent
接口。因此,托管数据绑定的类必须实现 IComponent
接口。IComponent
接口定义了 Disposed
事件和 Site
属性。IComponent
接口继承自 IDisposable
接口,该接口定义了 Dispose()
方法。标准处置框架可以从 System.ComponentModel.Component
类继承。
参与摘要
在许多应用程序中,应用程序类层次结构的基类实现了 IComponent
和 INotifyPropertyChanged
。实现这些接口可以使对象在 Visual Studio 用户界面设计器中被操作,并作为用户界面的数据源参与数据绑定。在这类应用程序中,对象已经支持作为数据源参与数据绑定。添加作为绑定主机参与数据绑定的支持仅需要实现 IBindableComponent
接口,该接口向对象添加了两个简单的属性。
实现数据绑定
作为数据源或绑定主机的数据绑定所需的接口几乎可以完全在一个基类中实现。本文附件中的示例代码中的 BindableComponent abstract
类要求派生类仅在属性更改发生时进行通知;所有其他支持均由基类提供。
IComponent
、IDisposable
和 INotifyPropertyChanged
的实现代码都是标准的,不受数据绑定框架的影响。对象应参与应用程序的标准处置框架和标准事件发布框架。本文附件中的示例代码中的 ApplicationBaseClass abstract
类中包含了这些框架的典型实现。
给定一个实现 INotifyPropertyChanged
的对象,实现 IBindableComponent
是直接的:它只有两个简单的属性。这些属性的实现可以委托给一个实用程序类,并由任何需要此支持的对象继承。本文附件中的示例代码中的 BindableComponent
类中包含了此接口的典型实现。
实现 IBindableComponent
有两个小问题。第一个是 BindingContext
实例是否要共享。绑定上下文是绑定管理器对象的集合。在用户界面中使用时,Control
对象通常会发现其包含的控件并使用其 BindingContext
。BindableComponent
不是 Control
,也不需要容器。BindableComponent
类将在需要时创建自己的 BindingContext
,或者其他代码可以为其 BindingContext
属性赋值。本文随附的示例程序将主窗体的 BindingContext
分配给示例组件托管的数据绑定。
第二个小问题在于数据绑定的默认更新模式。ControlBindingsCollection
对象上的 DefaultDataSourceUpdateMode
属性提供了添加到集合的绑定的默认 DataSourceUpdateMode
;该属性决定了托管对象的变化导致数据源对象发生变化的条件。默认值为 DataSourceUpdateMode.OnValidation
,表示更新将在验证后发生。非 Control
对象的继承类可能希望将 DefaultDataSourceUpdateMode
属性设置为 DataSourceUpdateMode.OnPropertyChanged
。本文随附的示例程序就是这样做的。
示例程序
示例程序展示了数据绑定在一个由四个对象组成的链中发生。两个端对象是 System.Windows.Forms.TextBox
控件。链中的两个内部对象是 IntermediateObject
对象;IntermediateObject
是一个示例 BindableComponent
实现。IntermediateObject
对象还将它们的值显示在额外的(只读)TextBox
控件上,以显示其状态。
两个 IntermediateObject
对象作为名为 IntermediateObject1
和 IntermediateObject2
的组件添加到主窗体中。对象之间的数据绑定在主窗体的 OnLoad
重写方法中设置。关键行如下:
TextboxA.DataBindings.Add ("Text", IntermediateObject1, "Value");
IntermediateObject1.BindingContext = BindingContext;
IntermediateObject1.DataBindings.Add ("Value", IntermediateObject2, "Value");
TextboxB.DataBindings.Add ("Text", IntermediateObject2, "Value");
这设置了三个数据绑定来桥接四个组件之间的连接。主窗体的绑定上下文用于 IntermediateObject
对象绑定的上下文。
运行该应用程序将显示一个带有四个 TextBox
控件的简单窗体。您可以将数据输入标记为“Textbox A”和“Textbox B”的 TextBox
控件中。这些是代码中引用的控件。“Intermediate 1”和“Intermediate 2”标记的 TextBox
控件显示了对 IntermediateObject1
和 IntermediateObject2
中间对象的视图。这些 TextBox
控件不是通过数据绑定更新的,而是通过 IntermediateObject1
和 IntermediateObject2
组件的显式操作更新的。
在标记为 Textbox A 的 TextBox
中输入数据,然后按 TAB 键离开该控件,会得到一个类似截图所示的屏幕。

请注意,在 Textbox A 中输入的值如何通过 TextboxA
托管的绑定复制到 IntermediateObject1
,然后通过 IntermediateObject1
托管的绑定复制到 IntermediateObject2
,最后通过 TextboxB
托管的绑定复制到 TextboxB
。
在标记为 Textbox B 的 TextBox
中输入数据,然后按 TAB 键离开该控件,也会类似地导致数据复制。在 Textbox B 中输入的值将通过 TextboxB
托管的绑定复制到 IntermediateObject2
,然后通过它托管的绑定复制到 IntermediateTextbox1
,最后通过它托管的绑定复制到 TextboxA
。
摘要
简单数据绑定是一种机制,通过该机制,一个对象的属性可以自动复制到另一个对象的属性。当这两个对象是同一底层数据的两个视图时,这非常有用。作为数据源参与数据绑定的能力涉及实现 INotifyPropertyChanged
接口。作为绑定主机参与数据绑定的能力还需要额外实现 IBindableComponent
接口。本文随附的代码展示了此类实现的一个示例。该实现被打包在一个 abstract
基类中,允许任何派生类托管数据绑定。
致谢
作者非常感谢 A123 Systems, Inc. 的 Bruce Carlson,他支持了本文所述软件和本文本身的开发。
历史
- 2010 年 10 月 1 日:文章原始版本