首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Windows Forms 數據綁定實戰:結合 ADO.NET 與自定義實體打造高效數據驅動應用

     編輯:關於.NET

Windows Forms 的數據綁定架構在 .NET Framework 2.0 中迎來重大升級,BindingSource、BindingNavigator 與 DataGridView 等控件讓開發者能以極少的代碼快速構建功能完整的數據驅動介面。本文將透過一個完整的 Northwind 訂單管理範例,示範如何放棄 DataSet,改用自定義實體類別搭配泛型集合,並結合 ADO.NET 直接操作資料庫,打造出既具備豐富 UI 交互又保持架構清晰的企業級應用程序。

應用程序總覽:訂單管理的完整體驗

範例應用程序提供直觀的訂單瀏覽、新增、刪除、修改、查詢與導航功能。左側 DataGridView 載入所有訂單列表,右側則以 TextBox、ComboBox 等控件顯示當前選中訂單的詳細信息。所有控件均綁定至同一 BindingSource,確保數據同步無縫銜接。

圖 1:DataGridView 中瀏覽 Northwind 訂單

頂部的 BindingNavigator 工具欄集成了標準導航按鈕(首筆、上一筆、下一筆、末筆)、新增/刪除/保存按鈕,以及一個搜索框(望遠鏡圖標),支援按訂單編號模糊查詢。右側詳細區域針對外鍵欄位(如 EmployeeID、CustomerID)採用 ComboBox 呈現,顯示員工姓名與客戶公司名稱而非無意義的 ID 值,大幅提升使用者體驗。

核心架構:自定義實體與 INotifyPropertyChanged

雖然 DataSet 功能強大,但自定義實體類別能提供更強的類型安全、更清晰的領域模型,並便於單元測試。本範例定義了 Order、Customer、Employee 三大實體,均實作 INotifyPropertyChanged 介面,確保屬性變更能自動通知 UI 更新。

以 Customer 類為例:

public class Customer : INotifyPropertyChanged
{
    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;
                OnPropertyChanged("CompanyName");
            }
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

關鍵在於 Set 存取器中先比較新舊值,僅在真正變更時觸發事件,避免不必要的 UI 重繪。BindingSource 內建監聽此事件,實現雙向綁定的自動刷新。

資料存取層:泛型集合與 ADO.NET 高效讀寫

資料存取層採用靜態方法封裝 CRUD 操作,回傳 List<T> 泛型集合。相較於 ArrayList,泛型集合提供編譯期類型檢查,省去強制轉換,程式碼更簡潔安全:

// 取得所有訂單
public static List<Order> GetEntityList()
{
    var list = new List<Order>();
    using (var cn = new SqlConnection(Common.ConnectionString))
    using (var cmd = new SqlCommand("pr_Order_GetList", cn) { CommandType = CommandType.StoredProcedure })
    {
        cn.Open();
        using (var rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
        {
            while (rdr.Read())
                list.Add(FillOrder(rdr));
        }
    }
    return list;
}

// 由 DataReader 填充實體
private static Order FillOrder(SqlDataReader rdr)
{
    return new Order
    {
        OrderID = Convert.ToInt32(rdr["OrderID"]),
        CustomerID = rdr["CustomerID"].ToString(),
        EmployeeID = Convert.ToInt32(rdr["EmployeeID"]),
        OrderDate = Convert.ToDateTime(rdr["OrderDate"]),
        ShipVia = rdr["ShipVia"].ToString(),
        ShipName = rdr["ShipName"].ToString(),
        ShipAddress = rdr["ShipAddress"].ToString(),
        ShipCity = rdr["ShipCity"].ToString(),
        ShipCountry = rdr["ShipCountry"].ToString()
    };
}

使用 SqlDataReader 而非 DataTable 可減少記憶體開銷,配合 CommandBehavior.CloseConnection 確保連線隨 Reader 關閉而自動釋放。寫入操作同樣封裝於靜態方法,透過輸出參數取得新增記錄的自動編號:

private static Order AddEntity(Order order)
{
    using (var cn = new SqlConnection(Common.ConnectionString))
    using (var cmd = new SqlCommand("pr_Order_Add", cn) { 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));

        var pOrderID = new SqlParameter("@orderID", SqlDbType.Int) { Direction = ParameterDirection.Output };
        cmd.Parameters.Add(pOrderID);

        cn.Open();
        cmd.ExecuteNonQuery();
        int newID = Convert.ToInt32(pOrderID.Value);
        return GetEntity(newID); // 回傳完整實體(含資料庫端計算欄位)
    }
}

表現層:Visual Studio 設計時綁定與 BindingSource 協作

圖 2:資料來源視窗中的自定義實體

在「資料來源」視窗中加入專案參考的實體類別後,即可為每個屬性指定生成的控件類型(如 OrderDate 對應 DateTimePicker,OrderID 設為 Label 唯讀)。針對外鍵屬性(CustomerID、EmployeeID),將控件類型改為 ComboBox,稍後再分別綁定客戶與員工清單。

將 Order 實體以「詳細資料」模式拖曳至表單,VS 自動生成:

  • 右側所有輸入控件及其綁定
  • BindingSource (orderBindingSource)
  • BindingNavigator (頂部工具欄)

再將 Order 實體以 DataGridView 模式拖曳至左側區域,VS 偵測到現有 BindingSource 而不再重複建立,確保左右兩側控件共享同一資料游標。

ComboBox 外鍵綁定技巧:雙重 BindingSource 協作

CustomerID ComboBox 需同時滿足兩個條件:

  1. 顯示客戶清單(CompanyName)供使用者選擇
  2. 將選中的 CustomerID 寫回 Order 實體

解決方案:

  1. 在資料來源視窗加入 Customer 實體
  2. 將 Customer 資料來源拖曳至 CustomerID ComboBox 上,VS 自動建立第二個 BindingSource (customerBindingSource)
  3. 設定 ComboBox 屬性:
    DataSource = customerBindingSource
    DisplayMember = "CompanyName"
    ValueMember = "CustomerID"
    DataBindings.Add("SelectedValue", orderBindingSource, "CustomerID")

EmployeeID ComboBox 同理處理。此技巧讓「顯示清單」與「寫回值」分離綁定,互不干擾。

執行期綁定程式碼:BindingList 為橋樑

表單建構子中呼叫 SetupBindings(),將各實體的 List<T> 包裝為 BindingList<T> 並指派給對應 BindingSource:

private void SetupBindings()
{
    orderBindingSource.DataSource = new BindingList<Order>(Order.GetEntityList());
    customerBindingSource.DataSource = new BindingList<Customer>(Customer.GetEntityList());
    employeeBindingSource.DataSource = new BindingList<Employee>(Employee.GetEntityList());
}

BindingList 實作 IBindingList,支援排序、搜尋與變更通知,是 Windows Forms 綁定的最佳介面卡。

刪除、新增與更新:事件處理程序中的業務邏輯

BindingNavigator 的「刪除」按鈕預設行為直接移除 BindingSource 項目,但實際專案通常需確認對話框、呼叫資料存取層、錯誤處理。因此將 DeleteItem 設為「無」,自行處理 Click 事件:

private void Delete()
{
    var order = orderBindingSource.Current as Order;
    if (order == null) return;

    if (MessageBox.Show($"確定刪除訂單 {order.OrderID}?", "確認", MessageBoxButtons.OKCancel) != DialogResult.OK)
        return;

    Order.DeleteEntity(order);      // 資料庫端刪除
    orderBindingSource.RemoveCurrent(); // UI 端移除
    MessageBox.Show($"訂單 {order.OrderID} 已刪除。");
}

「新增」按鈕會在 BindingSource 末尾插入新項目並清空詳細區域;「保存」按鈕則統一處理新增與更新:

private void Save()
{
    var order = orderBindingSource.Current as Order;
    if (order == null) return;

    // 驗證邏輯...
    Order.SaveEntity(order); // 內部依 OrderID 判斷 Insert/Update

    // 重新載入列表以同步資料庫端變更(如自動編號、觸發器計算欄位)
    orderBindingSource.DataSource = new BindingList<Order>(Order.GetEntityList());
}

進階搜尋:Predicate 委託與 List.Find 實現模糊查詢

工具欄搜尋框允許使用者輸入訂單編號前碼(如 "102" 查詢 10248、10249...)。利用 List<T>.Find(Predicate<T>) 配合輔助類別實現:

private class OrderFilter
{
    private readonly int _prefix;
    public OrderFilter(int prefix) { _prefix = prefix; }

    public bool BeginsWithOrderID(Order o) 
        => o.OrderID.ToString().StartsWith(_prefix.ToString());
}

private void toolBtnFindOrderNumber_Click(object sender, EventArgs e)
{
    var list = new List<Order>((BindingList<Order>)orderBindingSource.DataSource);
    var filter = new OrderFilter(Convert.ToInt32(toolTxtFindOrderNumber.Text));
    var found = list.Find(filter.BeginsWithOrderID);

    if (found == null)
        MessageBox.Show("找不到符合的訂單");
    else
        orderBindingSource.Position = orderBindingSource.IndexOf(found);
}

Predicate<T> 是泛型委託,List.Find 會依序傳入元素直到委託回傳 true,效能優於手寫迴圈且程式碼更具宣告式風格。

總結

本文完整展示了如何在 Windows Forms 中:

  • 以自定義實體取代 DataSet,配合 INotifyPropertyChanged 實現雙向綁定
  • 善用泛型 List<T> 與 BindingList<T> 獲得類型安全與綁定支援
  • 採用 SqlDataReader 輕量級讀取,搭配存儲過程封裝資料存取邏輯
  • 利用 Visual Studio 設計時綁定快速生成 UI,再以程式碼注入業務規則
  • 運用雙重 BindingSource 解決 ComboBox 外鍵顯示與寫回的雙重需求
  • 透過 Predicate<T> 實現高效的客戶端搜尋

這套架構既保持了關注點分離,又充分發揮 .NET Framework 2.0 綁定基礎設施的威力,是中型企業應用程序的優秀範本。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved