Effective C# 原则38:使用和支持数据绑定
有经验的Windows程序员一定对写代码从一个控件上取值,以及把值存储到控件上很熟悉:
public Form1 : Form
{
private MyType myDataValue;
private TextBox textBoxName;
private void InitializeComponent( )
{
textBoxName.Text = myDataValue.Name;
this.textBoxName.Leave += new
System.EventHandler( this.OnLeave );
}
private void OnLeave( object sender, System.EventArgs e )
{
myDataValue.Name = textBoxName.Text;
}
}
这太简单了,正如你知道的,重复代码。之所以不喜欢这样重复代码,就是因为应该有更好的方法。是的,.Net框架支持数据绑定,它可以把一个对象的属性映射到控件的属性上:
textBoxName.DataBindings.Add ( "Text",myDataValue, "Name" );
上面的代码就把textBoxName控件的“Text”属性上绑定了MyDataValue对象的"Name"属性。在内部有两个对象,绑定管理(BindingManager)和流通管理(CurrencyManager),
实现了在控件与数据源之间的传输实现。你很可能已经见过为种结构的例子,特别是在DataSet和DataGrid之间的。你也很可能已经做过数据绑定的例子。你很可能只在表面上简单的使用过从数据绑定上得到的功能。你可以通过高效的数据绑定避免写重复的代码。
关于数据绑定的完整处理方案可能至少要花上一本书来说明,要不就是两本。Windows应用程序和Web应用程序同时都支持数据绑定。比写一个完整的数据绑定论述要强的是,我确实想让你记住数据绑定的核心好处。首先,使用数据绑定比你自己写代码要简单得多。其次,你应该在对文字元素通过属性来显示时,尽可能的使用它,它可以很好的绑定。第三,在Windows窗体中,可以同步的对绑定在多控件上的数据,进行相关数据源的检测。
例如,假设只要在数据不合法时,要求将文字显示为红色,你可能会写这样的代码:
if (src.TextIsInvalid)
{
textBox1.ForeColor = Color.Red;
}
else
{
textBox1.ForeColor = Color.Black;
}
这很好,但只要在文字源发生改变时,你要随时调用这段代码。这可能是在用户编辑了文字,或者是在底层的数据源发生改变时。这里有太多的事件要处理了,而且很多地方你可能会错过。但,使用数据绑定时,在src对象上添加一个属性,返回恰当的前景颜色就行了。
另一个逻辑可能是要根据文字消息的状态,来设置值可变化为恰当颜色的值:
private Color _clr = Color.Black;
public Color ForegroundColor
{
get
{
return _clr;
}
}
private string _txtToDisplay;
public string Text
{
get
{
return _txtToDisplay;
}
set
{
_txtToDisplay = value;
UpdateDisplayColor( IsTextValid( ) );
}
}
private void UpdateDisplayColor( bool bValid )
{
_clr = ( bValid ) ? Color.Black : Color.Red;
}
简单的添加绑定到文本框里就行了:
textBox1.DataBindings.Add ("ForeColor", src, "ForegroundColor");
当数据绑定配置好以后,textBox1会根据内部源对象的值,用正确的颜色来绘制文本。这样,你就已经大大减少了从源数据到控件的数据来回传输。不再须要对不同地方显示不同颜色来处理很多事件了。你的数据源对象保持对属性的正确显示进行跟踪,而表单控件对数据绑定进行控制。
通过这个例子,我演示了Windows表单的数据绑定,同样的在web应用程序中也是一样的原则:你可以很好的绑定数据源的属性到web控件的属性上:
<asp:TextBox id=TextBox1 runat="server" Text="<%# src.Text %>" ForeColor="<%# src.ForegroundColor %>">
这就是说,当你创建一个应用程序在UI上显示的类型时,你应该添加一些必须的属性来创建和更新你的UI,以便用户在必要时使用。
当你的对象不支持你要的属性时怎么办呢?那就把它封装成你想要的。看这样的数据结构:
public struct FinancialResults
{
public decimal Revenue
{
get { return _revenue; }
}
public int NumberOfSales
{
get { return _numSales; }
}
public decimal Costs
{
get { return _cost;}
}
public decimal Profit
{
get { return _revenue - _cost; }
}
}
要求你在一个表单上以特殊的格式信息来显示这些,如果收益为负,你必须以红色来显示收益。如果薪水小于100,你应该用粗体显示。如果开销在10千(1万)以上,你也应该用粗体显示。创建FinancialResults结构的开发者没有添加UI功能到这个结构上。这很可能是正确的选择,FinancialResults应该限制它的功能,只用于存储实际的值。你可以创建一个新类型,包含UI格式化属性,以及在FinancialResults结构中的原始的存储属性:
public struct FinancialDisplayResults
{
private FinancialResults _results;
public FinancialResults Results
{
get { return _results; }
}
public Color ProfitForegroundColor
{
get
{
return ( _results.Profit >= 0 ) ?
Color.Black : Color.Red;
}
}
// other formatting options elided
}
这样,你就创建了一个简单的数据结构来帮助你所包含的数据结构来进行数据绑定:
// Use the same datasource. That creates one Binding Manager
textBox1.DataBindings.Add ("Text", src, "Results.Profit");
textBox1.DataBindings.Add ("ForeColor",src,”ProfitForegroundColor");
我已经创建了一个只读的属性,用于访问核心的财政数据结构。这种构造在你试图支持对数据的读写操作时不能工作,FinancialResults结构是值类型,这就是说获取访问器不提供对存储空间的访问,它只是返回一个拷贝。这样的方式很乐意返回一个拷贝,而这样的拷贝并不能在数据绑定中进行修改。然而,如果你试图对数据进行编辑时,FinancialResults类应该是一个类,而不是一个结构(参见原则6)。做为一个引用类型,你的获取访问器返回一个内部存储的引用,而且可以被用户编辑。内部的结构应该须要对存储的数据发生改变时做出响应。FinancialResults应该触发事件来告诉其它代码这一状态的改变。
有一个很重要的事情要记住:把数据源用在同一表单中的所有相关控件上。使用DataMember属性来区别每个控件显示的属性。你可以像这样写绑定过程:
// Bad practice: creates two binding managers
textBox1.DataBindings.Add ("Text",src.Results, "Profit");
textBox1.DataBindings.Add ("ForeColor",src,“rofitForegroundColor");
这会创建两个绑定管理者,一个为src对象,另一个为src.Results对象。每个数据源由不同的绑定管理者控制,如果你想让绑定管理者在数据源发生改变时,更新所有的属性,你须要确保数据源是一致的。
你几乎可以在所有的Windows控件和web控件上使用数据绑定。在控件里显示的值,字体,只读状态,甚至是控件控件的位置,都可以成为绑定操作的对象。我的建议是创建类或者结构,包含一些用户要求的,以某种样式显示的数据。这些数据就是用于更新控件。
另外,在简单控件中,数据绑定经常出现在DataSet和DataGrids中。这非常有用,你把DataGrid绑定到DataSet上,然后DataSet中所有的值就显示了。如果你的DataSet有多个表,你甚至还可以在多个表中间进行导航。这不是很好吗?
好了,下面的问题就是如果你的数据集不包含你想显示的字段时该怎么办。这时,你必须添加一个列到DataSet中,这一列计算一些UI中必须的值。如果值可以用SQL表达式计算,那么DataSet可以为你完成。下面的代码就添加了一个列到Employees
数据表中,用于显示格式化了名字:
DataTable dt = data.Tables["Employees"];
dt.Columns.Add("EmployeeName", typeof(string), "lastname + ', ' + firstname");
通过添加列到DataSet中,你可以添加这些列到DataGrid上。你所创建的对象层,是在数据存储对象的最项层上,用于创建数据表现层给你的用户。
到目前为止,这一原则里所使用的都是string类型,.net框架可以处理字符到数字的转化:它试图转化用户的输入到恰当的类型。如果失败,原始的值会恢复。这是可以工作的,但用户完全没的反馈信息,他们的输出被安静的忽略了。你可以通过处理绑定过程中的转化事件来添加反馈信息。这一事件在绑定管理者从控件上更新值到数据源时发生。ParseEventArgs包含了用户输入的文字,以及它所期望被转化的类型。你可以捕获这一事件,其后完成你自己的通知,也可以修改数据并且用你自己的值来更新数据:
private void Form1_Parse( object sender, ConvertEventArgs e )
{
try {
Convert.ToInt32 ( e.Value );
} catch
{
MessageBox.Show (
string.Format( "{0} is not an integer",
e.Value.ToString( ) ) );
e.Value = 0;
}
}
你可能还要处理Format事件,这一个HOOK,可以在数据从数据源到控件时格式化数据。你可以修改ConvertEventArgs的Value字段来格式化必须显示的字符串。
.Net提供了通用的框架,可以让你支持数据绑定。你的工作就是为你的应用程序和数据提供一些特殊的事件句柄。Windows表单和Web表单以及子系统都包含了丰富的数据绑定功能。框架库已经包含了所有你须要的工具,因此,你的UI代码应该真实的描述数据源和要显示的属性,以及在把这些元素存储到数据源时须要遵守的规则。你应该集中精力创建数据类型,用于描述显示的参数,然后Winform以及Webform的数据绑定完成其它的。不应该在把数据从用户控件到数据源之间进行传输时写相关的代码(译注:指用数据绑定,而不用其它的方法)。不管怎样,数据必须从你的业务对象关联到UI控件上与用户进行交互。通过创建类型层以及使用数据绑定的概念,你就可以少写很多代码。.Net框架已经
同时在Windows和Web应用程序中为你处理了传输的工作。