使用 WPF 的資料繫結

重要

本檔僅適用于 .NET Framework 上的 WPF

本檔說明 .NET Framework 上 WPF 的資料系結。 針對新的 .NET Core 專案,建議您使用 EF Core ,而不是 Entity Framework 6。 EF Core 中資料系結的檔如下: 開始使用 WPF

這個逐步解說會示範如何在「主要詳細資料」表單中將 POCO 類型系結至 WPF 控制項。 應用程式會使用 Entity Framework API,將來自資料庫的資料填入物件、追蹤變更,並將資料保存到資料庫。

此模型會定義兩種參與一對多關聯性的類型: Category (principal\master) 和 Product (dependent\detail)。 然後,Visual Studio 工具會用來將模型中定義的類型系結至 WPF 控制項。 WPF 資料系結架構可讓您在相關物件之間巡覽:選取主要檢視中的資料列會導致詳細資料檢視以對應的子資料更新。

本逐步解說中的螢幕擷取畫面和程式代碼清單取自 Visual Studio 2013,但您可以使用 Visual Studio 2012 或 Visual Studio 2010 完成本逐步解說。

使用 'Object' 選項建立 WPF 資料來源

使用舊版 Entity Framework 時,我們用來建議使用 Database 選項,根據使用 EF 設計工具建立的模型來建立新的資料來源。 這是因為設計工具會產生衍生自 ObjectCoNtext 的內容,以及衍生自 EntityObject 的實體類別。 使用 [資料庫] 選項可協助您撰寫與這個 API 介面互動的最佳程式碼。

適用于 Visual Studio 2012 和 Visual Studio 2013 的 EF 設計工具會產生衍生自 DbCoNtext 的內容,以及簡單的 POCO 實體類別。 使用 Visual Studio 2010 時,建議您交換至使用 DbCoNtext 的程式碼產生範本,如本逐步解說稍後所述。

使用 DbCoNtext API 介面時,您應該在建立新的資料來源時使用 [物件 ] 選項,如本逐步解說所示。

如有需要,您可以 針對使用 EF 設計工具 建立的模型還原為 ObjectCoNtext 型程式碼產生。

必要條件

您必須安裝 Visual Studio 2013、Visual Studio 2012 或 Visual Studio 2010,才能完成本逐步解說。

如果您使用 Visual Studio 2010,您也必須安裝 NuGet。 如需詳細資訊,請參閱 安裝 NuGet 。  

建立應用程式

  • 開啟 Visual Studio
  • 檔案 - > 新增 - > 專案...。
  • 在左窗格中選取 [Windows ],並在 右窗格中選取 [WPF][應用程式 ]
  • 輸入 WPFwithEFSample 作為名稱
  • 選取確定

安裝 Entity Framework NuGet 套件

  • 在方案總管中,以滑鼠右鍵按一下 WinFormswithEFSample 專案
  • 選取 [ 管理 NuGet 套件...
  • 在 [管理 NuGet 套件] 對話方塊中,選取 [ 線上 ] 索引標籤,然後選擇 EntityFramework 套件
  • 按一下 [安裝]

    注意

    除了 EntityFramework 元件之外,也會新增 System.ComponentModel.DataAnnotations 的參考。 如果專案具有 System.Data.Entity 的參考,則會在安裝 EntityFramework 套件時移除該專案。 System.Data.Entity 元件不再用於 Entity Framework 6 應用程式。

定義模型

在本逐步解說中,您可以選擇使用 Code First 或 EF Designer 來實作模型。 完成下列兩個區段的其中一個。

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

本節說明如何使用 Code First 建立模型及其相關聯的資料庫。 如果您寧願使用 Database First 從資料庫反向工程模型,請跳至下一節 ( 選項 2:使用 Database First 定義模型)

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

  • 將新的類別新增至 WPFwithEFSample:
    • 以滑鼠右鍵按一下專案名稱
    • 選取 [新增 ],然後 選取 [新增專案]
    • 選取 [類別 ],然後針對類別名稱輸入 Product
  • 下列程式碼取代 Product 類別定義:
    namespace WPFwithEFSample
    {
        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.Collections.ObjectModel;

    namespace WPFwithEFSample
    {
        public class Category
        {
            public Category()
            {
                this.Products = new ObservableCollection<Product>();
            }

            public int CategoryId { get; set; }
            public string Name { get; set; }

            public virtual ObservableCollection<Product> Products { get; private set; }
        }
    }

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

除了定義實體之外,您還需要定義衍生自 DbCoNtext 的類別,並公開 DbSet < TEntity > 屬性。 DbSet < TEntity > 屬性可讓內容知道您要包含在模型中的類型。

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

  • 使用下列定義,將新的 ProductCoNtext 類別新增至專案:
    using System.Data.Entity;

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

編譯專案。

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

本節說明如何使用 Database First,從使用 EF 設計工具從資料庫反向工程模型。 如果您已完成上一節( 選項 1:使用 Code First 定義模型), 請略過本節並直接移至 延遲載入 區段。

建立現有的資料庫

一般而言,當您將目標設為現有的資料庫時,就會建立它,但在本逐步解說中,我們需要建立資料庫才能存取。

隨 Visual Studio 一起安裝的資料庫伺服器會根據您安裝的 Visual Studio 版本而有所不同:

  • 如果您使用 Visual Studio 2010,您將建立 SQL Express 資料庫。
  • 如果您使用 Visual Studio 2012,您將建立 LocalDB 資料庫。

讓我們繼續產生資料庫。

  • 檢視 - > 伺服器總管

  • 以滑鼠右鍵按一下 [資料連線 - > 新增連線...

  • 如果您尚未從 [伺服器總管] 連線到資料庫,則必須先選取 [Microsoft SQL Server] 作為資料來源

    Change Data Source

  • 視您已安裝的本機DB 或 SQL Express 而定,連線至 LocalDB 或 SQL Express,然後輸入 產品 做為資料庫名稱

    Add Connection LocalDB

    Add Connection Express

  • 選取 [ 確定 ],系統會詢問您是否要建立新的資料庫,選取 [ 是]

    Create Database

  • 新的資料庫現在會出現在 [伺服器總管] 中,以滑鼠右鍵按一下它,然後選取 [ 新增查詢]

  • 將下列 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 作為名稱,然後按一下 [ 確定]

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

  • 選取 [從資料庫 產生],然後按 [ 下一步]

    Choose Model Contents

  • 選取您在第一節中建立之資料庫的連線,輸入 ProductCoNtext 作為連接字串的名稱,然後按 [下一步]

    Choose Your Connection

  • 按一下 [資料表] 旁的核取方塊以匯入所有資料表,然後按一下 [完成]

    Choose Your Objects

一旦反向工程師程式完成,新的模型就會新增至您的專案,並開啟以供您在 Entity Framework Designer 中檢視。 App.config 檔案也已新增至您的專案,其中包含資料庫的連線詳細資料。

Visual Studio 2010 中的其他步驟

如果您在 Visual Studio 2010 中工作,則必須更新 EF 設計工具以使用 EF6 程式碼產生。

  • 以滑鼠右鍵按一下 EF Designer 中模型的空白位置,然後選取 [ 新增程式碼產生專案...
  • 從左側功能表中選取 [線上範本 ],然後搜尋 DbCoNtext
  • 選取適用于 C# 的 EF 6.x DbCoNtext 產生器,輸入 ProductsModel 作為名稱, 然後按一下 [新增]

更新資料系結的程式碼產生

EF 會使用 T4 範本從模型產生程式碼。 隨附于 Visual Studio 或從 Visual Studio 資源庫下載的範本適用于一般用途。 這表示從這些範本產生的實體具有簡單的 ICollection < T > 屬性。 不過,使用 WPF 進行資料系結時,最好對集合屬性使用 ObservableCollection ,讓 WPF 能夠追蹤對集合所做的變更。 為此,我們將修改範本以使用 ObservableCollection。

  • 開啟方案總管 並尋找 ProductModel.edmx 檔案

  • 尋找將巢狀在 ProductModel.edmx 檔案底下的 ProductModel.tt 檔案

    WPF Product Model Template

  • 按兩下 ProductModel.tt 檔案,在 Visual Studio 編輯器中開啟它

  • 尋找並將 「 ICollection 」 的兩個出現專案取代為 「 ObservableCollection 」。 這些大約位於 296 和 484 行。

  • 尋找並將第一個出現的 「 HashSet 」 取代為 「 ObservableCollection 」。 此專案大約位於第 50 行。 請勿 取代程式碼稍後找到的第二個 HashSet。

  • 尋找並將唯一出現的 「 System.Collections.Generic 」 取代為 「 System.Collections.ObjectModel 」。 這大約位於第 424 行。

  • 儲存 ProductModel.tt 檔案。 這應該會導致重新產生實體的程式碼。 如果程式碼未自動重新產生,請以滑鼠右鍵按一下 ProductModel.tt,然後選擇 [執行自訂工具]。

如果您現在開啟 Category.cs 檔案 (其巢狀在 ProductModel.tt 下),您應該會看到 Products 集合的類型為 ObservableCollection < Product >

編譯專案。

消極式載入

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

EF 可讓您在第一次存取導覽屬性時,自動從資料庫載入相關實體。 使用這種類型的載入(稱為延遲載入),請注意,當您第一次存取每個導覽屬性時,如果內容不在內容中,就會對資料庫執行個別查詢。

使用 POCO 實體類型時,EF 會在執行時間期間建立衍生 Proxy 類型的實例,然後覆寫類別中的虛擬屬性以新增載入攔截,以達成延遲載入。 若要取得相關物件的延遲載入,您必須將導覽屬性 getter 宣告為 公用 虛擬 在 Visual Basic 中為可 覆寫),而且您的類別不得 密封 Visual Basic 中的 NotOverridable )。 使用 Database First 導覽屬性時,會自動設為虛擬,以啟用延遲載入。 在 [程式碼第一] 區段中,我們選擇將導覽屬性設為虛擬,原因相同。

將物件系結至控制項

新增模型中定義為這個 WPF 應用程式的資料來源的類別。

  • 按兩下 方案總管 中的 MainWindow.xaml 以開啟主表單

  • 從主功能表中,選取 [專案 - > 新增資料來源... ](在 Visual Studio 2010 中,您需要選取 [資料 - > 新增資料來源... ]

  • 在 [選擇資料來源類型] 視窗中,選取 [物件 ],然後按 [ 下一步]

  • 在 [選取資料物件] 對話方塊中,展開 WPFwithEFSample 兩次,然後選取 [類別]
    不需要選取 Product 資料來源,因為我們會透過 Category 資料來源上的 Product 屬性取得該資料來源

    Select Data Objects

  • 按一下完成

  • [資料來源] 視窗會在 MainWindow.xaml 視窗旁邊開啟。如果 [資料來源] 視窗 未顯示,請選取 [檢視 - > 其他 Windows- > 資料來源]

  • 按下釘選圖示,讓 [資料來源] 視窗不會自動隱藏。 如果視窗已經可見,您可能需要按 [重新整理] 按鈕。

    Data Sources

  • 選取 [ 類別 ] 資料來源,然後將它拖曳到表單上。

當我們拖曳此來源時,會發生下列情況:

  • categoryViewSource 資源和 categoryDataGrid 控制項已新增至 XAML
  • 父 Grid 元素上的 DataCoNtext 屬性已設定為 「{StaticResource categoryViewSource }」。 categoryViewSource 資源可作為 outer\parent Grid 元素的系結來源。 然後,內部 Grid 元素會繼承父 Grid 的 DataCoNtext 值(categoryDataGrid 的 ItemsSource 屬性設定為 「{Binding}」)
    <Window.Resources>
        <CollectionViewSource x:Key="categoryViewSource"
                                d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource categoryViewSource}">
        <DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True"
                    ItemsSource="{Binding}" Margin="13,13,43,191"
                    RowDetailsVisibilityMode="VisibleWhenSelected">
            <DataGrid.Columns>
                <DataGridTextColumn x:Name="categoryIdColumn" Binding="{Binding CategoryId}"
                                    Header="Category Id" Width="SizeToHeader"/>
                <DataGridTextColumn x:Name="nameColumn" Binding="{Binding Name}"
                                    Header="Name" Width="SizeToHeader"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>

新增詳細資料方格

既然我們有一個方格可顯示類別,讓我們新增詳細資料格線來顯示相關聯的產品。

  • 從 [ 類別目錄 ] 資料來源底下 選取 [Products ] 屬性,然後將它拖曳到表單上。
    • categoryProductsViewSource 資源和 productDataGrid 方格會新增至 XAML
    • 此資源的系結路徑會設定為 Products
    • WPF 資料系結架構可確保只有與所選類別相關的產品會顯示在 productDataGrid 中
  • 從 [工具箱] 中,將 [按鈕 ] 拖曳 至表單。 將 Name 屬性設定為 buttonSave ,並將 Content 屬性設定為 Save

表單看起來應該像這樣:

Designer Form

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

是時候將一些事件處理常式新增至主視窗了。

  • 在 XAML 視窗中,按一下 < Window 元素,這會選取主視窗

  • 在 [ 屬性] 視窗中,選擇 右上方的事件 ,然後按兩下 [載入] 標籤右側的 文字方塊

    Main Window Properties

  • 此外,請按兩下設計工具中的 [儲存] 按鈕,以新增 [儲存 ] 按鈕的 Click 事件

這會帶您前往表單的程式碼後置,我們現在會編輯程式碼,以使用 ProductCoNtext 來執行資料存取。 更新 MainWindow 的程式碼,如下所示。

程式碼會宣告 ProductCoNtext 長時間執行的 實例。 ProductCoNtext 物件可用來查詢及儲存資料至資料庫。 然後,會從覆寫 的 OnClosing 方法呼叫 ProductCoNtext 實例上的 Dispose( )。 程式碼批註提供程式碼功能的詳細資料。

    using System.Data.Entity;
    using System.Linq;
    using System.Windows;

    namespace WPFwithEFSample
    {
        public partial class MainWindow : Window
        {
            private ProductContext _context = new ProductContext();
            public MainWindow()
            {
                InitializeComponent();
            }

            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                System.Windows.Data.CollectionViewSource categoryViewSource =
                    ((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));

                // Load is an extension method on IQueryable,
                // defined in the System.Data.Entity namespace.
                // This method enumerates the results of the query,
                // similar to ToList but without creating a list.
                // When used with Linq to Entities this method
                // creates entity objects and adds them to the context.
                _context.Categories.Load();

                // After the data is loaded call the DbSet<T>.Local property
                // to use the DbSet<T> as a binding source.
                categoryViewSource.Source = _context.Categories.Local;
            }

            private void buttonSave_Click(object sender, RoutedEventArgs e)
            {
                // When you delete an object from the related entities collection
                // (in this case Products), the Entity Framework doesn’t mark
                // these child entities as deleted.
                // Instead, it removes the relationship between the parent and the child
                // by setting the parent reference to null.
                // So we manually have to delete the products
                // that have a Category reference set to null.

                // The following code uses LINQ to Objects
                // against the Local collection of Products.
                // 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 use 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);
                    }
                }

                _context.SaveChanges();
                // Refresh the grids so the database generated values show up.
                this.categoryDataGrid.Items.Refresh();
                this.productsDataGrid.Items.Refresh();
            }

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

    }

測試 WPF 應用程式

  • 編譯並執行應用程式。 如果您使用 Code First,您會看到 已為您建立 WPFwithEFSample.ProductCoNtext 資料庫。

  • 在頂端方格中輸入類別名稱,而底部方格 中的產品名稱請勿在識別碼資料行中輸入任何專案,因為主鍵是由資料庫產生

    Main Window with new categories and products

  • 按下 [ 儲存] 按鈕將資料儲存至資料庫

呼叫 DbCoNtext 的 SaveChanges() 之後,識別碼會填入資料庫產生的值。 因為我們在 SaveChanges() 之後 呼叫 Refresh() DataGrid 控制項也會以新的值更新。

Main Window with IDs populated

其他資源

若要深入瞭解如何使用 WPF 將資料系結至集合,請參閱 WPF 檔中的本主題