Effective C# 原则38:使用和支持数据绑定
Item 38: Utilize and Support Data Binding
有经验的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应用程序中为你处理了传输的工作。
========================================
Item 38: Utilize and Support Data Binding
Experienced Windows programmers are familiar with writing the code to place data values in controls and to store values from controls:
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;
}
}
It's simple, repetitive codeyou know, the kind you hate to write because there must be a better way. There is. The .NET Framework supports data binding, which maps a property of an object to a property in the control:
textBoxName.DataBindings.Add ( "Text",
myDataValue, "Name" );
The previous code binds the "Text" property of the textBoxName control to the "Name" property of the myDataValue object. Internally, two objects, the BindingManager and the CurrencyManager, implement the transfer of data between the control and the data source. You've probably seen this construct in many samples, particularly with DataSets and DataGrids. You've also done simple binding to text boxes. You've likely only scratched the surface of the capabilities you get from data binding. You can avoid writing repetitive code by utilizing data binding more effectively.
A full treatment of data binding would span at least one book, if not two. Both Windows applications and web applications support data binding. Rather than write a complete treatise of data binding, I want to make sure you remember the key advantages of it. First, using data binding is much simpler than writing your own code. Second, you should use it for more than text itemsother display properties can be bound as well. Third, on Windows forms, data binding handles synchronizing multiple controls that examine related data sources.
For example, suppose you get a requirement to display the text in red whenever the data shows an invalid value. You could write the following snippet:
if ( src.TextIsInvalid )
{
textBox1.ForeColor = Color.Red;
} else
{
textBox1.ForeColor = Color.Black;
}
That's well and good, but you need to call that snippet of code whenever the text in your source changes. That could be when the user edits the text or when the underlying data source changes. There are a lot of events to handle and many places that you might miss. Instead, use data binding. Add a property in your src object to return the proper foreground color.
Other logic will set the value of that variable to the proper color based on the state of the text message:
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;
}
Then simply add the binding to the text box:
textBox1.DataBindings.Add ("ForeColor",
src, "ForegroundColor");
When the data binding is configured, textBox1 will draw its text in the correct color, based on the internal value of the source object. You've done more to decouple the control from the data source. Instead of having multiple event handlers and multiple locations where the display color changes, you have two. Your data source object keeps track of the properties that affect the proper display. Your form controls the data binding.
Although the samples I've shown are Windows forms, the same principle works for web applications: You can bind properties of data sources to a property in the web control as well:
<asp:TextBox id=TextBox1 runat="server"
Text="<%# src.Text %>"
ForeColor="<%# src.ForegroundColor %>">
This means that when you create the types that your application displays in its UI, you should add the necessary properties to create and update your UI in response to user needs.
What do you do if the objects you have don't support the properties you need? You wrap what you have and add what you need. Consider this data structure:
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; }
}
}
You have requirements to display these in a form with some special formatting notes. If the profit is negative, you must display the profit in red. If the number of sales drops below 100, it should be bold. If the cost is above 10,000, it should be bold. The developer who created the FinancialResults structure did not add UI capabilities into the structure. That was most likely the right choice. FinancialResults should limit its capabilities to storing the actual values. You can create a new type to include the UI formatting properties with the original store properties in the FinancialResults structure:
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
}
You have created a single data structure to facilitate data binding of your contained structure:
// Use the same datasource. That creates one Binding Manager
textBox1.DataBindings.Add ("Text",
src, "Results.Profit");
textBox1.DataBindings.Add ("ForeColor",
src, "ProfitForegroundColor");
I've created one read-only property that allows access to the core financial structure. That construct doesn't work if you intend to support read/write access to the data. The FinancialResults struct is a value type, which means that the get accessor does not provide access to the existing storage; it returns a copy. This idiom has happily returned a copy that cannot be modified using data binding. However, if you intended editing, the FinancialResults type would be a class, not a struct (see Item 6). As a reference type, your get accessor returns a reference to the internal storage and would support edits by the user. The internal structure would need to respond to changes made to the internal storage. The FinancialResults would raise events to notify other code of changes in state.
It's important to remember to use the data source for all related controls in the same form. Use the DataMember property to differentiate the property displayed in each control. You could have written the binding construct this way:
// Bad practice: creates two binding managers
textBox1.DataBindings.Add ("Text",
src.Results, "Profit");
textBox1.DataBindings.Add ("ForeColor",
src, "ProfitForegroundColor");
That would create two binding managers, one for the src object and one for the src.Results object. Each data source is managed by a different binding manager. If you want the binding manager to update all property changes when the data source changes, you need to make sure that the data sources match.
You can use data binding for almost any property of a Windows or web control. The values displayed in the control, the font, the read-only state, and even the location of the control can be the target of a binding operation. My advice is to create the class or struct that contains the values you need to display your data in the manner requested by your users. Then use data binding to update the controls.
In addition to simple controls, data binding often involves DataSets and DataGrids. It's very powerful: You bind the DataGrid to the DataSet, and all the values in the DataSet are displayed. If your DataSet has multiple tables, you can even navigate between tables. What's not to love?
Well, the problem arises if your data set does not contain the fields you want to display. In those cases, you must add a column to the DataSet that computes the value needed for the user interface. If the value can be computed using a SQL expression, the DataSet can compute the value for you. The following code adds a column n to the Employees data table that displays a formatted version of the name:
DataTable dt = data.Tables[ "Employees" ];
dt.Columns.Add( "EmployeeName",
typeof( string ),
"lastname + ', ' + firstname");
By adding columns to the DataSet, you can add columns to the DataGrid. You build layers of objects on top of the stored data objects to create the data presentation you want to give the user.
All the items I showed you so far are string types. The framework does handle converting strings to numeric values: It tries to convert the user's input to the proper type. If that fails, the original value is restored. It works, but the user gets absolutely no feedback, their input is silently ignored. You add that feedback by processing the Parse event from the binding context. That event occurs when the binding manager updates the value in the data source from the value in the control. ParseEventArgs gives you the text typed by the user and the desired type to convert the text. You can trap this event and perform your own notification, even going so far as to modify the value and update the text with your own value:
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;
}
}
You might also want to handle the Format event. This is the hook that lets you format the data that comes from your data source and goes into the control. You can modify the Value field of ConvertEventArgs to format the string that should be displayed.
The .NET Framework provides the generic framework for you to support data binding. Your job is to provide the specific event handlers for your application and your data. Both the Windows Forms and Web forms subsystems contain rich data-binding capabilities. The library already contains all the tools you need, so your UI code should really be describing the data sources and properties to be displayed and what rules should be followed when you store those elements back in the data source. You concentrate on building the data types that describe the display parameters, and the Winforms and Webforms data binding does the rest. There is no way around writing the code that transfers values between the user controls and the data source objects. Somehow, data must get from your business objects to the controls that your users interact with. But by building layers of types and leveraging data-binding concepts, you write a lot less of it. The framework handles the transfers for you, in both Windows and web applications.