使用 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 作为数据源

    Change Data Source

  • 连接到 LocalDB 或 SQL Express,具体取决于你安装的是哪一个,然后输入 Products 作为数据库名称

    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 中包含的实体框架设计器来创建模型。

  • “项目”->“添加新项...”

  • 从左侧菜单中选择“数据”,然后选择“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 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 文件

    WPF Product Model Template

  • 双击 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 属性来访问它

    Select Data Objects

  • 单击“完成” 。

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

  • 点击图钉图标,“数据源”窗口将不会自动隐藏。 如果窗口已可见,可能需要点击“刷新”按钮。

    Data Sources

  • 选择“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

窗体应如下所示:

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 数据库。

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

    Main Window with new categories and products

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

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

Main Window with IDs populated

其他资源

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