使用反射将业务对象绑定到 ASP.NET 窗体控件


  本文标签:.net ASP ASP.NET

  使用反射以单行代码将业务对象绑定到 ASP.NET Web 窗体,从而降低复杂性并减少错误  。(本文包含一些指向英文站点的链接  。请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解  。)

  引言

  在 Web 开发人员的最常见任务之中,有一项任务是他们要反复执行的:建立更新数据库表的简单窗体  。我们将创建一个列表页面和一个窗体页面,列表页面中以表格形式显示记录,窗体页面中带有用于各个数据库字段的适当的窗体控件  。许多开发人员还使用表示数据库表的业务对象将代码组织到分为多层的设计中  。如果以业务对象 (Document) 来表示数据库表 (Documents),许多窗体的代码看上去将如下所示:

 
   
简化和缩短窗体代码

  在以上代码中,对每个控件进行显式转换,并将其设置为窗体控件的正确属性  。根据属性和窗体控件的数量,这部分代码可能会变长并难以管理  。代码还应包含类型转换的错误更正和 ListControl,这将进一步增加复杂性  。即使窗体是由代码生成工具(例如 Eric J. Smith 的优秀的 CodeSmith)生成的,当需要任何自定义逻辑关系时,很容易引入错误  。 

使用反射,可以仅使用单行代码便将业务对象的所有属性绑定到相应的窗体控件,从而减少代码的行数并增强可读性  。完成反射系统的建立后,以上代码将简化为:

protected void Page_Load(Object Src, EventArgs E) {
if (!IsPostBack) {
Document document =
Documents.GetDocument(Request.QueryString["DocumentID"]);

FormBinding.BindObjectToControls(document);
}
}
protected void Save_Click(Object Src, EventArgs E) {
Document document =
Documents.GetDocument(Request.QueryString["DocumentID"]);

FormBinding.BindControlsToObject(document);

Documents.Update(document);


此代码可用于所有标准的 ASP.NET 控件(TextBox、DropDownList、CheckBox 等)和许多第三方控件(例如 Free TextBox 和 Calendar Popup)  。无论有多少业务对象属性和窗体控件,这一行代码都能提供所需的全部功能,只要窗体控件的 ID 与业务对象属性名相匹配  。 

开始:从反射中检索属性列表

  首先,我们需要检查业务对象的属性,并查找与业务对象属性名具有相同 ID 的 ASP.NET 控件  。以下代码构成了绑定查找的基础:

public class FormBinding {
public static void BindObjectToControls(object obj,
Control container) {
if (obj == null) return;
Type objType = obj.GetType();
PropertyInfo[] objPropertiesArray =
objType.GetProperties();

foreach (PropertyInfo objProperty in objPropertiesArray) {

Control control =
container.FindControl(objProperty.Name);
if (control != null) {
// 处理控件 ...
}
}
}


在以上代码中,方法 BindObjectsToControls 接受了业务对象 obj 和一个容器控件  。容器控件通常是当前 Web 窗体的 Page 对象  。如果所用版本是会在运行时更改控件嵌套顺序的 ASP.NET 1.x MasterPages,您将需要指定窗体控件所在的 Content 控件  。这是在 ASP.NET 1.x 中,FindControl 方法对嵌套控件和命名容器的处理方式导致的  。 

在以上代码中,我们获取了业务对象的 Type,然后使用该 Type 来获取 PropertyInfo 对象的数组  。每个 PropertyInfo 对象都包含关于业务对象属性以及从业务对象获取和设置值的能力的信息  。我们使用 foreach 循环检查具有与业务对象属性名 (PropertyInfo.Name) 对应的 ID 属性的 ASP.NET 控件的容器  。如果找到控件,则尝试将属性值绑定到该控件  。 

将对象属性值绑定到控件

  过程中的大部分操作是在此阶段执行的  。我们需要用对象的属性值来填充找到的控件  。一种实现方法是为每种控件类型创建一个 if ... else 语句  。派生自 ListControl(DropDownList、RadioButtonList、CheckBoxList 和 ListBox)的所有控件都具有可以统一访问的公用接口,所以可以将它们编组在一起  。如果找到的控件是 ListControl,我们可以将其作为 ListControl 进行转换,然后设置选定项:

Control control = container.FindControl(objProperty.Name);
if (control != null) {
if (control is ListControl) {
ListControl listControl = (ListControl) control;
string propertyValue = objProperty.GetValue(obj,
null).ToString();
ListItem listItem =
listControl.Items.FindByValue(propertyValue);
if (listItem != null) listItem.Selected = true;
} else {
// 处理其他控件类型
}


不幸的是,其他控件类型并不从父类中派生  。以下几个公用控件都具有 .Text 字符串属性:TextBox、Literal 和 Label  。但该属性不是从公用父类中派生出来的,所以需要分别转换每种控件类型  。我们还需要转换其他控件类型,例如 Calendar 控件,以便使用适当的属性(在 Calendar 的例子中,是 SelectedDate 属性)  。要包含所有标准的 ASP.NET 窗体控件,并访问窗体控件的正确属性并不需要太多的代码行  。

if (control is ListControl) {
ListControl listControl = (ListControl) control;
string propertyValue = objProperty.GetValue(obj,
null).ToString();
ListItem listItem = listControl.Items.FindByValue(propertyValue);
if (listItem != null) listItem.Selected = true;
} else if (control is CheckBox) {
if (objProperty.PropertyType == typeof(bool))
((CheckBox) control).Checked = (bool)
objProperty.GetValue(obj, null);
} else if (control is Calendar) {
if (objProperty.PropertyType == typeof(DateTime))
((Calendar) control).SelectedDate = (DateTime)
objProperty.GetValue(obj, null);
} else if (control is TextBox) {
((TextBox) control).Text = objProperty.GetValue(obj,
null).ToString();
} else if (control is Literal)(
//... 等等  。还可用于标签等属性  。


此方法完整地涵盖了标准的 ASP.NET 1.x 控件  。从这个角度来看,我们拥有了功能齐全的 BindObjectToControls 方法  。但在起作用的同时,此方法的应用范围会受到限制,因为它仅考虑内置的 ASP.NET 1.x 控件  。如果要支持新的 ASP.NET 2.0 控件,或者要使用任何第三方控件,我们必须在 FormBinding 项目中引用控件的程序集,并将控件类型添加到 if ... else 列表  。 

此问题的解决方案是第二次使用反射,以查看各个控件的属性,并找出控件是否具有与业务对象的属性类型对应的属性类型  。 
用已知属性设置未知控件的值

  如上所述,有些控件共享字符串属性 .Text,大多数窗体控件以实质相同的方式使用此属性  。该属性用于获取和设置用户输入的数据  。有大量控件还使用了其他一些公用属性和属性类型  。以下是这些属性中的一些:称为 .SelectedDate 的 DateTime 属性,它在许多日历和日期选取器控件中使用;称为 .Checked 的布尔属性,它在布尔型控件中使用;称为 .Value 的字符串属性,它常见于隐藏控件  。这四个属性(string Text、string Value、bool Checked 和 DateTime SelectedDate)是最常见的控件属性  。如果可以将系统设计成无论何种控件类型,都绑定到这些属性,那么我们的绑定方法将适用于使用那四个属性的任何控件  。 

在以下代码中,我们将第二次使用反射(这一次是对窗体控件使用,而不是对业务对象使用),以确定它是否具有任何常用属性  。如果有,则尝试将业务对象的属性值设置为控件的属性  。作为示例,我们将对整个 PropertyInfo 数组进行迭代,并查找称为 .Text 的字符串属性  。如果控件具有该属性,则将数据从业务对象发送到该控件的属性  。 

  if (control is ListControl) {
// ...
} else {
// 获取控件的类型和属性
//
Type controlType = control.GetType();
PropertyInfo[] controlPropertiesArray =
controlType.GetProperties();

// 查找 .Text 属性
//
foreach (PropertyInfo controlProperty
in controlPropertiesArray) {
if (controlPropertiesArray.Name == "Text" &&
controlPropertiesArray.PropertyType == typeof(String)) {
// 设置控件的 .Text 属性
//
controlProperty.SetValue(control,
(String) objProperty.GetValue(obj, null), null);

}
}



如果找到 .Text,则使用 PropertyInfo 类的 GetValue 方法从业务对象的属性中检索值  。然后,使用控件的 .Text 属性的 SetValue 方法  。在此,我们还使用 Type 命令将控件的属性设置为 typeof(String),并使用 (String) 符号显式转换来自属性的值  。 

为了使 BindObjectToControls 方法完整,我们还需要处理其他公用属性,即 .Checked、.SelectedDate 和 .Value  。在以下代码中,我们将控件属性搜索打包到称为 FindAndSetControlProperty 的辅助方法中,以简化代码  。

if (control is ListControl) {
// ...
} else {
// 获取控件的属性
//
Type controlType = control.GetType();
PropertyInfo[] controlPropertiesArray =
controlType.GetProperties();

bool success = false;
success = FindAndSetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"Checked", typeof(bool) );

if (!success)
success = FindAndSetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"SelectedDate", typeof(DateTime) );

if (!success)
success = FindAndSetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"Value", typeof(String) );

if (!success)
success = FindAndSetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"Text", typeof(String) );

}

private static void FindAndSetControlProperty(object obj,
PropertyInfo objProperty, Control control,
PropertyInfo[] controlPropertiesArray, string propertyName,
Type type) {
// 在整个控件属性中进行迭代

foreach (PropertyInfo controlProperty in
controlPropertiesArray) {
// 检查匹配的名称和类型
if (controlPropertiesArray.Name == "Text" &&
controlPropertiesArray.PropertyType == typeof(String)) {
// 将控件的属性设置为
// 业务对象属性值
controlProperty.SetValue(control,
Convert.ChangeType(
objProperty.GetValue(obj, null), type) , null);
return true;
}
}
return false;


以上属性检查的顺序很重要,因为有些控件具有以上属性中的多个,但我们只想设置一个  。例如,CheckBox 控件既有 .Text 属性也有 .Checked 属性  。在此示例中,我们希望使用 .Checked 属性而不是 .Text 属性,所以将 .Checked 放在属性搜索顺序的首位  。任何情况下,如果找到具有正确名称和类型的控件属性,则尝试将控件的属性设置为业务对象属性的值  。

从这个角度来看,我们拥有了功能齐全的 BindObjectToControls 方法  。利用该方法,我们可以在 ASPX 窗体上的任何地方,使用任何类和控件的任意组合进行调用,而这确实有效  。现在,我们需要创建在提交窗体时进行反转的方法  。我们需要从表示用户输入的控件中检索新值,而不是将控件属性的值设置为业务对象的值  。

  反转过程:BindControlsToObject

  在 BindControlsToObject 方法中,我们将以同样的方式开始,即从业务对象中检索属性的列表,然后使用 FindControl 方法找到具有与对象属性相匹配的 ID 的控件  。如果找到控件,则检索值并将该值返回给业务对象  。此部分还将包含 ListControl 的单独代码,因为这些控件具有公用接口  。我们将使用另一种辅助方法来搜索并检索控件中的值,然后将该值返回给业务对象  。

public static void BindControlsToObject(object obj,
Control container) {
Type objType = obj.GetType();
PropertyInfo[] objPropertiesArray = objType.GetProperties();

foreach (PropertyInfo objProperty in objPropertiesArray) {

if (control is ListControl) {
ListControl listControl = (ListControl) control;
if (listControl.SelectedItem != null)
objProperty.SetValue(obj,
Convert.ChangeType(list.SelectedItem.Value,
objProperty.PropertyType), null);

} else {
// 获取控件的属性
//
Type controlType = control.GetType();
PropertyInfo[] controlPropertiesArray =
controlType.GetProperties();

bool success = false;
success = FindAndGetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"Checked", typeof(bool) );

if (!success)
success = FindAndGetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"SelectedDate", typeof(DateTime) );

if (!success)
success = FindAndGetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"Value", typeof(String) );

if (!success)
success = FindAndGetControlProperty(obj,
objProperty, control, controlPropertiesArray,
"Text", typeof(String) );

}
}
}

private static void FindAndGetControlProperty(object obj,
PropertyInfo objProperty, Control control, PropertyInfo[]
controlPropertiesArray, string propertyName, Type type) {
// 在整个控件属性中进行迭代
foreach (PropertyInfo controlProperty in
controlPropertiesArray) {
// 检查匹配的名称和类型
if (controlPropertiesArray.Name == "Text" &&
controlPropertiesArray.PropertyType == typeof(String)) {
// 将控件的属性设置为
// 业务对象属性值
try {
objProperty.SetValue(obj,
Convert.ChangeType(
controlProperty.GetValue(control, null),
objProperty.PropertyType) , null);
return true;
} catch {
// 无法将来自窗体控件
// 的数据转换为
// objProperty.PropertyType
return false;
}
}
}
return true;


完成这两种方法后,我们的窗体语法将得到简化,如以上简化和缩短窗体代码中所述  。每个属性和控件的类型转换与错误更正都是自动进行的  。这两种方法(BindObjectToControls 和 BindControlsToObject)为开发人员创建窗体提供了很大的灵活性  。它们还可以用于处理以下这些常见方案:

• 如果将新属性添加到业务对象,并且需要在窗体上访问该新属性,那么开发人员只需将控件添加到页面,并将控件的 ID 设置为新属性的名称,FormBinding 方法将处理剩下的一切  。

• 如果开发人员需要更改用于特定属性的控件的类型,例如从 TextBox 更改为第三方的 HTML 编辑器控件,他/她仅需要确保新控件具有以上属性之一(例如 .Text ),窗体将以与之前完全一致的方式进行工作  。

• 全部使用 TextBox 控件也可以快速生成窗体,但输入仍将转换为适用于业务对象属性的正确类型  。例如,可以用 TextBox 控件来代替 Calendar 控件或第三方的日期选取器控件  。只要用户输入 DateTime 字符串作为值,便会将 TextBox 的 .Text 属性中的值转换为 DateTime,就如同它是日历控件上的 SelectedDate 属性一样  。如果以后将 TextBox 更改为日期选取器控件,逻辑关系将保持不变  。

• 通过将所有控件更改为 Literal 控件,开发人员还可以快速创建“视图”页面  。Literal 的 .Text 属性将被设置为业务对象属性的值,就如同它是 TextBox 一样  。

• 在实际方案中,窗体还包含其他数据类型和自定义配置  。用于处理这些特定操作的代码可以放置在对 BindObjectToControls 和 BindControlsToObject 的调用之后  。 

  
性能和 FormBinding 方案的扩展

  有些开发人员可能想知道,使用反射引起的性能下降是否值得  。在我的测试中,使用了具有七种属性(int DocumentID、bool Active、DateTime Created、int CategoryID、String Title、string Author 和 String htmlText)的对象,BindObjectToControls 用时约 1/3 毫秒,BindControlsToObject 用时大约 1 毫秒  。这些值是通过循环运行 1000 次 BindObjectToControls 和 BindControlsToObject 方法得到的  。对于常见的“添加”和“编辑”窗体方案,这样的性能应不会引起任何重大的问题,而且确实能够提高开发速度和灵活性  。

尽管此方法几乎适用于每种窗体,但有时可能需要修改以上代码  。在某些方案中,开发人员要使用的控件可能并不使用以上属性之一作为其主要接口  。在此情形中,需要更新 FormBinding 方法,以包括该属性和类型  。 

  结论

  这两种 FormBinding 方法(BindObjectToControls 和 BindControlsToObject)可用于极大地简化窗体代码,并为ASP.NET 窗体的开发提供了最大的灵活性  。对它们的使用使我获益良多,希望您的团队同样能够从中受益  。 

  参考资料 

  • ASP.NET Unleashed    
• Programming Microsoft ASP.NET 


作者简介 

John Dyer 是 Dallas Theological Seminary 的顶级 Web 开发人员,负责指导他们富有盛誉的联机培训计划,该计划建立在 Telligent System 的 Community Server 之上  。