使用 WPF 进行数据绑定
重要
本文档仅适用于 .NET Framework 上的 WPF
本文档介绍 .NET Framework 上 WPF 的数据绑定。 对于新的 .NET Core 项目,建议使用 EF Core 而不是实体框架 6。 有关 EF Core 中数据绑定的文档,请参阅 WPF 入门。
本分步演练介绍如何以“主-从”形式将 POCO 类型绑定到 WPF 控件。 应用程序使用实体框架 API,用数据库中的数据填充对象、跟踪更改并将数据保存到数据库。
该模型定义了两种参与一对多关系的类型:类别(主体\主)和产品(从属\从)。 然后,使用 Visual Studio 工具将模型中定义的类型绑定到 WPF 控件。 WPF 数据绑定框架允许在相关对象之间导航:在主视图中选择行将导致详细信息视图使用相应的子数据进行更新。
本演练中的屏幕截图和代码列表取自 Visual Studio 2013,但你可以使用 Visual Studio 2012 或 Visual Studio 2010 完成本演练。
使用“对象”选项创建 WPF 数据源
对于之前的实体框架版本,我们曾建议在基于使用 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”和右侧窗格中的“WPFApplication”
- 输入 WPFwithEFSample 作为名称
- 选择“确定”
安装实体框架 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 定义模型
本部分演示如何使用 Code First 创建模型及其关联的数据库。 如果想要使用 Database First 通过 EF Designer 从数据库对模型实施反向工程,请跳到下一部分(选项 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; }
}
}
“类别”类上的“产品”属性和“产品”类上的“类别”属性是导航属性。 在实体框架中,导航属性提供了一种在两个实体类型之间导航关系的方法。
除了定义实体外,还需要定义派生自 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 Designer 从数据库对模型实施反向工程。 如果已经完成上一部分(选项 1:使用 Code First 定义模型),请跳过本部分,直接转到“延迟加载”部分。
创建现有数据库据
通常,当目标为现有数据库时,该数据库已经创建完成,但在本演练中,我们需要创建一个要访问的数据库。
随 Visual Studio 安装的数据库服务器因你安装的 Visual Studio 版本而异:
- 如果使用 Visual Studio 2010,你将创建一个 SQL Express 数据库。
- 如果使用 Visual Studio 2012,你将创建一个 LocalDB 数据库。
接下来,生成数据库。
“视图”->“服务器资源管理器”
右键单击“数据连接”->“添加连接...”
如果尚未从服务器资源管理器连接到数据库,则需要选择 Microsoft SQL Server 作为数据源
连接到 LocalDB 或 SQL Express,具体取决于你安装的是哪一个,然后输入 Products 作为数据库名称
选择“确定”,系统会询问你是否要创建新数据库,请选择“是”
新数据库现在将出现在服务器资源管理器中,右键单击它并选择“新建查询”
将以下 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 中包含的实体框架设计器来创建模型。
“项目”->“添加新项...”
从左侧菜单中选择“数据”,然后选择“ADO.NET 实体数据模型”
输入“ProductModel”作为名称并单击“确定”
此操作将启动实体数据模型向导
选择“从数据库生成”,然后单击“下一步”
选择在第一部分中创建的数据库连接,输入“ProductContext”作为连接字符串的名称,然后单击“下一步”
单击“表”旁边的复选框以导入所有表,然后单击“完成”
反向工程完成后,会将新模型添加到项目中并打开,以便在 Entity Framework Designer 中查看。 App.config 文件也已添加到项目中,其中包含数据库的连接详细信息。
Visual Studio 2010 中的其他步骤
如果使用 Visual Studio 2010,则需要更新 EF Designer 才能使用 EF6 代码生成。
- 在 EF 设计器中右键单击模型的空白处,然后选择“添加代码生成项...”
- 从左侧菜单中选择“联机模板”并搜索“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 编辑器中打开
找到并用“ObservableCollection”替换两次出现的“ICollection”。 它们大约位于第 296 行和第 484 行。
找到并用“ObservableCollection”替换第一次出现的“HashSet”。 此事件大约位于第 50 行。 请勿替换代码中稍后第二次出现的 HashSet。
找到并用“System.Collections.ObjectModel”替换仅出现一次的“System.Collections.Generic”。 它大约位于第 424 行。
保存 ProductModel.tt 文件。 这应该会导致重新生成实体的代码。 如果未自动重新生成代码,则右键单击 ProductModel.tt,然后选择“运行自定义工具”。
如果现在打开 Category.cs 文件(嵌套在 ProductModel.tt 下),你应该会看到 Products 集合的类型为 ObservableCollection<Product>。
编译该项目。
延迟加载
“类别”类上的“产品”属性和“产品”类上的“类别”属性是导航属性。 在实体框架中,导航属性提供了一种在两个实体类型之间导航关系的方法。
EF 提供了在首次访问导航属性时自动从数据库加载相关实体的选项。 如果使用此类型的加载(称为“延迟加载”),请注意,首次访问每个导航属性时,将对数据库执行单独的查询(如果内容不在上下文中)。
使用 POCO 实体类型时,EF 通过在运行时创建派生代理类型的实例,然后替代类中的虚拟属性以添加加载挂钩,来实现延迟加载。 若要获取相关对象的延迟加载,必须将导航属性 getter 声明为“公共”和“虚拟”(在 Visual Basic 中为 Overridable),并且不得密封类(在 Visual Basic 中为 NotOverridable)。 使用 Database First 时,会自动将导航属性设为虚拟,以启用延迟加载。 在 Code First 部分,出于同样的原因,我们选择将导航属性设为虚拟。
将对象绑定到控件
将在模型中定义的类作为此 WPF 应用程序的数据源添加。
在“解决方案资源管理器”中双击 MainWindow.xaml 以打开主窗体
从主菜单中,选择“项目”->“添加新数据源...”(在 Visual Studio 2010 中,需要选择“数据”->“添加新数据源...”)
在“选择数据源类型”窗口中,选择“对象”并单击“下一步”
在“选择数据对象”对话框中,展开“WPFwithEFSample”两次并选择“Category”
无需选择 Product 数据源,因为我们将通过 Category 数据源上的 Products 属性来访问它单击“完成” 。
“数据源”窗口在 MainWindow.xaml 窗口旁边打开。如果未显示“数据源”窗口,请选择“查看”->“其他窗口”->“数据源”
点击图钉图标,“数据源”窗口将不会自动隐藏。 如果窗口已可见,可能需要点击“刷新”按钮。
选择“Category”数据源,并将其拖到窗体上。
当我们拖动此源时,发生了以下情况:
- 系统将 categoryViewSource 资源和 categoryDataGrid 控件添加到 XAML 中
- 父 Grid 元素上的 DataContext 属性设置为“{StaticResource categoryViewSource }”。 categoryViewSource 资源充当外部\父 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>
添加“详细信息”网格
我们已经有一个显示 Category 的网格,接下来添加一个详细信息网格来显示关联的 Products。
- 从“Category”数据源下选择“Products”属性,并将其拖到窗体上。
- 系统会将 categoryProductsViewSource 资源和 productDataGrid 网格添加到 XAML 中
- 此资源的绑定路径设置为 Products
- WPF 数据绑定框架确保只有与所选 Category 相关的 Products 才会显示在 productDataGrid 中
- 从工具箱中,将 Button 拖到窗体上。 将 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 数据库。
在顶部网格中输入类别名称,在底部网格中输入产品名称。不要在 ID 列中输入任何内容,因为主键是由数据库生成的
按“保存”按钮将数据保存到数据库
调用 DbContext 的 SaveChanges() 之后,ID 将填充为数据库生成的值。 因为我们在 SaveChanges() 之后调用了 Refresh(),所以 DataGrid 控件也会更新为新值。
其他资源
若要详细了解如何使用 WPF 将数据绑定到集合,请参阅 WPF 文档中的此主题。