本頁內容
引言在 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 窗體的 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 窗體上的任何地方,使用任何類和控件的任意組合進行調用,而這確實有效,F在,我們需要創建在提交窗體時進行反轉的方法。我們需要從表示用戶輸入的控件中檢索新值,而不是將控件屬性的值設置為業務對象的值。 反轉過程: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)為開發人員創建窗體提供了很大的靈活性。它們還可以用于處理以下這些常見方案:
性能和 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 窗體的開發提供了最大的靈活性。對它們的使用使我獲益良多,希望您的團隊同樣能夠從中受益。 |
文章來源于領測軟件測試網 http://www.kjueaiud.com/