Windows Forms 綁定控件顯著改進了過去的數據綁定控件。它們可使您快速處理與設置窗體有關的冗 余任務,您也可以對其行為進行廣泛地自定義和擴展。數據可在各種容器中傳輸,包括 DataSet 和自定 義類實體,Windows® Forms 綁定工具使您能夠綁定到所有這些類型的對象。如果不想使用 DataSet ,可以創建自定義實體以用作您的應用程序的數據存儲,並可以使用 List<T> 和其他集合類型來 存儲自定義實體集。可使用 BindingSource 和 BindingNavigator 輕松地綁定這些類型的自定義實體。 在本專欄中,我將說明如何使用 Microsoft® .NET Framework 2.0 中的現有綁定工具綁定業務實體 的自定義列表,我也將為此而編寫一個功能完善的數據驅動 Windows Forms 應用程序。
首先介紹此應用程序,特別是它的 DataGridView、BindingSource 和 BindingNavigator 綁定控件的 使用。然後介紹較低的層次並演示它們的體系結構以及如何對數據進行檢索、保留、訪問和發回數據庫。 示例應用程序的所有代碼都包括在本期的下載文件中。
測試驅動應用程序
此應用程序將允 許用戶查看、添加、刪除、更新、查找和導航記錄。它會將 Northwind 訂單數據加載到 DataGridView, 如圖 1 所示。選擇訂單後,窗體右側的 TextBox、ComboBox 和其他控件會填充所選訂單的信息。所有控 件都可通過 BindingSource 控件綁定到同一個數據源。
圖 1在 DataGridView 中查看 Northwind 訂單
在圖 1 中,BindingNavigator 控件是一個跨窗體頂部顯 示的工具欄。它包含標准的導航按鈕,用於更改屏幕將顯示的訂單記錄。導航按鈕應與左側的網格結合使 用,該網格可使這些按鈕與當前記錄保持同步。工具欄還包含一些用於執行添加、刪除和更新訂單信息的 事件處理程序的按鈕。最後,應用程序允許您搜索特定訂單(注意望遠鏡圖標)。
可使用 ComboBox 控件顯示代表外鍵引用的訂單記錄的字段。例如,ComboBox 可用來顯示銷售人員(即雇員)的 名單。特定訂單的銷售人員將在 ComboBox 中進行選擇。這一方法比顯示“雇員 ID”要更好 一些,因為後者很可能對應用程序的用戶沒有什麼意義。在圖 1 中,請注意,ComboBox 中顯示的是雇員 名稱,而不是雇員 ID。ComboBox 中還將顯示客戶名稱。
實現自定義實體和接口
雖然 DataSet 是數據訪問庫中一個功能強大的工具,但在應用程序中使用自定義類管理和表示該數據模型也是 很有效的。有關這兩種方法的優缺點的討論非常多,DataSet 或自定義類兩大陣營各自都有大批的堅守者 。事實上,在企業體系結構中這兩種方法都是可行的。另外,ADO.NET 工具可與 DataSet 和自定義類配 合使用來創建表示數據對象的實體。關鍵在於您必須具有某種數據存儲才能包含您的數據。在本應用程序 中,我將使用自定義實體。
此示例應用程序包含兩個項目:一個用於表示數據,另一個用於業務 邏輯和數據訪問。在較低層創建自定義實體時,您必須為該實體創建屬性。例如,Customer 類具有 CustomerID 屬性和 CompanyName 屬性。圖 2 顯示表示 CustomerID 和 CompanyName 的私有字段和公共 屬性。盡管鍵入此代碼會讓人有些乏味,尤其與使用 DataSet 相比更是如此,但使用一些可即時生成屬 性的重構工具甚至代碼生成工具來生成整個類,可使類的創建非常簡單。
Figure 2 Customer 類屬性
public event PropertyChangedEventHandler PropertyChanged; private string _CustomerID; public string CustomerID { get { return _CustomerID; } set { _CustomerID = value; } } private string _CompanyName; public string CompanyName { get { return _CompanyName; } set { if (!value.Equals(_CompanyName)) { _CompanyName = value; if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("CompanyName")); } } } }
請看一下圖 2 中的代碼。在本例中,Customer 類實現了一個 INotifyPropertyChanged 接口 ,如果 CompanyName 屬性發生變化,該接口會觸發名為 PropertyChanged 的事件。請注意, CompanyName 屬性的 set 訪問器將進行檢查,以確保此屬性值在設置新值前已經發生實際變化。如果是 這樣,PropertyChanged 事件會被觸發且該類會通知所有偵聽此更改的對象。在我的應用程序中, BindingSource 將在被通知更改時自動用新值更新窗體上的控件。
應用程序還包含以下三個實體類:Order、Customer 和 Employee。所有這些類都實現 INotifyPropertyChanged 接口並包含用於處理屬性值的獲取和設置的 Property 訪問器。
泛型和 ADO.NET
建立實體後,必須創建一些方法用來檢索和保存這些實體。此應用程序的實體實現了一個 包含以下部分或全部靜態方法的標准列表:GetEntity、GetEntityList、SaveEntity 和 DeleteEntity。
可為這些實體創建一個類並創建一個單獨的類用來包含數據訪問方法。通常,只有在體系結構顯 示實體與用於對其進行保存和檢索的方法並不是緊密耦合時,我才會將這些類分離。在本示例應用程序中 ,由於方法緊密耦合,所以我選擇將這些方法置於一個單獨的類並使其成為靜態方法。
圖 3 顯示 GetEntityList<Order> 方法,該方法返回表示 Northwind 數據庫中的所有訂單的 Order 實體列 表。(當然,如果我們已針對特定客戶或日期范圍添加了訂單參數,系統可能會對該列表進行篩選。)通 過使用泛型並返回 List<T>(而不是 ArrayList),代碼可保證 List 中所包含的任何對象均為類 型 T。這也意味著您可以訪問 List<T> 中的實體的屬性,而不必將其轉換為該實體類型,因為如 果實體存儲在非泛型列表中,您必須執行此操作。例如,您可使用以下代碼從列表中的第一個訂單獲取 OrderID:
List<Order> orderList = GetMyListOfOrders(); int orderID = orderList[0].OrderID;
Figure 3 獲取 List <Order>
public static List<Order> GetEntityList() { List<Order> OrderList = new List<Order>(); using (SqlConnection cn = new SqlConnection(Common.ConnectionString)) { string proc = "pr_Order_GetList"; using (SqlCommand cmd = new SqlCommand(proc, cn)) { cmd.CommandType = CommandType.StoredProcedure; cn.Open(); SqlDataReader rdr = cmd.ExecuteReader( CommandBehavior.CloseConnection); while (rdr.Read()) { Order order = FillOrder(rdr); OrderList.Add(order); } if (!rdr.IsClosed) rdr.Close(); } } return OrderList; }
如果實體存儲在非泛型列表中,那麼必須將對象轉換為該類型:
ArrayList orderList = GetMyListOfOrders(); int orderID = ((Order)orderList[0])).OrderID;
我本可以使用訂單創建和填充 DataSet 或 DataTable。但我卻選擇了使用 SqlDataReader,因為我更喜歡更快速地訪問它所提供的數據。如圖 3 所 示,我通過 SqlDataReader 獲取數據,為每個行實例化並填充 Order 實體,然後將該實體添加到 List<Order> 中,並在 SqlDataReader 中對每行重復這一過程。我本來還可以使用 DataTable 並 對 DataRow 進行迭代。性能差異是微不足道的,但對於本例來說,造成 DataTable 額外開銷確實沒有什 麼好處,因為我只是迭代了各個行和填充自己的自定義實體列表。FillOrder 方法執行以下代碼,從而創 建 Order 實例並從 SqlDataReader 設置其屬性:
Order order = new Order(); order.OrderID = Convert.ToInt32(rdr["OrderID"]); order.CustomerID = rdr["CustomerID"].ToString(); order.EmployeeID = Convert.ToInt32(rdr["EmployeeID"]); order.OrderDate = Convert.ToDateTime(rdr["OrderDate"]); order.ShipVia = rdr["ShipVia"].ToString(); order.ShipName = rdr["ShipName"].ToString(); order.ShipAddress = rdr["ShipAddress"].ToString(); order.ShipCity = rdr["ShipCity"].ToString(); order.ShipCountry = rdr["ShipCountry"].ToString(); return order;
請注意,在圖 3 中,我將 CommandBehavior.CloseConnection 傳遞給了 ExecuteReader 方法。這將使 SqlConnection 對象在 SqlDataReader 關閉時立即關閉。
實體中 還有一些靜態方法可用於插入、更新和刪除數據。我已創建了一個名為 SaveEntity 的公共靜態方法,它 將接受實體並確定是新實體還是現有實體,然後調用相應的存儲過程以執行動作查詢。Customer 類的靜 態 AddEntity(在圖 4 中顯示)接受 Order 實體,然後將該實體的值映射到存儲過程的相應參數。
Figure 4 添加實體
private static Order AddEntity(Order order) { int orderID = 0; using (SqlConnection cn = new SqlConnection(Common.ConnectionString)) { string proc = "pr_Order_Add"; using (SqlCommand cmd = new SqlCommand(proc, cn)) { cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@customerID", order.CustomerID); cmd.Parameters.AddWithValue("@employeeID", order.EmployeeID); cmd.Parameters.AddWithValue("@orderDate", order.OrderDate); cmd.Parameters.AddWithValue("@shipVia", order.ShipVia); cmd.Parameters.AddWithValue("@shipName", GetValue(order.ShipName)); cmd.Parameters.AddWithValue("@shipAddress", GetValue(order.ShipAddress)); cmd.Parameters.AddWithValue("@shipCity", GetValue(order.ShipCity)); cmd.Parameters.AddWithValue("@shipCountry", GetValue(order.ShipCountry)); cmd.Parameters.Add(new SqlParameter("@orderID", SqlDbType.Int)); cmd.Parameters["@orderID"].Direction = ParameterDirection.Output; cn.Open(); cmd.ExecuteNonQuery(); orderID = Convert.ToInt32(cmd.Parameters["@orderID"].Value); } order = GetEntity(orderID); } return order; }
ADO.NET 和自定義實體都是此體系結構的重要部分。檢索方法使用 ADO.NET 從數據庫獲取數 據;然後填充自定義實體並將其返回到表示層。SaveEntity 和 DeleteEntity 方法接受自定義實體,然 後提取這些實體的值以對數據庫應用更改。
數據源
我已經介紹了如何創建、填充和返回自 定義實體,下面讓我們看一下表示層。由於我的類庫項目中含有 Customer、Order 和 Employee 類,所 以我可以使用這些類在窗體中創建綁定控件。“數據源”窗口顯示可供項目使用的所有數據源 。在圖 5 中,您可以看到我已經將在類庫項目中創建的三個實體添加到 UI 項目的“數據源 ”窗口中。
圖 5數據源窗口
可從 Web 服務、對象或數據庫獲取數據源。在本例中,我已通過使用 類實體添加了對象數據源。我已通過完成相應向導添加了數據源,該向導會提示您選擇要作為數據源添加 的命名空間和類。如果已經引用類庫項目,將顯示 Customer、Employee 和 Order 類。
添加數據 源後,數據源將顯示在“數據源”窗口中。圖 5 中的 Order 數據源顯示 Order 類的所有公 共屬性。每個屬性名旁邊都有一個圖標,表示將用於顯示各屬性值的控件的類型。OrderDate 屬性顯示 DateTimePicker 控件,ShipAddress 顯示 TextBox 控件。可通過單擊“數據源”窗口中的屬 性名稱文本並從下拉列表中選擇其他控件來更改這些控件。例如,我已將 OrderID 屬性的控件改為 Label 控件,因為我希望它是只讀的。
我不希望代表指向其他實體的引用字段的屬性(如 Order.CustomerID 屬性)直接顯示出來。而是想為其顯示更具描述性的值。例如,我已將 CustomerID 屬性的控件類型改為 ComboBox,以在列表中填充所有客戶數據並顯示相應客戶的 CompanyName 屬性。我 已將 CustomerID 和 EmployeeID 屬性改為 ComboBox。也可以通過從列表中選擇 [無] 選項(而不是控 件類型)指定不想在窗體中顯示屬性。
設置完我的屬性和要在其中顯示這些屬性的控件後,我單 擊“數據源”窗口中的 Order 實體,然後從列表中選擇“詳細信息”。這允許我 將 Order 數據源拖至窗體,並在窗體中自動生成顯示控件和綁定控件(BindingSource 和 BindingNavigator)。這就是圖 1 中窗體右側控件的創建方式。
綁定控件
BindingSource 是鏈接在 Order 實體和窗體控件之間的一個不可見控件。BindingSource 中的當前 Order 實體將顯示在 屏幕(可在此屏幕中查看或編輯這些控件)右側的控件中。在本應用程序中,該實體將綁定到 List<Order> 實體(我將從 Order 類的靜態方法 GetEntityList 向其傳遞信息)。
BindingNavigator 控件也可通過將 Order 數據源拖至窗體而創建。此控件將在窗體的頂部顯示一個 工具欄,默認情況下,此工具欄中將包含幾個按鈕,用於添加、保存和刪除記錄以及進行導航。由於 BindingNavigator 與 BindingSource 控件互相同步,所以當某個記錄被重新定位在其中一個控件時,另 一個控件將自動反映位置更改。例如,如果用戶在 BindingNavigator 控件中單擊“下一個” 按鈕,BindingSource 控件便會將其當前實體更改為下一個實體,而且右側控件將相應顯示當前實體的屬 性值。
BindingNavigator 的按鈕也屬於控件,因此它們也有一些屬性和事件可以進行設置。我已 為工具欄上的“保存”和“刪除”按鈕添加了事件處理程序,因此我可以在將更改 後的數據保留在數據庫之前通過先前創建的實體靜態數據訪問方法添加驗證和其他邏輯。
BindingNavigator 控件會處理記錄移動並可提供一種保存數據更改的簡便方法,您不需要包含它 。通過將數據源拖動到窗體而自動創建控件後,您可以將其從窗體中刪除,而不會產生任何不良影響。當 然,之後您必須編寫您自己的代碼以實現在整個訂單列表中移動和保存更改。是否應執行此操作完全取決 於您的 UI 要求。
CustomerID 的 ComboBox 當前被綁定到 Order 數據源的 CustomerID 屬性( 通過 BindingSource 控件)。我仍需要使用客戶列表來填充 ComboBox,並顯示客戶的 CompanyName。我 在創建了自己的類庫項目後,創建了一個 Customer 實體並在其中提供了 GetEntityList<Customer> 方法。然後,我為 Customer 實體添加了數據源。我所需要做的只是將 Customer 數據源拖放到 CustomerID ComboBox 上。這將創建第二個 BindingSource 控件。ComboBox 使 用這一新的 BindingSource 來加載客戶列表以進行顯示。因此,所選的 ComboBox 值仍綁定到 Order 的 BindingSource 控件,客戶列表及其 CompanyName 屬性顯示在此控件中。重復這一過程以填充 Employee ComboBox 的列表。由於我將數據源放到了 ComboBox 上,因此這足以說明我並不需要其他 BindingNavigator 控件。
不過,除了 BindingNavigator 的工具欄之外,我還想為用戶提供第二 種導航訂單記錄的方法。所以我返回到了“數據源”窗口,單擊 Order 數據源,將選項改為 DataGridView 並將 Order 數據源拖至窗體中。這樣便在窗體上創建了一個 DataGridView 控件,其中包 含一個表示 Order 實體的各個屬性的列。然後,我刪除了大多數列,以便僅在網格中顯示重要信息。由 於窗體中已經存在一個綁定 Order 數據源的 BindingSource 控件,所以不會創建任何其他綁定控件。此 時,所有控件的當前訂單都完全相同,因為這些控件均鏈接到 Order 的 BindingSource 控件。
綁定代碼
現在,我已經設計了一些控件,但還沒有為窗體編寫任何代碼。客戶、雇員和訂單的 BindingSource 控件全部設置完畢,即刻可以使用,但我尚未向其傳遞相應的數據。可通過首先調用每個 實體的靜態方法 GetEntityList,然後再檢索實體的 List<T> 輕松執行此操作。之後 List<T> 會被轉換為 BindingList<T>,並被設置為各個相應 BindingSource 控件的數據源 。
圖 6 顯示如何將這三個 BindingSource 控件中的每一個設置為各自的列表。這就是用戶在 UI 中導航和查看數據時所需的全部代碼。我在窗體的構造函數中調用 SetupBindings 方法,因此在窗體初 次加載時檢索、綁定和顯示數據。然後,用戶可使用 BindingNavigator 工具欄上的導航按鈕或通過在 DataGridView 控件中選擇一行來浏覽記錄。但是,我們仍必須編寫允許用戶進行更改的事件處理程序。
Figure 6 設置 BindingSource 控件的數據
private void SetupBindings() { BindingList<Order> orderList = new BindingList<Order>(Order.GetEntityList()); orderBindingSource.DataSource = orderList; BindingList<Customer> customerList = new BindingList<Customer>(Customer.GetEntityList()); customerBindingSource.DataSource = customerList; BindingList<Employee> empList = new BindingList<Employee>(Employee.GetEntityList()); employeeBindingSource.DataSource = empList; }
保存數據
我希望應用程序允許用戶通過單擊工具欄上的“刪除”按鈕刪除當前選擇和顯示的訂單。首先,我將 “刪除”按鈕的 DeleteItem 屬性設置為“無”,以強制其使用自定義代碼來執行刪除。接下來,我添加 執行刪除的事件處理程序,它將調用名為 Delete 的私有方法,如圖 7 中所示。
Figure 7 刪除當前訂單
private void Delete() { Order order = orderBindingSource.Current as Order; int orderID = order.OrderID; DialogResult dlg = MessageBox.Show( string.Format("是否確實要刪除訂單 {0}?", orderID.ToString())); if (dlg == System.Windows.Forms.DialogResult.OK) { Order.DeleteEntity(order); orderBindingSource.RemoveCurrent(); MessageBox.Show(string.Format( "訂單 {0} 已刪除。", orderID.ToString())); } }
Delete 方法從 BindingSource 的 Current 屬性獲取當前訂單,並將其轉換為實體類型 Order。然後 ,該方法會詢問用戶是否確定要刪除訂單。如果用戶單擊“確定”,Order 實體將被傳遞給類庫項目中的 DeleteEntity 靜態方法。最後,通過執行 BindingSource 控件的 RemoveCurrent 方法從 BindingSource 刪除訂單。
BindingNavigator 的工具欄還有一個“添加”按鈕,用於將新行添加到 DataGridView 並清除窗口右 側的控件。然後,用戶可輸入和選擇新訂單的值,然後單擊 BindingNavigator 工具欄上的“保存”按鈕 。我已向此“保存”按鈕添加了一個事件處理程序,以將訂單實體傳遞給 Order 實體的 SaveEntity 靜 態方法。用戶還可以編輯當前的訂單記錄,並單擊同一“保存”按鈕以將編輯後的訂單實體傳遞給 Order 實體的 SaveEntity 靜態方法。SaveEntity 方法將通過檢查 Order 實體的 OrderID 屬性的值處理插入 和更新。插入或更新實體後,通過重新獲取訂單列表並重置 BindingSource 的數據源來重新加載 DataGridView,從而使數據最新。
使用謂詞查找實體
創建一個友好的用戶界面非常重要。如果用戶知道 Order ID,就可能想要跳到特定訂單。我通過將 ToolStripTextBox 控件添加到 BindingNavigator(用戶可在其中輸入要查找的完整或部分 OrderID)處 理這種情況。然後,我添加了一個新的工具欄按鈕和一個用於啟動訂單記錄搜索的事件處理程序。圖 1 顯示了這些控件以及用戶可在其中按 Order ID 查找訂單的工具提示文本。
使用 Select 方法或 Find 方法在 DataTable 內查找 DataRow 非常簡單。但是僅僅因為我使用自定 義實體並不意味著我必須放棄類似的功能。在本例中,我必須在 List<Order> 中查找以用戶在搜 索控件中輸入的值開頭的 Order 實體。List<T> 提供了一些方法來協助在其列表中查找一個或多 個項目,其中包括 Find、FindAll、FindIndex、FindLast 和 FindLastIndex 方法。我將使用 Find 方 法,它接受 Predicate<T> 作為其唯一的參數。
Predicate<T> 必須為 Predicate<Order>,因為我有一個 List<Order>。 Predicate 是一個委托類型,可在 List<Order> 中搜索與所定義條件(即以在搜索字段中輸入的 搜索值開頭)相匹配的 Order 實體。創建 Predicate 之前,我首先創建了一個類以便來協助搜索(如圖 8 所示)。OrderFilter 類在其構造函數中接受此訂單以進行搜索。該類還具有兩個用於查找特定實體的 方法。每個方法都返回一個布爾值,指示是否存在匹配項。這些方法是傳遞給 Predicate 的委托的基礎 。
Figure 8 OrderFilter 類
private class OrderFilter { private int orderID = 0; public OrderFilter(int orderID) { this.orderID = orderID; } public bool MatchesOrderID(Order order) { return order.OrderID == orderID; } public bool BeginsWithOrderID(Order order) { return order.OrderID.ToString().StartsWith(orderID.ToString()); } }
圖 9 顯示了用於查找訂單和重新定位 BindingSource 的代碼。首先,我從 BindingSource 獲取對 Order 實體列表的引用,以使代碼更易讀。然後,我會創建一個 OrderFilter 類的實例,並用要搜索的 訂單 ID 對其進行初始化。接下來,我創建 Predicate 並向其傳遞 OrderFilter 類的 BeginsWithOrderID 方法。最後,我執行 List<Order> 的 Find 方法並向其傳遞剛剛創建的 Predicate。這將依次迭代 Order 實體的列表並將其全部傳遞給 OrderFilter.BeginsWithOrderID 方法 。返回 true 的第一個實體將被返回,然後用於將 BindingSource 重新定位到其索引。
Figure 9 查找訂單
private void toolBtnFindOrderNumber_Click(object sender, EventArgs e) { List<Order> orderList = new List<Order>( orderBindingSource.DataSource as BindingList<Order>); OrderFilter orderFilter = new OrderFilter( Convert.ToInt32(toolTxtFindOrderNumber.Text)); Predicate<Order> filterByOrderID = new Predicate<Order>(orderFilter.BeginsWithOrderID); Order order = orderList.Find(filterByOrderID); if (order == null) MessageBox.Show("未找到相符的訂單", "未找到", MessageBoxButtons.OK); else { int index = orderBindingSource.IndexOf(order); orderBindingSource.Position = index; } }
總結
在本專欄中,我演示了 .NET Windows Forms 綁定控件的強大功能。綁定控件可與體系結構中的現有 實體或 DataSet 一起用來快速創建綁定窗體。如果您發現這些控件缺少任何功能,可通過對窗體進行自 定義來得到所需的功能。無論您是喜歡自定義實體還是 DataSet,兩個工具都可通過使用數據綁定在 .NET 企業應用程序中實現。
將您想向 John 詢問的問題和提出的意見發送至 [email protected].
本文配套源碼:http://www.bianceng.net/dotnet/201212/767.htm