什么是数据绑定
此篇译文是针对.NET 2.0中的WinForm数据绑定
从一个Windows窗体的角度来看,“数据绑定”是一种把数据绑定到一种用户界面元素(控件)的通用机制。在Windows窗体中有两种数据绑定类型:简单绑定和复杂绑定。
简单绑定
简单绑定是将一个用户界面元素(控件)的属性绑定到一个类型(对象)实例上的某个属性的方法。例如,如果一个开发者有一个Customer类型的实例,那么他就可以把Customer的“Name”属性绑定到一个TextBox的“Text”属性上。“绑定”了这2个属性之后,对TextBox的Text属性的更改将“传播”到Customer的Name属性,而对Customer的Name属性的更改同样会“传播”到TextBox的Text属性。Windows窗体的简单数据绑定支持绑定到任何public或者internal级别的.NET Framework属性。
例子:对一个业务对象的简单数据绑定
/*******************************************************************
* 设置(使用VS的窗体设计器):
*
* 添加3个 TextBox到窗体Form(textBox1, textBox2 和textBox3)
* 添加下面的代码到Form.Load事件处理方法
******************************************************************/
/*******************************************************************
* 创建一个 customer 实例(使用下面的 Customer 类型)
******************************************************************/
Customer cust = newCustomer(0, "Mr. Zero", 10.0M);
/*******************************************************************
* 绑定textBox1, textBox2 和textBox3
******************************************************************/
this.textBox1.DataBindings.Add("Text", cust, "ID", true);
this.textBox2.DataBindings.Add("Text", cust, "Name", true);
this.textBox2.DataBindings.Add("Text", cust, "Rate", true);
Customer业务对象的定义:
/*******************************************************************
* 设置(使用 Visual Studio 窗体设计器):
*
* 添加一个新的 C# 类文件到你的项目并且把它命名为 “Customer.cs”
* 用下面的那个Customer类取代自动生成的Customer类代码
******************************************************************/
public class Customer
{
/* 私有变量 */
private int _id;
private string _name;
private Decimal _rate;
/* 构造函数 */
public Customer()
{
this.ID = -1;
this.Name = string.Empty;
this.Rate = 0.0M;
}
public Customer(int id, string name, Decimal rate)
{
this.ID = id;
this.Name = name;
this.Rate = rate;
}
/* 公共属性 */
public int ID
{
get { return _id; }
set { _id = value; }
}
public string Name
{
get { return _name; }
set { _name = value; }
}
public Decimal Rate
{
get { return _rate; }
set { _rate = value; }
}
}
复杂数据绑定
复杂数据绑定是把一个基于列表的用户界面元素(比如ComboBox、Grid)绑定到一个数据实例列表(比如DataTable)的方法。和简单数据绑定一样,复杂数据绑定通常也是用户界面元素发生改变时传播到数据列表,数据列表发生改变时传播到用户界面元素。Windows窗体复杂数据绑定支持绑定到那些支持IList接口(或者是IEnumerable接口,如果使用的是BindingSource组件的话)的数据列表。
例子:复杂数据绑定(VS 2005)
/*******************************************************************
* 设置 (使用 Visual Studio 窗体设计器):
*
* 添加一个DataGridVie到窗体Form (dataGridView1)
* 添加下面的代码到Form.Load事件响应方法
******************************************************************/
/*******************************************************************
* 创建一个Customer列表. 这个列表实例被命名为 blc.
* 注意: Customer 有这些属性: ID, Name and Rate
******************************************************************/
BindingList<Customer> blc = new BindingList<Customer>();
blc.Add(new Customer(0, "Mr. Zero", 10.0M));
blc.Add(new Customer(1, "Mrs. One", 15.0M));
blc.Add(new Customer(2, "Dr. Two", 20.0M));
/*******************************************************************
* 利用复杂数据绑定将DataGridView绑定到Customer列表
* 绑定(customer业务类型在上面有显示).
******************************************************************/
this.dataGridView1.DataSource = blc;
针对列表的简单数据绑定
简单数据绑定有2种形式:属性对属性的绑定(前面描述过的那种绑定);属性对“一个列表中的某一项的属性”的绑定。属性对“一个列表中的某一项的属性”的绑定形式上和属性对属性的绑定是一样的,除了数据源是一个条目(Item)列表而非单个条目(Item)(比如,是BindingList<Customer>而不是Customer)。当使用简单数据绑定绑定到一个列表时,Windows窗体数据绑定运行时(runtime)j将用户界面元素属性绑定到列表中的某一项上的一个属性。默认情况下,运行时(runtime)将绑定到列表中的第一项(例如,绑定TextBox的Text属性到Customers[0].Name),如果同一个列表被绑定到另外一个使用复杂数据绑定的控件上(正如下面的例子),Windows窗体运行时(runtime)将自动根据复杂数据绑定控件中的当前选中项同步简单数据绑定控件中的属性.,例如,一个开发者可以用简单数据绑定把一个TextBox的Text属性绑定到一个Customer列表,然后他们可以用复杂数据绑定把同一个列表绑定到一个Grid控件。当他们这么做了之后,随着他们在Grid中选择不同的条目,TextBox的Text属性就会自动的重新绑定到Grid的当前选中条目(比如,TextBox将会显示当前选中的Customer的Name)。在Windows窗体中,保持不同的绑定项目的同步性被称作“Currency Management”(字面翻译就是“流通管理”),核心的Windows窗体“Currentcy Management”引擎被一个叫做“CurrencyManager”的类型所实现。
例子:简单数据绑定到一个列表
/*******************************************************************
*设置 (使用 Visual Studio 窗体设计器)
*
* 添加一个DataGridVie到窗体Form (dataGridView1)
* 添加3个 TextBox到窗体Form(textBox1, textBox2 和textBox3)
* 添加下面的代码到Form.Load事件响应方法
******************************************************************/
/*******************************************************************
*创建一个Customer列表. 这个列表实例被命名为 blc
*注意: Customer 有这些属性: ID, Name and Rate
******************************************************************/
BindingList<Customer> blc = new BindingList<Customer>();
blc.Add(new Customer(0, "Mr. Zero", 10.0M));
blc.Add(new Customer(1, "Mrs. One", 15.0M));
blc.Add(new Customer(2, "Dr. Two", 20.0M));
/*******************************************************************
*
利用复杂数据绑定将DataGridView绑定到Customer列表
* 绑定(customer业务类型在上面有显示).
******************************************************************/
this.dataGridView1.DataSource = blc;
/*******************************************************************
* 绑定业务对象列表到TextBoxe. 这里使用简单数据绑定
* 绑定到一个列表中的某一项的属性上
******************************************************************/
this.textBox1.DataBindings.Add("Text", blc, "ID", true);
this.textBox2.DataBindings.Add("Text", blc, "Name", true);
this.textBox3.DataBindings.Add("Text", blc, "Rate", true);
Windows窗体数据绑定引擎不仅可以保证属性和列表的同步,它还提供一些常用的服务来帮助简化这个处理过程。数据绑定引擎提供以下服务:
类型转换(Type Conversion)
如果需要的话,Windows窗体将执行类型转换作为绑定处理的一部分。例如,如果一个业务对象的整型(int)类型的属性(比如Customer.ID)被绑定到一个控件的字符串(string)类型的属性(比如TextBox.Text)上时,数据绑定运行时将把整型值和字符串值互相转换。Windows窗体利用TypeConverter、IFormattable和IConvertible来执行类型转换。
格式化
Windows窗体绑定支持利用.NET Framework格式化字符串来格式化目标数据。比如,当绑定一个业务对象的Decimal类型的属性(比如Order.Total)到一个控件的字符串类型的属性(比如TextBox.Text),一个格式化字符串(比如“c”)可以被指定,因此这个值可以被显示为本地化的货币显示形式(比如¥1.10),Windows窗体利用IFormattable来进行字符串格式化。
例子:格式化
/*******************************************************************
*设置 (使用 Visual Studio 窗体设计器):
*
* 添加 2个TextBoxe 到 Form (textBox1 和textBox2)
* 添加下面的代码到Form.Load事件响应方法
******************************************************************/
/*******************************************************************
* 数据源设置
*
* 创建一个叫做Numbers的表,并添加3列
* ID: int
* Name: string
* Cost: Decimal
******************************************************************/
DataTable _dt;
_dt = newDataTable("Numbers");
_dt.Columns.Add("ID", typeof(int));
_dt.Columns.Add("Name", typeof(string));
_dt.Columns.Add("Cost", typeof(decimal));
_dt.Rows.Add(0, "Zero", 10.0M);
_dt.Rows.Add(1, "One", 11.1M);
_dt.Rows.Add(2, "Two", 12.2M);
/*******************************************************************
* 绑定 TextBox.Text (string) 到Numbers.ID (integer)
* 绑定运行时将处理int和string之间的类型转换
******************************************************************/
this.textBox1.DataBindings.Add("Text", _dt, "ID", true);
/*******************************************************************
* 绑定 TextBox.Text (string) 到Numbers.Cost
* 绑定运行时将把值显示成货币形式(¥value.00)
*
* 注意:这里手工创建了 Binding并将它添加到控件的DataBinding集合
* 而不是使用DataBindings.Add 的重载方法.
******************************************************************/
Binding cb = newBinding("Text", _dt, "Cost", true);
/* .NET Framework 货币格式化字符串 */
cb.FormatString = "c";
/* 添加绑定到TextBox */
this.textBox2.DataBindings.Add(cb);
错误处理
Windows窗体数据绑定整合了Windows窗体ErrorProvider控件。当使用ErrorProvider并且如果一个简单数据绑定操作失败的话,ErrorProvider控件将在用户界面元素上提供一个代表发生错误的可视反馈(一个错误图标)。ErrorProvider控件将查找在绑定期间发生的异常,另外,会在支持IDataErrorInfo接口的数据源上报告IDataErrorInfo信息
例子:错误处理
/*******************************************************************
*设置 (使用 Visual Studio 窗体设计器):
*
* 添加2个 TextBoxe到Form (textBox1 和 textBox2)
* 添加一个ErrorProvider到 Form (errorProvider1)
* 添加下面的代码到Form.Load事件响应方法
******************************************************************/
/*******************************************************************
* 数据源设置:
*
*创建一个叫做Numbers的表,并添加3列
* ID: int
* Name: string
* Cost: Decimal
******************************************************************/
DataTable _dt;
_dt = newDataTable("Numbers");
_dt.Columns.Add("ID", typeof(int));
_dt.Columns.Add("Name", typeof(string));
_dt.Columns.Add("Cost", typeof(decimal));
_dt.Rows.Add(0, "Zero", 10.0M);
_dt.Rows.Add(1, "One", 11.1M);
_dt.Rows.Add(2, "Two", 12.2M);
/*******************************************************************
* 设置 ErrorProvider:
*
* 把它绑定到TextBox使用的同一个数据源上.
******************************************************************/
this.errorProvider1.DataSource = _dt;
/*******************************************************************
*绑定 TextBox.Text (string) 到Numbers.ID (integer)
* 绑定运行时将处理int和string之间的类型转换
******************************************************************/
this.textBox1.DataBindings.Add("Text", _dt, "ID", true);
/*******************************************************************
*绑定 TextBox.Text (string) 到Numbers.Cost
* 绑定运行时将把值显示成货币形式(¥value.00)
*
* 注意:这里手工创建了 Binding并将它添加到控件的DataBinding集合
* 而不是使用DataBindings.Add 的重载方法
******************************************************************/
Binding cb = newBinding("Text", _dt, "Cost", true);
/* .NET Framework货币格式化字符串*/
cb.FormatString = "c";
/*添加绑定到TextBox */
this.textBox2.DataBindings.Add(cb);
Currency Management 和 BindingContext
Currency Management
Windows窗体数据绑定提供的最重要的服务之一是Currency Management。在Windows窗体数据绑定的上下文中,Currency只是为一个列表维护和同步“当前项”。在Windows窗体中,Currency Management通过一个叫做“CurrencyManager”的类型提供,这个CurrencyManager是一个抽象类“BindingManagerBase”的子类。CurrencyManager提供一组事件,包括“CurrentChanged”和“PositionChanged”,控件和开发者通过这组事件都能够监视到“Currency”中的改变。此外,CurrencyManager还提供一些属性像“Position”和“Current”,来允许控件和开发者设置和取回当前项(位置)。Windows窗体数据绑定的一个关键方面是,Currency不是被像DataGrid这样的控件所维护,而是由被多个控件共享的一个CurrencyManager的一个实例来维护。
Binding Context
当使用简单列表绑定(就是上面说过的“针对列表的简单数据绑定”)的时候,简单数据绑定的控件需要同步它的数据源中的“当前项”。为了做到这点,绑定需要得到它关联的CurrencyManager的“当前”项。绑定通过它的控件的“BindingContext”属性得到它的数据源的CurrencyManager,一个控件典型的是从它的父窗体中得到它的BindingContext。“BindingContext”是一个单窗体的CurrencyManager缓存,更确切地说,BindingContext是一个BindingManagerBase实例的缓存,而BindingManagerBase时CurrencyManager的基类。
举个例子,假设一个开发者有一个Customer列表,绑定到一个DataGrid控件(DataGrid.DataSource设置到一个Customer列表)。此外,开发者还有一个简单控件,比如一个TextBox,绑定到同一个列表(TextBox利用简单列表绑定把它的Text绑定到Customer列表中的Name属性),当在DataGrid中点击一项时,DataGrid将使被点击的那想成为当前选中项。DataGrid怎么做到的呢?DataGrid首先访问它的BindingContext属性获取它的数据源(列表)的BindingManagerBase(CurrencyManager),“BindingContext”返回缓存的BindingManagerBase(如果不存在的话,会创建一个),然后DataGrid会使用BindingManagerBase的API来改变“当前”项(它通过设置CurrencyManager的Position属性来达到目的)。当一个简单数据绑定被构造出来之后,控件将得到与它的数据源关联的BindingManagerBase(CurrencyManager),它将监听BindingManagerBase上的change事件,并且在BindingManagerBase的“当前项”改变时同步更新它的绑定属性(比如Text属性)。正确的同步的关键是DataGrid和TextBox两者必须要使用相同的CurrencyManager(BindingManagerBase)。如果他们使用的是不同的CurrencyManager,那么当DataGrid中的条目发生改变时简单绑定的属性将无法正确更新。
正如前面提到的,控件(和开发者)能够利用一个控件的BindingContext属性得到一个与数据源关联的BindingManagerBase。当从BindingContext请求一个BindingManagerBase的时候,BindingContext将首先查到它的缓存看是否有被请求的BindingManagerBase。如果缓存中不存在这个BindingManagerBase,BindingContext将创建并返回一个新的(并且把它加入缓存)。BindingContext典型情况下对于每个窗体是全局的(子控件的代理他们父窗体的BindingContext),因此典型情况下BindingManagerBase(CurrencyManagers)被一个窗体上所有的控件所共享。BindingContext有2种形式:
/* 获取一个给定数据源的BindingManagerBase */
bmb = this.BindingContext[dataTable];
/* 获取给定数据源和数据成员的BindingManagerBase */
bmb = this.BindingContext[dataSet, "Numbers"];
第一种形式通常用来获取一个像ADO.NET的DataTable这样的列表的相关的BindingManagerBase。第二种形式用来获取含有子列表的父级数据源(比如含有子DataTable的DataSet)的BindingManagerBase。BindingContext最令人不解的方面之一是使用不同的形式来指定同一个数据源,将导致两个不同的BindingManagerBase实例。迄今为止,这是控件绑定到相同的数据源但却没有同步的最常见原因(控件通过BindingContext来获取一个BindingManagerBase)。
例子:控件同步
/* 创建含有一个DataTable的DataSet */
DataSet dataSet = new DataSet();
DataTable dataTable = dataSet.Tables.Add("Numbers");
dataTable.Columns.Add("ID", typeof(int));
dataTable.Columns.Add("Name", typeof(string));
dataTable.Rows.Add(0, "Zero");
dataTable.Rows.Add(1, "One");
/*******************************************************************
* 绑定第一个DataGridView和TextBox到dataSet中的”Numbers”表
* 这个DataGridView 将使用 BindingContext 得到一个数据源的CurrencyManager
* DataGridView1 将使用以下形式的BindingContext
*
* bmb = BindingContext[dataSet, “Numbers”];
*
* textBox1的文本绑定也将得到一个BindingManagerBase,并且使用如下的BindingContext形式
*
* bmb = BindingContext[dataSet, “Number”];
*
* 因此dataGridView1 和 textBox1 将共享同一个BindingManagerBase (CurrencyManager)
*
*******************************************************************/
this.dataGridView1.DataSource = dataSet;
this.dataGridView1.DataMember = "Numbers";
this.textBox1.DataBindings.Add("Text", dataSet, "Numbers.Name", true);
/*******************************************************************
* 变量 “dataTable” 也指向这个 “Numbers” 表. 虽然
* 上面的 DataGridView 和TextBox 利用“DataSource” 和“DataMember” 形式
* 绑定到表, 现在也可以绑定到相同的表(和数据),通过直接绑定到“dataTable”,正如下面所示
* 当这么做的时候, DataGridView2 将使用以下形式的BindingContext
*
* bmb = BindingContext[dataTable];
*
* textBox12的文本绑定也将使用如下的BindingContext形式
*
* bmb = BindingContext[dataTable];
*
* 因此dataGridView2 和 textBox2 将共享相同的BindingManagerBase (CurrencyManager)
* 然而他们将不会和dataGridView1、textBox1共享相同的CurrencyManager,因为他们使用不同的形式
* 来获取他们的绑定.
*******************************************************************/
this.dataGridView2.DataSource = dataTable;
this.textBox2.DataBindings.Add("Text", dataTable, "Name", true);
控制绑定操作
Windows窗体简单绑定类型(System.Windows.Forms.Binding)允许开发者控制在用户界面元素内容发生改变时怎样和何时更新绑定的数据源属性,以及在数据源属性发生变化时怎样和何时更新用户界面元素。例如,如果一个开发者把Customer实例的Name属性绑定到一个TextBox控件的Text属性上,开发者可以指定何时把用户界面元素发生的更改“传播”到数据源。当前支持的选项是:在TextBox的验证(validation)期间(当用户将焦点移出TextBox时——这是默认值);当文本值发生任何改变时;以及永不更新数据源。开发者也能够控制何时把数据源的更改更新到绑定到的用户界面元素。当前支持的选项的是:当数据源属性改变时(默认值)和永不更新。注意,开发者可以组合使用“永不更新”和调用绑定API(ReadValue/WriteValue)来提供他们自己的显示的数据源与用户界面元素间的数据同步规则。
例子:显式的绑定
/*******************************************************************
* 设置 (使用 Visual Studio 窗体设计器):
*
* 添加3个TextBoxe到窗体Form (textBox1, textBox2 and textBox2)
* 添加1个 ErrorProvider 到窗体Form (errorProvider1)
* 添加一个按钮Button到窗体Form (button1)
* 添加下面的代码到Form.Load 事件处理方法
******************************************************************/
/*******************************************************************
* 数据源设置:
*
* 创建一个叫做 Numbers的表并添加3列
* ID: int
* Name: string
* Cost: Decimal
******************************************************************/
DataTable _dt;
_dt = newDataTable("Numbers");
_dt.Columns.Add("ID", typeof(int));
_dt.Columns.Add("Name", typeof(string));
_dt.Columns.Add("Cost", typeof(decimal));
_dt.Rows.Add(0, "Zero", 10.0M);
_dt.Rows.Add(1, "One", 11.1M);
_dt.Rows.Add(2, "Two", 12.2M);
/*******************************************************************
* 设置 ErrorProvider:
*
* 把它绑定到TextBox使用的同一个数据源.
******************************************************************/
this.errorProvider1.DataSource = _dt;
/*******************************************************************
* 绑定 TextBox.Text (string) 到Numbers.ID (integer)
*
* 设置DataSourceUpdateMode 为OnPropertyChanged. 这将
* 导致一旦TextBox.Text发生改变,就会立即更新数据源属性
*
* 注意, 在这种模式下ErrorProvider将立刻显示一个错误,而不是在用户把焦点
* 移出TextBox之后才只显示一个错误
******************************************************************/
this.textBox1.DataBindings.Add("Text", _dt, "ID", true,
DataSourceUpdateMode.OnPropertyChanged);
/*******************************************************************
* 绑定 TextBox.Text 到 Form.Size (在这个场景下, 窗体Form 是数据源)
* 不要让控件的改变更新到数据源(窗体Form)
******************************************************************/
Binding sizeBinding = newBinding("Text", this, "Size", true,
DataSourceUpdateMode.Never);
this.textBox2.DataBindings.Add(sizeBinding);
/*******************************************************************
* 设置Button.Click 显示的更新数据源(Form.Size).
*
* 使用匿名代理让代码更简洁
******************************************************************/
this.button1.Click += delegate(object button, EventArgs args)
{
sizeBinding.WriteValue();
};