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

承载简单数据绑定

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (6投票s)

2010 年 10 月 1 日

CPOL

12分钟阅读

viewsIcon

26355

downloadIcon

797

展示了如何同时作为数据源和绑定主机参与数据绑定

引言

数据绑定是 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");

这会将对象 NameTextboxText 属性绑定到对象 _customerValue 属性。在此绑定中,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 属性可以为 nullstring.Empty,表示发布对象的全部属性已更改。

属性更改事件只能在属性值实际更改时发布,而不是仅仅在属性被赋值时发布。一个典型的不可绑定属性可能实现如下:

public string Value { get; set; }

这会指示编译器生成后备存储和简单的 getset 访问器。

同一个属性的“工业级”可绑定版本可能如下所示:

/// <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 标志的模式便于引入更复杂的更改确定。例如,业务对象可能希望将 nullstring.Empty 视为等效。
  • 最后,如果属性赋值导致属性值发生更改,则会更新后备存储并发布属性更改事件。此处,发布委托给一个名为 PublishPropertyChangedEvent 的方法,该方法接受发生值更改的属性的名称。

事件发布最佳实践需要几个不那么明显的步骤。不能调用 null 委托,因此必须在检查之前复制 EventHandler 订阅者列表null:这可以避免在检查 null 和委托调用之间发生可能出现的竞用条件,这种情况可能发生在最后一个订阅者取消订阅事件时。EventChangedEventArgs 对象仅在非 null 订阅者列表存在时才需要构造。订阅者列表必须扩展为单独的委托,并单独调用每个委托,捕获并丢弃异常:这可以防止有缺陷的事件处理程序过早地终止事件处理程序的调用。将实际属性更改事件的发布委托给 PublishPropertyChangedEvent 方法,可以遵循这些最佳实践,而不会弄乱各个属性。

作为绑定主机参与

对象可以通过实现 System.Windows.Forms.IBindableComponent 接口来成为绑定主机。此接口定义了两个属性:BindingContextDataBindings。实现类只需初始化这些属性;此后,无需与它们进行任何交互。从基类继承这些属性是一个非常有吸引力的选择。

BindingContext 属性的类型为 BindingContextBindingContext 对象是 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 类继承。

参与摘要

在许多应用程序中,应用程序类层次结构的基类实现了 IComponentINotifyPropertyChanged。实现这些接口可以使对象在 Visual Studio 用户界面设计器中被操作,并作为用户界面的数据源参与数据绑定。在这类应用程序中,对象已经支持作为数据源参与数据绑定。添加作为绑定主机参与数据绑定的支持仅需要实现 IBindableComponent 接口,该接口向对象添加了两个简单的属性。

实现数据绑定

作为数据源或绑定主机的数据绑定所需的接口几乎可以完全在一个基类中实现。本文附件中的示例代码中的 BindableComponent abstract 类要求派生类仅在属性更改发生时进行通知;所有其他支持均由基类提供。

IComponentIDisposableINotifyPropertyChanged 的实现代码都是标准的,不受数据绑定框架的影响。对象应参与应用程序的标准处置框架和标准事件发布框架。本文附件中的示例代码中的 ApplicationBaseClass abstract 类中包含了这些框架的典型实现。

给定一个实现 INotifyPropertyChanged 的对象,实现 IBindableComponent 是直接的:它只有两个简单的属性。这些属性的实现可以委托给一个实用程序类,并由任何需要此支持的对象继承。本文附件中的示例代码中的 BindableComponent 类中包含了此接口的典型实现。

实现 IBindableComponent 有两个小问题。第一个是 BindingContext 实例是否要共享。绑定上下文是绑定管理器对象的集合。在用户界面中使用时,Control 对象通常会发现其包含的控件并使用其 BindingContextBindableComponent 不是 Control,也不需要容器。BindableComponent 类将在需要时创建自己的 BindingContext,或者其他代码可以为其 BindingContext 属性赋值。本文随附的示例程序将主窗体的 BindingContext 分配给示例组件托管的数据绑定。

第二个小问题在于数据绑定的默认更新模式。ControlBindingsCollection 对象上的 DefaultDataSourceUpdateMode 属性提供了添加到集合的绑定的默认 DataSourceUpdateMode;该属性决定了托管对象的变化导致数据源对象发生变化的条件。默认值为 DataSourceUpdateMode.OnValidation,表示更新将在验证后发生。非 Control 对象的继承类可能希望将 DefaultDataSourceUpdateMode 属性设置为 DataSourceUpdateMode.OnPropertyChanged。本文随附的示例程序就是这样做的。

示例程序

示例程序展示了数据绑定在一个由四个对象组成的链中发生。两个端对象是 System.Windows.Forms.TextBox 控件。链中的两个内部对象是 IntermediateObject 对象;IntermediateObject 是一个示例 BindableComponent 实现。IntermediateObject 对象还将它们的值显示在额外的(只读)TextBox 控件上,以显示其状态。

两个 IntermediateObject 对象作为名为 IntermediateObject1IntermediateObject2 的组件添加到主窗体中。对象之间的数据绑定在主窗体的 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 控件显示了对 IntermediateObject1IntermediateObject2 中间对象的视图。这些 TextBox 控件不是通过数据绑定更新的,而是通过 IntermediateObject1IntermediateObject2 组件的显式操作更新的。

在标记为 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 日:文章原始版本
© . All rights reserved.