通过 WinForms 进行数据绑定Databinding with WinForms

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

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

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

先决条件Pre-Requisites

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

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

创建应用程序Create the Application

  • 打开 Visual Studio
  • 文件->> 项目 。
  • 在左侧窗格中选择 “ windows “,在右窗格中选择 “ windows FormsApplication
  • 输入WinFormswithEFSample作为名称
  • 选择“确定”

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

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

    备注

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

为集合实现 IListSourceImplementing IListSource for Collections

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

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

此类启用双向数据绑定和排序。类从 ObservableCollection 派生 并添加 IListSource 的显式实现。实现 IListSource 的执行 getlist ()方法以返回保持与 ObservableCollection 同步的 IBindingList 实现。对 tobindinglist 获得生成的 IBindingList 实现支持排序。对 tobindinglist 获得扩展方法是在 EntityFramework 程序集中定义的。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.ComponentModel;
  5. using System.Diagnostics.CodeAnalysis;
  6. using System.Data.Entity;
  7. namespace WinFormswithEFSample
  8. {
  9. public class ObservableListSource<T> : ObservableCollection<T>, IListSource
  10. where T : class
  11. {
  12. private IBindingList _bindingList;
  13. bool IListSource.ContainsListCollection { get { return false; } }
  14. IList IListSource.GetList()
  15. {
  16. return _bindingList ?? (_bindingList = this.ToBindingList());
  17. }
  18. }
  19. }

定义模型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 类。

  • 将新产品类添加到项目
  • 将默认生成的代码替换为以下代码:
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. namespace WinFormswithEFSample
  7. {
  8. public class Product
  9. {
  10. public int ProductId { get; set; }
  11. public string Name { get; set; }
  12. public int CategoryId { get; set; }
  13. public virtual Category Category { get; set; }
  14. }
  15. }
  • Category类添加到项目。
  • 将默认生成的代码替换为以下代码:
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. namespace WinFormswithEFSample
  7. {
  8. public class Category
  9. {
  10. private readonly ObservableListSource<Product> _products =
  11. new ObservableListSource<Product>();
  12. public int CategoryId { get; set; }
  13. public string Name { get; set; }
  14. public virtual ObservableListSource<Product> Products { get { return _products; } }
  15. }
  16. }

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

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

  • 向项目中添加一个新的ProductContext类。
  • 将默认生成的代码替换为以下代码:
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data.Entity;
  4. using System.Linq;
  5. using System.Text;
  6. namespace WinFormswithEFSample
  7. {
  8. public class ProductContext : DbContext
  9. {
  10. public DbSet<Category> Categories { get; set; }
  11. public DbSet<Product> Products { get; set; }
  12. }
  13. }

编译该项目。

选项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作为名称,然后单击 “确定”

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

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

    ChooseModelContents

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

    选择连接

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

    选择对象

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

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

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

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

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

EF 使用 T4 模板从模型生成代码。 Visual Studio 附带的模板或从 Visual Studio 库下载的模板专用于一般用途。 这意味着从这些模板生成的实体具有简单的 ICollection 属性。 但是,在执行数据绑定时,需要具有实现 IListSource 的集合属性。 这就是我们创建上述 ObservableListSource 类的原因,现在我们要修改模板以使用此类。

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

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

    产品型号模板

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

  • 查找并将 “ICollection“ 的两个匹配项替换为 “ObservableListSource“。 这些代码位于大约第296行和第484行。

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

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

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

编译该项目。

延迟加载Lazy Loading

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

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

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

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

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

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

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

  • 在 “选择数据对象” 对话框中,展开WinFormswithEFSample两次,然后选择 “类别“,因为我们将通过类别数据源上的产品属性来获取产品数据源。

    数据源

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

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

    数据源2

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

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

  • 编辑categoryDataGridView上的列。 我们想要将 “类别 id “ 列设置为只读。 保存数据后,数据库将生成 “类别 id “ 属性的值。

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

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

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

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

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

    窗体1设计器

  • 双击按钮,为 “保存” 按钮添加事件处理程序。 这将添加事件处理程序,并将你带入窗体的隐藏代码。 下一部分将添加categoryBindingNavigatorSaveItem_单击“事件处理程序” 的代码。

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

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

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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Drawing;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using System.Windows.Forms;
  10. using System.Data.Entity;
  11. namespace WinFormswithEFSample
  12. {
  13. public partial class Form1 : Form
  14. {
  15. ProductContext _context;
  16. public Form1()
  17. {
  18. InitializeComponent();
  19. }
  20. protected override void OnLoad(EventArgs e)
  21. {
  22. base.OnLoad(e);
  23. _context = new ProductContext();
  24. // Call the Load method to get the data for the given DbSet
  25. // from the database.
  26. // The data is materialized as entities. The entities are managed by
  27. // the DbContext instance.
  28. _context.Categories.Load();
  29. // Bind the categoryBindingSource.DataSource to
  30. // all the Unchanged, Modified and Added Category objects that
  31. // are currently tracked by the DbContext.
  32. // Note that we need to call ToBindingList() on the
  33. // ObservableCollection<TEntity> returned by
  34. // the DbSet.Local property to get the BindingList<T>
  35. // in order to facilitate two-way binding in WinForms.
  36. this.categoryBindingSource.DataSource =
  37. _context.Categories.Local.ToBindingList();
  38. }
  39. private void categoryBindingNavigatorSaveItem_Click(object sender, EventArgs e)
  40. {
  41. this.Validate();
  42. // Currently, the Entity Framework doesn’t mark the entities
  43. // that are removed from a navigation property (in our example the Products)
  44. // as deleted in the context.
  45. // The following code uses LINQ to Objects against the Local collection
  46. // to find all products and marks any that do not have
  47. // a Category reference as deleted.
  48. // The ToList call is required because otherwise
  49. // the collection will be modified
  50. // by the Remove call while it is being enumerated.
  51. // In most other situations you can do LINQ to Objects directly
  52. // against the Local property without using ToList first.
  53. foreach (var product in _context.Products.Local.ToList())
  54. {
  55. if (product.Category == null)
  56. {
  57. _context.Products.Remove(product);
  58. }
  59. }
  60. // Save the changes to the database.
  61. this._context.SaveChanges();
  62. // Refresh the controls to show the values
  63. // that were generated by the database.
  64. this.categoryDataGridView.Refresh();
  65. this.productsDataGridView.Refresh();
  66. }
  67. protected override void OnClosing(CancelEventArgs e)
  68. {
  69. base.OnClosing(e);
  70. this._context.Dispose();
  71. }
  72. }
  73. }

测试 Windows 窗体应用程序Test the Windows Forms Application

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

    保存前窗体1

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

    保存后窗体1

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

    服务器对象资源管理器