使用反射將業務對象綁定到 ASP.NET 表單控制項發布日期: 12/10/2004 | 更新日期: 12/10/2004
John Dyer
Dallas Theological Seminary
適用於:
Microsoft Visual Studio 2005 及早期版本
ASP.NET 1.1
C# 程式設計語言
Visual Basic 程式設計語言
摘要:使用反射以單行代碼將業務對象綁定到 ASP.NET Web Form,從而降低複雜性並減少錯誤。(本文包含一些指向英文網站的連結。請注意,在樣本檔案中,程式員的注釋使用的是英文,本文中將其譯為中文是為了便於讀者理解。)
下載 MSDFormBinding.msi 樣本檔案。
本頁內容
|
引言 |
|
簡化和縮短表單代碼 |
|
開始:從反射中檢索屬性列表 |
|
將對象屬性值綁定到控制項 |
|
用已知屬性設定未知控制項的值 |
|
反轉過程:BindControlsToObject |
|
效能和 FormBinding 方案的擴充 |
|
結論 |
|
參考資料 |
引言
在 Web 開發人員的最常見任務之中,有一項任務是他們要反覆執行的:建立更新資料庫表的簡單表單。我們將建立一個列表頁面和一個表單頁面,列表頁面中以表格形式顯示記錄,表單頁面中帶有用於各個資料庫欄位的適當的表單控制項。許多開發人員還使用表示資料庫表的業務對象將程式碼群組織到分為多層的設計中。如果以業務對象 (Document) 來表示資料庫表 (Documents),許多表單的代碼看上去將如下所示:
<script runat="server">protected void Page_Load(Object Src, EventArgs E) {if (!IsPostBack) { Document document = Documents.GetDocument(Request.QueryString["DocumentID"]); Title.Text = document.Title; Active.Checked = document.Active; CreatedDate.Text = document.CreatedDate.ToString(); AuthorID.FindByValue(document.AuthorID.ToString()).Selected = true; // ... 等等 HtmlBody.Text = document.HtmlBody;}}protected void SaveButton_Click(Object Src, EventArgs E) { Document document = Documents.GetDocument(Request.QueryString["DocumentID"]); document.Title = Title.Text; document.Active = Active.Checked; document.CreatedDate = Convert.ToDateTime(CreatedDate.Text); document.AuthorID = Convert.ToInt32(AuthorID.SelectedItem.Value); // ... 等等 document.HtmlBody = HtmlBody.Text; Documents.Update(document);}</script>
返回頁首
簡化和縮短表單代碼
在以上代碼中,對每個控制項進行顯式轉換,並將其設定為表單控制項的正確屬性。根據屬性和表單控制項的數量,這部分代碼可能會變長並難以管理。代碼還應包含類型轉換的錯誤更正和 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 Form的 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)是最常見的控制項屬性。如果可以將系統設計成無論何種控制項類型,都綁定到這些屬性,那麼我們的Binder 方法將適用於使用那四個屬性的任何控制項。
在以下代碼中,我們將第二次使用反射(這一次是對表單控制項使用,而不是對業務對象使用),以確定它是否具有任何常用屬性。如果有,則嘗試將業務對象的屬性值設定為控制項的屬性。作為樣本,我們將對整個 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 之上。他還是廣泛使用的 ASP.NET HTML 編輯程式 FreeTextBox 的作者,該控制項為除他之外的很多人帶來了經濟收益。在業餘時間,他在 Dallas 神學院攻讀神學碩士,並在準備 2005 年 1 月 1 日的婚禮。