使用 WinForms 进行数据绑定

本分步演练演示如何将 POCO 类型绑定到“主详细信息”窗体中的窗口窗体(WinForms)控件。 应用程序使用 Entity Framework 使用数据库中的数据填充对象、跟踪更改并将数据保存到数据库。

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

本演练中的屏幕截图和代码列表取自 Visual Studio 2013,但你可以使用 Visual Studio 2012 或 Visual Studio 2010 完成本演练。

先决条件

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

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

创建应用程序

  • 打开 Visual Studio
  • 文件 -> 新建 -> Project....
  • 在左窗格中选择 Windows,并在右窗格中选择 Windows FormsApplication
  • 输入 WinFormswithEFSample 作为名称
  • 选择 “确定”

安装 Entity Framework NuGet 包

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

    注释

    除了 EntityFramework 程序集外,还添加了对 System.ComponentModel.DataAnnotations 的引用。 如果项目具有对 System.Data.Entity 的引用,则在安装 EntityFramework 包时会将其删除。 System.Data.Entity 程序集不再用于 Entity Framework 6 应用程序。

为集合实现 IListSource

集合属性必须实现 IListSource 接口,以便在使用 Windows 窗体时启用具有排序功能的双向数据绑定。 为此,我们将扩展 ObservableCollection 以添加 IListSource 功能。

  • ObservableListSource 类添加到项目:
    • 右键单击项目名称
    • 选择“添加 -> 新项目”
    • 选择并输入ObservableListSource作为类名
  • 将默认生成的代码替换为以下代码:

此类启用双向数据绑定和排序。 该类派生自 ObservableCollection<T> ,并添加 IListSource 的显式实现。 实现 IListSource 的 GetList() 方法可返回与 ObservableCollection 保持同步的 IBindingList 实现。 ToBindingList 生成的 IBindingList 实现支持排序。 ToBindingList 扩展方法在 EntityFramework 程序集中定义。

    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public class ObservableListSource<T> : ObservableCollection<T>, IListSource
            where T : class
        {
            private IBindingList _bindingList;

            bool IListSource.ContainsListCollection { get { return false; } }

            IList IListSource.GetList()
            {
                return _bindingList ?? (_bindingList = this.ToBindingList());
            }
        }
    }

定义模型

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

选项 1:首先使用代码定义模型

本部分介绍如何使用 Code First 创建模型及其关联的数据库。 如果您更倾向于使用 Database First 从数据库反向生成模型,请跳到下一节(选项 2:使用 Database First 定义模型)

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

  • 向项目添加新 的 Product
  • 将默认生成的代码替换为以下代码:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        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;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        public class Category
        {
            private readonly ObservableListSource<Product> _products =
                    new ObservableListSource<Product>();

            public int CategoryId { get; set; }
            public string Name { get; set; }
            public virtual ObservableListSource<Product> Products { get { return _products; } }
        }
    }

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

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

  • 向项目添加新 的 ProductContext 类。
  • 将默认生成的代码替换为以下代码:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using System.Text;

    namespace WinFormswithEFSample
    {
        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 作为数据源

    更改数据源

  • 根据您的安装情况,连接到 LocalDB 或 SQL Server Express,并输入 Products 作为数据库名称。

    添加连接 LocalDB

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

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

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

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

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

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

    选择模型内容

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

    选择连接

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

    选择对象

反向工程过程完成后,新模型将添加到项目中,并打开可在实体框架设计器中查看。 App.config 文件也已添加到项目中,其中包含数据库的连接详细信息。

Visual Studio 2010 中的其他步骤

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

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

更新数据绑定的代码生成

EF 使用 T4 模板从模型生成代码。 Visual Studio 随附或从 Visual Studio 库下载的模板适用于常规用途。 这意味着从这些模板生成的实体具有简单的 ICollection<T> 属性。 但是,执行数据绑定时,需要具有实现 IListSource 的集合属性。 这就是为什么我们创建了上述 ObservableListSource 类,现在我们将修改模板以使用此类。

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

  • 查找将嵌套在 ProductModel.edmx 文件下的 ProductModel.tt 文件

    产品模型模板

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

  • 查找“ICollection”的两个出现位置,并将其替换为“ObservableListSource”。 这些大约位于第 296 行和第 484 行。

  • 找出第一个出现的“HashSet”,并将其替换为“ObservableListSource”。 此事件位于大约第 50 行。 请勿 替换代码中稍后出现的第二个 HashSet。

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

如果现在打开Category.cs文件(该文件嵌套在 ProductModel.tt 下),则应看到 Products 集合的类型为 ObservableListSource<Product>

编译项目。

延迟加载

Product 类上的 Products 属性和 Product 类上的 Category 属性是导航属性。 在 Entity Framework 中,导航属性提供了一种导航两种实体类型之间的关系的方法。

EF 提供在首次访问导航属性时自动从数据库加载相关实体的选项。 使用此类型的加载(称为延迟加载),请注意,首次访问每个导航属性时,如果上下文中尚不存在内容,则会对数据库执行单独的查询。

使用 POCO 实体类型时,EF 实现延迟加载的方式是在运行时创建派生代理类型的实例,并重写类中的虚拟属性,以添加加载挂钩。 若要实现相关对象的延迟加载,您需要将导航属性 getter 声明为 公共虚拟(在 Visual Basic 中为可重写),同时,类不能是密封的(Visual Basic 中的 NotOverridable)。 在使用 Database First 方法时,导航属性会自动设为虚拟以实现延迟加载。 在 Code First 部分中,我们选择将导航属性设为虚拟,原因相同

将对象绑定到控件

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

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

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

  • 在“选择数据对象”对话框中,展开 WinFormswithEFSample 两次并选择 “类别 ”,无需选择“产品”数据源,因为我们将通过“类别”数据源上的 Product 属性访问它。

    数据源

  • 单击“ 完成”。 如果“数据源”窗口未显示,请选择“ 视图 -> 其他 Windows-> 数据源”

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

    数据源 2

  • 在解决方案资源管理器中,双击 Form1.cs 文件以在设计器中打开主窗体。

  • 选择 类别 数据源并将其拖动到窗体上。 默认情况下,新的 DataGridView(categoryDataGridView)和导航工具栏控件将添加到设计器中。 这些控件也绑定到已创建的 BindingSource(categoryBindingSource)和 Binding Navigator (categoryBindingNavigator) 组件。

  • 编辑 categoryDataGridView 上的列。 我们希望将 CategoryId 列设置为只读。 保存数据后,数据库将生成 CategoryId 属性的值。

    • 右键单击 DataGridView 控件并选择“编辑列...”
    • 选择 CategoryId 列并将 ReadOnly 设置为 True
    • 按“确定”
  • 从类别数据源下选择“产品”,然后将其拖动到窗体上。 productDataGridView 和 productBindingSource 将添加到窗体中。

  • 编辑 productDataGridView 上的列。 我们希望隐藏 CategoryId 和 Category 列,并将 ProductId 设置为只读。 保存数据后,数据库将生成 ProductId 属性的值。

    • 右键单击 DataGridView 控件并选择“ 编辑列...”
    • 选择 ProductId 列并将 ReadOnly 设置为 True
    • 选择 CategoryId 列,然后按 “删除 ”按钮。 对 “类别” 列执行相同的操作。
    • 按下“确定”

    到目前为止,我们将 DataGridView 控件与设计器中的 BindingSource 组件相关联。 在下一部分中,我们将在后台代码中添加代码,将 categoryBindingSource.DataSource 设置为 DbContext 当前跟踪的实体集合。 当我们从类别下拖放产品时,WinForms 负责将 productsBindingSource.DataSource 属性设置为 categoryBindingSource,将 productsBindingSource.DataMember 属性设置为 Products。 由于此绑定,只有属于当前所选类别的产品才会显示在 productDataGridView 中。

  • 单击鼠标右键并选择“已启用”,在导航工具栏上启用“保存”按钮。

    窗体 1 设计器

  • 通过双击按钮添加保存按钮的事件处理程序。 这将添加事件处理程序,并将你带到窗体的后台代码。 下一部分中将添加 categoryBindingNavigatorSaveItem_Click 事件处理程序的代码。

添加处理数据交互的代码

现在,我们将添加代码以使用 ProductContext 执行数据访问。 更新主窗体窗口的代码,如下所示。

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

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public partial class Form1 : Form
        {
            ProductContext _context;
            public Form1()
            {
                InitializeComponent();
            }

            protected override void OnLoad(EventArgs e)
            {
                base.OnLoad(e);
                _context = new ProductContext();

                // Call the Load method to get the data for the given DbSet
                // from the database.
                // The data is materialized as entities. The entities are managed by
                // the DbContext instance.
                _context.Categories.Load();

                // Bind the categoryBindingSource.DataSource to
                // all the Unchanged, Modified and Added Category objects that
                // are currently tracked by the DbContext.
                // Note that we need to call ToBindingList() on the
                // ObservableCollection<TEntity> returned by
                // the DbSet.Local property to get the BindingList<T>
                // in order to facilitate two-way binding in WinForms.
                this.categoryBindingSource.DataSource =
                    _context.Categories.Local.ToBindingList();
            }

            private void categoryBindingNavigatorSaveItem_Click(object sender, EventArgs e)
            {
                this.Validate();

                // Currently, the Entity Framework doesn’t mark the entities
                // that are removed from a navigation property (in our example the Products)
                // as deleted in the context.
                // The following code uses LINQ to Objects against the Local collection
                // to find all products and marks any that do not have
                // a Category reference as deleted.
                // 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 do 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);
                    }
                }

                // Save the changes to the database.
                this._context.SaveChanges();

                // Refresh the controls to show the values         
                // that were generated by the database.
                this.categoryDataGridView.Refresh();
                this.productsDataGridView.Refresh();
            }

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

测试 Windows 窗体应用程序

  • 编译并运行应用程序,可以测试功能。

    保存前的表单 1

  • 保存存储生成的密钥后,密钥会显示在屏幕上。

    表单 1 保存后

  • 如果使用 Code First,则还会看到已为你创建 WinFormswithEFSample.ProductContext 数据库。

    服务器对象资源管理器