本分步演练演示如何将 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 属性可让上下文知道要在模型中包括的类型。 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 设计器从数据库中逆向工程您的模型。 如果已完成上一部分(选项 1:使用 Code First 定义模型),请跳过本部分并直接转到 “延迟加载 ”部分。
创建现有数据库
通常,当你针对现有数据库时,该数据库已创建,但在本教程中,我们需要创建一个可供访问的数据库。
随 Visual Studio 一起安装的数据库服务器因已安装的 Visual Studio 版本而异:
- 如果使用 Visual Studio 2010,则将创建 SQL Express 数据库。
- 如果使用 Visual Studio 2012,则将创建 LocalDB 数据库。
让我们继续生成数据库。
视图 -> 服务器资源管理器
右键单击 数据连接 -> 添加连接...
如果尚未从服务器资源管理器连接到数据库,则需要选择Microsoft SQL Server 作为数据源
根据您的安装情况,连接到 LocalDB 或 SQL Server Express,并输入 Products 作为数据库名称。
选择“确定”,系统会询问是否要创建新数据库,选择“是”
新数据库现在将显示在服务器资源管理器中,右键单击它并选择“ 新建查询”
将以下 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-> 数据源”
按别针图标,这样“数据源”窗口不会自动隐藏。 如果窗口已可见,可能需要点击刷新按钮。
在解决方案资源管理器中,双击 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 中。
单击鼠标右键并选择“已启用”,在导航工具栏上启用“保存”按钮。
通过双击按钮添加保存按钮的事件处理程序。 这将添加事件处理程序,并将你带到窗体的后台代码。 下一部分中将添加 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 窗体应用程序
编译并运行应用程序,可以测试功能。
保存存储生成的密钥后,密钥会显示在屏幕上。
如果使用 Code First,则还会看到已为你创建 WinFormswithEFSample.ProductContext 数据库。