使用 WinForms 進行資料綁定

這個逐步教學展示了如何在「主細節」表單中將 POCO 類型綁定到 Windows Forms(WinForms)控制項。 該應用程式使用 Entity Framework 來填充資料庫中的物件、追蹤變更,並將資料持久化到資料庫中。

該模型定義了參與一對多關係的兩種類型:類別(主要/主控)和產品(依賴/詳細)。 接著,Visual Studio 工具將模型中定義的型別綁定到 WinForms 控制項。 WinForms 資料綁定框架允許在相關物件間的導航:在主檢視中選取列,詳細檢視會更新相應的子資料。

本攻略中的截圖與程式碼列表取自 Visual Studio 2013,但你也可以使用 Visual Studio 2012 或 Visual Studio 2010 完成此攻略。

先決條件

你需要安裝 Visual Studio 2013、Visual Studio 2012 或 Visual Studio 2010 才能完成這個攻略。

如果你用的是 Visual Studio 2010,也必須安裝 NuGet。 欲了解更多資訊,請參閱 安裝 NuGet。

建立應用程式

  • 開啟 Visual Studio
  • 檔案 -> 新檔 -> 專案....
  • 在左側窗格選擇 Windows,在右側窗格選擇 Windows FormsApplication
  • 請輸入 WinFormswithEFSample 作為名稱
  • 選擇 確定

安裝 Entity Framework NuGet 套件

  • 在解決方案總管中,右鍵點擊 WinFormswithEFSample 專案
  • 選擇 管理 NuGet 套件......
  • 在「管理 NuGet 套件」對話框中,選擇 「線上 」標籤並選擇 EntityFramework 套件
  • 按一下 [安裝]

    備註

    除了 EntityFramework 組合外,還新增了對 System.ComponentModel.DataAnnotations 的參考。 如果專案中有 System.Data.Entity 的參考,安裝 EntityFramework 套件時該參考將會被移除。 System.Data.Entity 組合語言已不再用於 Entity Framework 6 應用程式。

實現 IListSource 介面以應用於集合

集合屬性必須實作 IListSource 介面,以啟用 Windows 表單時的雙向資料綁定與排序。 為此,我們將擴充 ObservableCollection 以加入 IListSource 功能。

  • 在專案中新增一個 ObservableListSource 類別:
    • 右鍵點擊專案名稱
    • 選擇 新增 -> 新項目
    • 選擇 類別 並輸入 ObservableListSource 以取得類別名稱
  • 將預設產生的程式碼替換為以下程式碼:

此類別支援雙向資料綁定及排序功能。 該類別源自 ObservableCollection<T> ,並新增了 IListSource 的明確實作。 IListSource 的 GetList() 方法實作用來回傳一個與 ObservableCollection 同步的 IBindingList 實作。 由 ToBindingList 產生的 IBindingList 實作支援排序功能。 ToBindingList 擴充方法定義於 EntityFramework 組合語言中。

    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public class ObservableListSource<T> : ObservableCollection<T>, IListSource
            where T : class
        {
            private IBindingList _bindingList;

            bool IListSource.ContainsListCollection { get { return false; } }

            IList IListSource.GetList()
            {
                return _bindingList ?? (_bindingList = this.ToBindingList());
            }
        }
    }

定義模型

在這個導覽中,你可以選擇使用 Code First 或 EF Designer 實作模型。 完成以下兩個部分之一。

選項一:先用程式碼定義模型

本節將展示如何使用 Code First 建立模型及其相關資料庫。 如果你更願意使用 Database First 方法,利用 EF 設計器從資料庫逆向工程你的模型,可以跳到下一節(選項 2:使用 Database First 定義模型)

使用 Code First 開發時,通常會先撰寫 .NET Framework 類別來定義你的概念(領域)模型。

  • 新增一個產品類別到專案
  • 將預設產生的程式碼替換為以下程式碼:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        public class Product
        {
            public int ProductId { get; set; }
            public string Name { get; set; }

            public int CategoryId { get; set; }
            public virtual Category Category { get; set; }
        }
    }
  • 在專案中新增一個 Category 類別。
  • 將預設產生的程式碼替換為以下程式碼:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        public class Category
        {
            private readonly ObservableListSource<Product> _products =
                    new ObservableListSource<Product>();

            public int CategoryId { get; set; }
            public string Name { get; set; }
            public virtual ObservableListSource<Product> Products { get { return _products; } }
        }
    }

除了定義實體外,你還需要定義一個從 DbContext 衍生並公開 DbSet<TEntity> 屬性的類別。 DbSet 屬性讓上下文知道你想包含哪些類型。 DbContext 與 DbSet 類型皆在 EntityFramework 組合中定義。

DbContext 衍生型別的實例會在運行時間管理實體物件,其中包括將來自資料庫的數據填入對象、變更追蹤,以及將數據保存至資料庫。

  • 新增一個 ProductContext 類別到專案中。
  • 將預設產生的程式碼替換為以下程式碼:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using System.Text;

    namespace WinFormswithEFSample
    {
        public class ProductContext : DbContext
        {
            public DbSet<Category> Categories { get; set; }
            public DbSet<Product> Products { get; set; }
        }
    }

編譯專案。

選項二:使用 Database First 定義模型

本節展示如何使用 Database First 透過 EF 設計器來從資料庫中逆向工程你的模型。 如果你已經完成了前一節(選項一:使用 Code First 定義模型),那就跳過這一段,直接進入 Lazy Loading 章節。

建立現有資料庫

通常當你針對現有資料庫時,資料庫會已經建立好,但這次攻略我們需要建立一個資料庫來存取。

安裝於 Visual Studio 的資料庫伺服器會依你安裝的 Visual Studio 版本而有所不同:

  • 如果你用的是 Visual Studio 2010,你會建立一個 SQL Express 資料庫。
  • 如果你用的是 Visual Studio 2012,那你就是要建立 一個 LocalDB 資料庫。

我們開始產生資料庫。

  • 檢視 -> 伺服器檔案總管

  • 右鍵點擊 資料連線 -> 新增連線...

  • 如果你之前沒從 Server Explorer 連接過資料庫,你需要選擇 Microsoft SQL Server 作為資料來源

    變更資料來源

  • 依你安裝的 LocalDB 或 SQL Express 連接,並輸入 Products 作為資料庫名稱

    新增連線 LocalDB

    新增連接快遞

  • 選擇 確定 ,系統會詢問你是否要建立新的資料庫,請選擇 「是」

    建立資料庫

  • 新資料庫現在會出現在伺服器檔案總管中,右鍵點擊並選擇 新查詢

  • 將以下 SQL 複製到新查詢中,然後右鍵點擊查詢並選擇 執行

    CREATE TABLE [dbo].[Categories] (
        [CategoryId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        CONSTRAINT [PK_dbo.Categories] PRIMARY KEY ([CategoryId])
    )

    CREATE TABLE [dbo].[Products] (
        [ProductId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        [CategoryId] [int] NOT NULL,
        CONSTRAINT [PK_dbo.Products] PRIMARY KEY ([ProductId])
    )

    CREATE INDEX [IX_CategoryId] ON [dbo].[Products]([CategoryId])

    ALTER TABLE [dbo].[Products] ADD CONSTRAINT [FK_dbo.Products_dbo.Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([CategoryId]) ON DELETE CASCADE

逆向工程模型

我們將使用 Visual Studio 內建的 Entity Framework Designer,來建立我們的模型。

  • 專案 -> 新增項目...

  • 從左側選單選擇 資料 ,然後 ADO.NET 實體資料模型

  • 輸入 ProductModel 作為名稱並點 選確定

  • 這會啟動 實體資料模型精靈

  • 選擇「從資料庫產生」並點擊「下一步」

    選擇模型內容

  • 選擇你在第一節建立的資料庫連結,輸入 ProductContext 作為連接字串的名稱,然後點擊 「下一步」

    選擇您的連線

  • 點選「表格」旁的勾選框以匯入所有表格,然後點擊「完成」

    選擇你的物件

逆向工程完成後,新模型會加入你的專案,並開放在實體框架設計器中查看。 你的專案中也新增了一個 App.config 檔案,裡面有資料庫的連線細節。

Visual Studio 2010 的額外步驟

如果你是在 Visual Studio 2010 工作,那你需要更新 EF 設計器以使用 EF6 程式碼產生。

  • 在 EF Designer 中右鍵點選模型的空白位置,選擇 「新增程式碼產生項目...
  • 從左側選單選擇 「線上範本」 並搜尋 DbContext
  • 選擇 EF 6.x 的 C# 資料庫上下文產生器, 輸入 ProductsModel 作為名稱,然後點擊新增

更新資料綁定的程式碼產生

EF 會用 T4 範本從你的模型產生程式碼。 隨 Visual Studio 附帶或從 Visual Studio 圖庫下載的範本,皆供一般用途使用。 這表示從這些範本產生的實體具有簡單的 ICollection<T> 屬性。 然而,在進行資料綁定時,最好能有實作 IListSource 的集合屬性。 這就是為什麼我們建立了上述 ObservableListSource 類別,現在我們會修改範本以利用這個類別。

  • 打開 解決方案總管 ,找到 ProductModel.edmx 檔案

  • 找到會被置入 ProductModel.edmx 檔案下方的 ProductModel.tt 檔案

    產品模型範本

  • 雙擊 ProductModel.tt 檔案即可在 Visual Studio 編輯器中開啟

  • 找出並用「ObservableListSource」替換兩次出現的「ICollection」。 這些列車大約位於296號線和484號線。

  • 找到並替換第一個出現的「HashSet」為「ObservableListSource」。 此現象位於約第50行。 不要 替換程式碼後面第二次出現的 HashSet。

  • 儲存 ProductModel.tt 檔案。 這應該會讓實體的程式碼被重新生成。 如果程式碼沒有自動重新生成,請右鍵點擊 ProductModel.tt 並選擇「執行自訂工具」。

如果你現在打開Category.cs檔案(它位於 ProductModel.tt 下巢狀),你會看到 Products 集合的類型是 ObservableListSource<Product>。

編譯專案。

延遲載入

Product 類別上的 Products 屬性和 Product 類別上的 Category 屬性是導覽屬性。 在 Entity Framework 中,導覽屬性提供一種方式來巡覽兩個實體類型之間的關聯性。

EF 提供一個選項,讓你在第一次存取導航屬性時自動從資料庫載入相關實體。 使用這種類型的負載(稱為延遲載入),第一次存取每個導覽屬性時,請注意,若內容未載入至上下文中,便會對資料庫執行個別查詢。

使用 POCO 實體類型時,EF 透過在執行時建立衍生代理類型實例,然後覆寫類別中的虛擬屬性以附加載入掛鉤來達成延遲載入。 要讓相關物件的 lazy 載入,你必須將導航屬性 getter 宣告為 公開 和 虛擬 (在 Visual Basic 中為 可覆寫),且你的類別不能是 封閉的 (在 Visual Basic 中為 NotOverridable)。 使用 Database First 時,導覽屬性會自動設為虛擬的,以啟用延遲載入。 在「Code First」部分,我們選擇讓導航屬性虛擬化,原因相同

將對象系結至控件

將模型中定義的類別加入為此 WinForms 應用程式的資料來源。

  • 從主選單中,選擇 專案 -> 新增資料來源...... (在 Visual Studio 2010 中,你需要選擇 資料 -> 新增資料來源......)

  • 在「選擇資料來源類型」視窗中,選擇物件並點擊「下一頁」

  • 在「選擇資料物件」對話框中,展開兩次 WinFormswithEFSample 並選擇 「類別 」。不需要選擇「產品」資料來源,因為我們會透過「類別」資料來源上的「產品」屬性進入。

    資料來源

  • 點擊完成。如果資料來源視窗沒有顯示,請選擇「檢視」->「其他視窗->資料來源」

  • 按下針腳圖示,這樣資料來源視窗就不會自動隱藏。 如果視窗已經顯示,您可能需要按下重新整理按鈕。

    資料來源2

  • 在解決方案總管中,雙擊 Form1.cs 檔案即可在設計器中開啟主表單。

  • 選擇「 分類 資料來源」並在表單上拖曳。 預設情況下,設計器會新增新的 DataGridView(categoryDataGridView)與導航工具列控制項。 這些控制項綁定於 BindingSource(categoryBindingSource)和 Binding Navigator(categoryBindingNavigator)元件,並同時建立。

  • 編輯 categoryDataGridView 上的欄位。 我們想將 CategoryId 欄位設為唯讀。 CategoryId 屬性的值會在儲存資料後由資料庫產生。

    • 右鍵點擊 DataGridView 控制項,選擇編輯欄位...
    • 選擇 CategoryId 欄位,並將 ReadOnly 設為 True。
    • 按下確定鍵
  • 從分類資料來源下選擇產品,然後在表單中拖曳。 productDataGridView 和 productBindingSource 會被加入表單中。

  • 編輯 productDataGridView 上的欄位。 我們想隱藏 CategoryId 和 Category 欄位,並將 ProductID 設為唯讀。 ProductId 屬性的值會在儲存資料後由資料庫產生。

    • 右鍵點選 DataGridView 控制項,選擇 編輯欄位...。
    • 選擇 ProductID 欄位,並將 ReadOnly 設 為 true。
    • 選擇 CategoryId 欄位並按下 移除 按鈕。 對 分類 欄位也做同樣的處理。
    • 按 [確定]。

    到目前為止,我們將 DataGridView 控制項與設計器中的 BindingSource 元件關聯起來。 在下一節,我們將為程式碼後續新增程式碼,將 categoryBindingSource.DataSource 設定為目前由 DbContext 追蹤的實體集合。 當我們從 Category 底下拖放 Products 時,WinForms 會幫忙把 productsBindingSource.DataSource 屬性設定到 categoryBindingSource,然後把 productsBindingSource.DataMember 屬性設定成 Products。 由於此綁定,只有屬於目前所選類別的產品會顯示在 productDataGridView 中。

  • 點擊滑鼠右鍵並選擇啟用,啟用導航工具列的儲存按鈕。

    表單1設計師

  • 雙擊按鈕即可新增存檔按鈕的事件處理程式。 這樣會新增事件處理程序,並帶你進入表單背後的程式碼。 categoryBindingNavigatorSaveItem_Click事件處理程式的程式碼將在下一節加入。

新增處理資料互動的程式碼

我們現在會新增程式碼,讓它使用 ProductContext 來執行資料存取。 請如下所示更新主表單視窗的程式碼。

該程式碼宣告了一個長期執行的 ProductContext 實例。 ProductContext 物件用於查詢並儲存資料到資料庫。 接著,ProductContext 實例上的 Dispose() 方法會從覆寫的 OnClosing 方法中呼叫。 程式碼註解會詳細說明程式碼的功能。

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public partial class Form1 : Form
        {
            ProductContext _context;
            public Form1()
            {
                InitializeComponent();
            }

            protected override void OnLoad(EventArgs e)
            {
                base.OnLoad(e);
                _context = new ProductContext();

                // Call the Load method to get the data for the given DbSet
                // from the database.
                // The data is materialized as entities. The entities are managed by
                // the DbContext instance.
                _context.Categories.Load();

                // Bind the categoryBindingSource.DataSource to
                // all the Unchanged, Modified and Added Category objects that
                // are currently tracked by the DbContext.
                // Note that we need to call ToBindingList() on the
                // ObservableCollection<TEntity> returned by
                // the DbSet.Local property to get the BindingList<T>
                // in order to facilitate two-way binding in WinForms.
                this.categoryBindingSource.DataSource =
                    _context.Categories.Local.ToBindingList();
            }

            private void categoryBindingNavigatorSaveItem_Click(object sender, EventArgs e)
            {
                this.Validate();

                // Currently, the Entity Framework doesn’t mark the entities
                // that are removed from a navigation property (in our example the Products)
                // as deleted in the context.
                // The following code uses LINQ to Objects against the Local collection
                // to find all products and marks any that do not have
                // a Category reference as deleted.
                // The ToList call is required because otherwise
                // the collection will be modified
                // by the Remove call while it is being enumerated.
                // In most other situations you can do LINQ to Objects directly
                // against the Local property without using ToList first.
                foreach (var product in _context.Products.Local.ToList())
                {
                    if (product.Category == null)
                    {
                        _context.Products.Remove(product);
                    }
                }

                // Save the changes to the database.
                this._context.SaveChanges();

                // Refresh the controls to show the values         
                // that were generated by the database.
                this.categoryDataGridView.Refresh();
                this.productsDataGridView.Refresh();
            }

            protected override void OnClosing(CancelEventArgs e)
            {
                base.OnClosing(e);
                this._context.Dispose();
            }
        }
    }

測試 Windows 表單應用程式

  • 編譯並執行應用程式,你可以測試功能。

    存檔前的表格1

  • 儲存商店後,螢幕上會顯示產生的金鑰。

    表單 1 存檔後

  • 如果你使用 Code First,你也會看到一個 WinFormswithEFSample.ProductContext 資料庫為你建立。

    伺服器物件總管