使用 WPF 进行数据绑定Databinding with WPF

重要

此文档仅对 .NET Framework 上的 WPF 有效

本文档介绍 .NET Framework 上的 WPF 的数据绑定。 对于新的 .NET Core 项目,建议使用EF Core而不是实体框架6。 EF Core 中的数据绑定的文档将在#778 问题中进行跟踪。

此分步演练演示如何将 POCO 类型绑定到 “主/详细信息” 窗体中的 WPF 控件。 应用程序使用实体框架 Api 使用数据库中的数据填充对象、跟踪更改并将数据保存到数据库。

该模型定义了两种参与一对多关系的类型:类别(主体 \ 主)和产品(依赖 \ 详细信息)。 然后,使用 Visual Studio 工具将模型中定义的类型绑定到 WPF 控件。 WPF 数据绑定框架允许在相关对象之间导航:在主视图中选择行将导致详细信息视图与相应的子数据一起更新。

本演练中的屏幕截图和代码清单取自 Visual Studio 2013,但你可以通过 Visual Studio 2012 或 Visual Studio 2010 完成此演练。

使用 “对象” 选项创建 WPF 数据源Use the ‘Object’ Option for Creating WPF Data Sources

使用早期版本的实体框架我们在基于使用 EF 设计器创建的模型创建新数据源时,建议使用数据库选项。 这是因为设计器将生成派生自 EntityObject 的 ObjectContext 和实体类的上下文。 使用数据库选项有助于编写与此 API 图面交互的最佳代码。

Visual Studio 2012 和 Visual Studio 2013 的 EF 设计器生成一个从 DbContext 派生的上下文以及简单的 POCO 实体类。 在 Visual Studio 2010 中,我们建议使用 DbContext,如本演练稍后所述,换到使用的代码生成模板。

使用 DbContext API 图面时,应在创建新的数据源时使用Object选项,如本演练中所示。

如果需要,可以针对用 EF 设计器创建的模型,恢复为基于 ObjectContext 的代码生成

先决条件Pre-Requisites

需要安装 Visual Studio 2013、Visual Studio 2012 或 Visual Studio 2010 才能完成此演练。

如果你使用的是 Visual Studio 2010,则还必须安装 NuGet。 有关详细信息,请参阅安装 NuGet

创建应用程序Create the Application

  • 打开 Visual Studio
  • 文件- > 新建- > 项目 …。
  • Windows 左侧窗格中选择 “Windows”,然后在右窗格中选择 “ WPFApplication
  • 输入 WPFwithEFSample 作为名称
  • 选择 “确定”

安装实体框架 NuGet 包Install the Entity Framework NuGet package

  • 在解决方案资源管理器中,右键单击WinFormswithEFSample项目
  • 选择 “管理 NuGet 包 …
  • 在 “管理 NuGet 包” 对话框中,选择 “联机“ 选项卡,然后选择 “ EntityFramework “ 包
  • 单击“安装”

    备注

    除了 EntityFramework 程序集之外,还添加了对 System.componentmodel 的引用。 如果项目具有对 EntityFramework 的引用,则在安装包时将被删除。 System.web 程序集不再用于实体框架6应用程序。

定义模型Define a Model

在本演练中,您可以使用 Code First 或 EF 设计器选择实现模型。 完成以下两个部分中的一个。

选项1:使用 Code First 定义模型Option 1: Define a Model using Code First

本部分说明如何使用 Code First 创建模型及其关联的数据库。 如果要 Database First 使用 EF 设计器从数据库中反向工程模型,请跳到下一节(选项2:使用 Database First 定义模型)

使用 Code First 开发时,通常首先编写定义概念(域)模型 .NET Framework 类。

  • 向 WPFwithEFSample 添加新类
    • 右键单击项目名称
    • 选择 “添加“,然后选择 “新建项
    • 选择并输入 Product 类名的产品
  • Product 类定义替换为以下代码:
  1. namespace WPFwithEFSample
  2. {
  3. public class Product
  4. {
  5. public int ProductId { get; set; }
  6. public string Name { get; set; }
  7. public int CategoryId { get; set; }
  8. public virtual Category Category { get; set; }
  9. }
  10. }
  11. - Add a **Category** class with the following definition:
  12. using System.Collections.ObjectModel;
  13. namespace WPFwithEFSample
  14. {
  15. public class Category
  16. {
  17. public Category()
  18. {
  19. this.Products = new ObservableCollection<Product>();
  20. }
  21. public int CategoryId { get; set; }
  22. public string Name { get; set; }
  23. public virtual ObservableCollection<Product> Products { get; private set; }
  24. }
  25. }

Product类上 “类别类” 和 “类别“ 属性的 “产品“ 属性是导航属性。 在实体框架中,导航属性提供了一种方法,用于在两个实体类型之间导航关系。

除了定义实体外,还需要定义派生自 DbContext 的类,并公开 DbSet < TEntity > 属性。 DbSet < TEntity > 属性使上下文知道要在模型中包括的类型。

DbContext 派生类型的实例在运行时管理实体对象,这包括使用数据库中的数据填充对象、更改跟踪以及将数据保存到数据库。

  • 使用以下定义将新的ProductContext类添加到项目:
  1. using System.Data.Entity;
  2. namespace WPFwithEFSample
  3. {
  4. public class ProductContext : DbContext
  5. {
  6. public DbSet<Category> Categories { get; set; }
  7. public DbSet<Product> Products { get; set; }
  8. }
  9. }

编译该项目。

选项2:使用 Database First 定义模型Option 2: Define a model using Database First

本部分说明如何使用 Database First 从使用 EF 设计器的数据库中对模型进行反向工程。 如果已完成上一部分(选项1:使用 Code First 定义模型),则跳过此部分,直接转到延迟加载部分。

创建现有数据库Create an Existing Database

通常,当目标为现有数据库时,它将被创建,但在本演练中,我们需要创建一个要访问的数据库。

随 Visual Studio 一起安装的数据库服务器因安装的 Visual Studio 版本而异:

  • 如果使用的是 Visual Studio 2010,则将创建 SQL Express 数据库。
  • 如果使用的是 Visual Studio 2012,则将创建一个LocalDB数据库。

接下来,生成数据库。

  • 视图- > 服务器资源管理器

  • 右键单击 “数据连接- > 添加连接 …

  • 如果尚未从服务器资源管理器连接到数据库,则需要选择 Microsoft SQL Server 作为数据源

    更改数据源

  • 连接到 LocalDB 或 SQL Express,具体取决于已安装的数据,并输入Products作为数据库名称

    添加连接 LocalDB

    添加连接 Express

  • 选择 “确定” ,系统会询问您是否要创建新数据库,请选择 “是”

    创建数据库

  • 新数据库现在将出现在服务器资源管理器中,右键单击该数据库并选择 “新建查询

  • 将以下 SQL 复制到新的查询中,然后右键单击该查询,然后选择 “执行

  1. CREATE TABLE [dbo].[Categories] (
  2. [CategoryId] [int] NOT NULL IDENTITY,
  3. [Name] [nvarchar](max),
  4. CONSTRAINT [PK_dbo.Categories] PRIMARY KEY ([CategoryId])
  5. )
  6. CREATE TABLE [dbo].[Products] (
  7. [ProductId] [int] NOT NULL IDENTITY,
  8. [Name] [nvarchar](max),
  9. [CategoryId] [int] NOT NULL,
  10. CONSTRAINT [PK_dbo.Products] PRIMARY KEY ([ProductId])
  11. )
  12. CREATE INDEX [IX_CategoryId] ON [dbo].[Products]([CategoryId])
  13. ALTER TABLE [dbo].[Products] ADD CONSTRAINT [FK_dbo.Products_dbo.Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([CategoryId]) ON DELETE CASCADE

反向工程模型Reverse Engineer Model

我们将使用在 Visual Studio 中包含的 Entity Framework Designer 来创建模型。

  • 项目- > 添加新项 .。。

  • 从左侧菜单中选择 “数据“,然后ADO.NET 实体数据模型

  • 输入ProductModel作为名称,然后单击 “确定”

  • 这将启动实体数据模型向导

  • 选择 “从数据库生成“,然后单击 “下一步

    选择模型内容

  • 选择在第一部分中创建的数据库的连接,输入 “ ProductContext “ 作为连接字符串的名称,然后单击 “下一步

    选择连接

  • 单击 “表” 旁边的复选框以导入所有表,然后单击 “完成”

    选择对象

反向工程过程完成后,会将新模型添加到项目中,并打开,以便在 Entity Framework Designer 中查看。 App.config 文件也已添加到您的项目中,其中包含数据库的连接详细信息。

Visual Studio 2010 中的其他步骤Additional Steps in Visual Studio 2010

如果使用的是 Visual Studio 2010,则需要更新 EF 设计器以使用 EF6 代码生成。

  • 在 EF 设计器中右键单击模型的空位置,然后选择 “添加代码生成项 …
  • 从左侧菜单中选择 “联机模板“,然后搜索DbContext
  • 选择适用于 C 的 EF DbContext 生成器 # , 输入ProductsModel作为名称,然后单击 “添加”

更新数据绑定的代码生成Updating code generation for data binding

EF 使用 T4 模板从模型生成代码。 Visual Studio 附带的模板或从 Visual Studio 库下载的模板专用于一般用途。 这意味着从这些模板生成的实体具有简单的 ICollection < T > 属性。 但是,使用 WPF 进行数据绑定时,需要使用ObservableCollection作为集合属性,以便 WPF 可以跟踪对集合所做的更改。 为此,我们将修改模板以使用 ObservableCollection。

  • 打开解决方案资源管理器并查找ProductModel文件

  • 查找ProductModel.tt文件,该文件将嵌套在 ProductModel 文件下

    WPF 产品型号模板

  • 双击 ProductModel.tt 文件以在 Visual Studio 编辑器中将其打开

  • 查找并将 “ICollection“ 的两个匹配项替换为 “ObservableCollection“。 它们大约位于第296行和第484行。

  • 查找并将 “HashSet“ 的第一个匹配项替换为 “ObservableCollection“。 此事件大约位于第50行。 请勿替换稍后在代码中找到的第二个 HashSet。

  • 查找并将 “System.collections.objectmodel.collection” 的唯一匹配项替换为 “****System.Collections.ObjectModel“。 此位置大致位于第424行。

  • 保存 ProductModel.tt 文件。 这会导致重新生成实体的代码。 如果代码未自动重新生成,则右键单击 ProductModel.tt 并选择 “运行自定义工具”。

如果现在打开 Category.cs 文件(该文件嵌套在 ProductModel.tt 下),则应看到 Products 集合具有类型**ObservableCollection < 产品 > **。

编译该项目。

延迟加载Lazy Loading

Product类上 “类别类” 和 “类别“ 属性的 “产品“ 属性是导航属性。 在实体框架中,导航属性提供了一种方法,用于在两个实体类型之间导航关系。

当首次访问导航属性时,EF 使你可以选择自动从数据库加载相关实体。 使用这种类型的加载(称为 “延迟加载”)时,请注意,第一次访问每个导航属性时,将对数据库执行单独的查询(如果内容尚未出现在上下文中)。

使用 POCO 实体类型时,EF 通过在运行时创建派生代理类型的实例,然后重写类中的虚拟属性来添加加载挂钩来实现延迟加载。 若要获取相关对象的延迟加载,必须将导航属性 getter 声明为公共虚拟(在 Visual Basic 中可重写),并且不能密封类(Visual BasicNotOverridable )。 使用时,会自动将 Database First 导航属性设为虚拟,以启用延迟加载。 在 “Code First” 部分中,我们选择将导航属性设置为虚拟,因为同一原因

将对象绑定到控件Bind Object to Controls

将在模型中定义的类作为此 WPF 应用程序的数据源添加。

  • 在解决方案资源管理器中双击 “ mainwindow.xaml “ 以打开主窗体

  • 从主菜单中,选择 “项目- > 添加新数据源 … “ (在 Visual Studio 2010 中,需要选择 “数据- > 添加新数据源 …“)

  • 在 “选择数据源” Typewindow 中,选择 “对象“,然后单击 “下一步

  • 在 “选择数据对象” 对话框中,展开WPFwithEFSample 两次,然后选择 “类别
    不需要选择产品数据源,因为我们将通过类别数据源上的产品属性来访问它。

    选择数据对象

  • 单击 “完成”

  • 如果 “数据源” 窗口未显示,请在 “Mainwindow.xaml” 窗口旁打开 “数据源” 窗口 ,选择 “查看”-“ > 其他 Windows- > 数据源

  • 按固定图标,使 “数据源” 窗口不会自动隐藏。 如果窗口已显示,可能需要按 “刷新” 按钮。

    Data Sources

  • 选择 类别数据源并将其拖到窗体上。

拖动此源时,会发生以下情况:

  • CategoryViewSource资源和categoryDataGrid控件添加到了 XAML
  • 父网格元素上的 DataContext 属性已设置为 “{StaticResource categoryViewSource }”。CategoryViewSource资源用作外部 \ 父网格元素的绑定源。 然后,内部网格元素从父网格继承 DataContext 值(categoryDataGrid 的 System.windows.controls.itemscontrol.itemssource 属性设置为 “{Binding}”)
  1. <Window.Resources>
  2. <CollectionViewSource x:Key="categoryViewSource"
  3. d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
  4. </Window.Resources>
  5. <Grid DataContext="{StaticResource categoryViewSource}">
  6. <DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True"
  7. ItemsSource="{Binding}" Margin="13,13,43,191"
  8. RowDetailsVisibilityMode="VisibleWhenSelected">
  9. <DataGrid.Columns>
  10. <DataGridTextColumn x:Name="categoryIdColumn" Binding="{Binding CategoryId}"
  11. Header="Category Id" Width="SizeToHeader"/>
  12. <DataGridTextColumn x:Name="nameColumn" Binding="{Binding Name}"
  13. Header="Name" Width="SizeToHeader"/>
  14. </DataGrid.Columns>
  15. </DataGrid>
  16. </Grid>

添加详细信息网格Adding a Details Grid

现在,我们有了一个显示类别的网格,接下来要添加详细信息网格以显示关联的产品。

  • 类别数据源下面选择 Products属性,并将其拖到窗体上。
    • CategoryProductsViewSource资源和productDataGrid网格将添加到 XAML
    • 此资源的绑定路径已设置为 “产品”
    • WPF 数据绑定框架可确保仅在productDataGrid中显示与所选类别相关的产品
  • 从 “工具箱” 中,将按钮拖到窗体上。 将 “名称“ 属性设置为buttonSave ,将 “ Content “ 属性设置为 “保存“。

窗体应该如下所示:

Designer

添加处理数据交互的代码Add Code that Handles Data Interaction

现在可以向主窗口添加一些事件处理程序。

  • 在 XAML 窗口中,单击** < window**元素,这将选择主窗口

  • 在 “属性“ 窗口中,选择右上角的 “事件“,然后双击加载的标签右侧的文本框

    主窗口属性

  • 同时,通过双击设计器中的 “保存” 按钮,为 “保存“ 按钮添加Click事件。

这会使你转到窗体的隐藏代码,现在我们将编辑代码以使用 ProductContext 来执行数据访问。 更新 Mainwindow.xaml 的代码,如下所示。

该代码声明了ProductContext的长时间运行的实例。 ProductContext对象用于查询数据并将数据保存到数据库。 然后,从重写的OnClosing方法中调用ProductContext实例上的Dispose () 。代码注释提供有关代码的作用的详细信息。

  1. using System.Data.Entity;
  2. using System.Linq;
  3. using System.Windows;
  4. namespace WPFwithEFSample
  5. {
  6. public partial class MainWindow : Window
  7. {
  8. private ProductContext _context = new ProductContext();
  9. public MainWindow()
  10. {
  11. InitializeComponent();
  12. }
  13. private void Window_Loaded(object sender, RoutedEventArgs e)
  14. {
  15. System.Windows.Data.CollectionViewSource categoryViewSource =
  16. ((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));
  17. // Load is an extension method on IQueryable,
  18. // defined in the System.Data.Entity namespace.
  19. // This method enumerates the results of the query,
  20. // similar to ToList but without creating a list.
  21. // When used with Linq to Entities this method
  22. // creates entity objects and adds them to the context.
  23. _context.Categories.Load();
  24. // After the data is loaded call the DbSet<T>.Local property
  25. // to use the DbSet<T> as a binding source.
  26. categoryViewSource.Source = _context.Categories.Local;
  27. }
  28. private void buttonSave_Click(object sender, RoutedEventArgs e)
  29. {
  30. // When you delete an object from the related entities collection
  31. // (in this case Products), the Entity Framework doesn’t mark
  32. // these child entities as deleted.
  33. // Instead, it removes the relationship between the parent and the child
  34. // by setting the parent reference to null.
  35. // So we manually have to delete the products
  36. // that have a Category reference set to null.
  37. // The following code uses LINQ to Objects
  38. // against the Local collection of Products.
  39. // The ToList call is required because otherwise the collection will be modified
  40. // by the Remove call while it is being enumerated.
  41. // In most other situations you can use LINQ to Objects directly
  42. // against the Local property without using ToList first.
  43. foreach (var product in _context.Products.Local.ToList())
  44. {
  45. if (product.Category == null)
  46. {
  47. _context.Products.Remove(product);
  48. }
  49. }
  50. _context.SaveChanges();
  51. // Refresh the grids so the database generated values show up.
  52. this.categoryDataGrid.Items.Refresh();
  53. this.productsDataGrid.Items.Refresh();
  54. }
  55. protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
  56. {
  57. base.OnClosing(e);
  58. this._context.Dispose();
  59. }
  60. }
  61. }

测试 WPF 应用程序Test the WPF Application

  • 编译并运行该应用程序。 如果使用 Code First,则会看到为你创建WPFwithEFSample. ProductContext数据库。

  • 在顶部网格中输入类别名称,底部网格中的产品名称不要在 ID 列中输入任何内容,因为主键是由数据库生成的

    新类别和产品的主窗口

  • 按 “保存“ 按钮将数据保存到数据库

调用 DbContext 的SaveChanges () 后,将用数据库生成的值填充 id。 因为我们在SaveChanges () 后调用了Refresh () ,所以DataGrid控件也将更新为新值。

主窗口,其中填充了 Id

其他资源Additional Resources

若要了解有关使用 WPF 将数据绑定到集合的详细信息,请参阅 WPF 文档中的此主题