使用 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] 作為資料來源
視您已安裝的本機DB 或 SQL Express 而定,連線至 LocalDB 或 SQL Express,然後輸入 產品 做為資料庫名稱
選取 [ 確定 ],系統會詢問您是否要建立新的資料庫,選取 [ 是]
新的資料庫現在會出現在 [伺服器總管] 中,以滑鼠右鍵按一下它,然後選取 [ 新增查詢]
將下列 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 作為連接字串的名稱,然後按 [下一步]
按一下 [資料表] 旁的核取方塊以匯入所有資料表,然後按一下 [完成]
一旦反向工程師程式完成,新的模型就會新增至您的專案,並開啟以供您在 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 檔案
按兩下 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 屬性取得該資料來源按一下完成。
[資料來源] 視窗會在 MainWindow.xaml 視窗旁邊開啟。如果 [資料來源] 視窗 未顯示,請選取 [檢視 - > 其他 Windows- > 資料來源]
按下釘選圖示,讓 [資料來源] 視窗不會自動隱藏。 如果視窗已經可見,您可能需要按 [重新整理] 按鈕。
選取 [ 類別 ] 資料來源,然後將它拖曳到表單上。
當我們拖曳此來源時,會發生下列情況:
- 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 。
表單看起來應該像這樣:
新增處理資料互動的程式碼
是時候將一些事件處理常式新增至主視窗了。
在 XAML 視窗中,按一下 < Window 元素,這會選取主視窗
在 [ 屬性] 視窗中,選擇 右上方的事件 ,然後按兩下 [載入] 標籤右側的 文字方塊
此外,請按兩下設計工具中的 [儲存] 按鈕,以新增 [儲存 ] 按鈕的 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 資料庫。
在頂端方格中輸入類別名稱,而底部方格 中的產品名稱請勿在識別碼資料行中輸入任何專案,因為主鍵是由資料庫產生
按下 [ 儲存] 按鈕將資料儲存至資料庫
呼叫 DbCoNtext 的 SaveChanges() 之後,識別碼會填入資料庫產生的值。 因為我們在 SaveChanges() 之後 呼叫 Refresh() DataGrid 控制項也會以新的值更新。
其他資源
若要深入瞭解如何使用 WPF 將資料系結至集合,請參閱 WPF 檔中的本主題 。