使用 WinForms 进行数据绑定

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

该模型定义了两种参与一对多关系的类型:Category (principal\master) 和 Product (dependent\detail)。 然后,使用 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
  • “文件”->“新建”->“项目…”
  • 在左窗格中选择“Windows”,并在右窗格中选择“Windows FormsApplication”
  • 输入“WinFormswithEFSample”作为名称
  • 选择“确定”

安装实体框架 NuGet 包

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

    注意

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

为集合实现 IListSource

使用 Windows 窗体时,集合属性必须实现 IListSource 接口以启用使用排序的双向数据绑定。 为此,我们将扩展 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 定义模型

本部分演示如何使用 Code First 创建模型及其关联的数据库。 如果想要使用 Database First 通过 EF Designer 从数据库对模型实施反向工程,请跳到下一部分(选项 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 属性让上下文知道要包括在模型中的类型。 DbContext 和 DbSet 类型在 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 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> 属性。 但是,在执行数据绑定时,需要具有实现 IListSource 的集合属性。 这是我们创建上述 ObservableListSource 类的原因,现在我们要修改模板以使用此类。

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

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

    Product Model Template

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

  • 找到并用“ObservableListSource”替换两次出现的“ICollection”。 它们大约位于第 296 行和第 484 行。

  • 找到并用“ObservableListSource”替换第一次出现的“HashSet”。 此内容大约位于第 50 行。 请勿替换代码中稍后第二次出现的 HashSet

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

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

编译该项目。

延迟加载

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

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

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

将对象绑定到控件

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

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

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

  • 在“选择数据对象”对话框中,展开“WinFormswithEFSample”两次,并选择“Category”。无需选择“Product”数据源,因为我们将通过“Category”数据源上的“Product”的属性来获取

    Data Source

  • 单击“完成”。如果没有出现“数据源”窗口,选择“查看”->“其他窗口”->“数据源”

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

    Data Source 2

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

  • 选择“Category”数据源,并将其拖到窗体上。 默认情况下,会将新的 DataGridView (categoryDataGridView) 和导航工具栏控件添加到设计器中。 这些控件将绑定到 BindingSource (categoryBindingSource) 和绑定导航器 (categoryBindingNavigator) 组件,这两个组件也需要创建。

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

    • 右键单击 DataGridView 控件,然后选择“编辑列…”
    • 选择 CategoryId 列,然后将 ReadOnly 设置为 True
    • 点击“确定”
  • 选择“Category”数据源下的“Products”,并将其拖到窗体上。 productDataGridView 和 productBindingSource 将添加到窗体中。

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

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

    目前为止,我们已在设计器中将 DataGridView 控件与 BindingSource 组件相关联。 在下一部分,我们将向代码隐藏添加代码,以便将 categoryBindingSource.DataSource 设置为 DbContext 当前跟踪的实体集合。 从“Category”下面拖放 Products 时,WinForms 会将 productsBindingSource.DataSource 属性设置为 categoryBindingSource,并将 productsBindingSource.DataMember 属性设置为 Products。 由于此绑定,productDataGridView 中将只显示属于当前所选“Category”的产品。

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

    Form 1 Designer

  • 双击按钮,为保存按钮添加事件处理程序。 此操作可添加事件处理程序,并为你显示窗体的代码隐藏。 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 窗体应用程序

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

    Form 1 Before Save

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

    Form 1 After Save

  • 如果使用 Code First,则还会创建一个 WinFormswithEFSample.ProductContext 数据库

    Server Object Explorer