WinForms 中的 Entity Framework






4.89/5 (138投票s)
一个使在 WinForms 项目中轻松使用 Entity Framework 的组件,包括设计时绑定支持。
- 下载示例 (EF4, C#) - 1.03 MB
- 下载示例 (EF4, C#, 将数据层与 UI 层分离) - 120 KB
- 下载示例 (EF4, VB, 由 'VBForever' 提供) - 946 KB
- 下载示例 (EF5, C#) - 1.3 MB
- 下载示例 (EF6 C#) = 1.3 MB
介绍
ADO.NET Entity Framework 是 Microsoft 最新的数据技术。它允许您创建易于编程的概念性数据模型。它还包含一个连接概念模型和实际数据存储的层,因此您可以轻松地在概念模型和后端数据库之间传输数据。
如果您习惯使用传统的 ADO.NET 数据类(DataSet
、DataAdapter
等),那么您可能会发现使用 ADO.NET Entity Framework 可以使许多事情变得更简单。
- 您无需处理派生自
DataTable
和DataRow
的类(例如CustomerDataTable
、CustomerDataRow
),而是直接处理实际的对象(例如Customer
)。结果是代码更加清晰直观。 - 没有数据适配器。数据会在您请求时自动检索,并且保存更改只需调用
SaveChanges
方法。 - Visual Studio 自动生成的数据访问层代码更容易维护、扩展和重用。
我们不会详细讨论 ADO.NET Entity Framework 的技术细节。这是一个深刻的主题,在许多优秀的文章和书籍中都有详细介绍。其中一些在本文档末尾的“参考文献”部分列出。
相反,我们将重点介绍如何在 WinForms 应用程序中使用 ADO.NET Entity Framework。虽然 Entity Framework 的所有强大功能都可以在 WinForms 应用程序中使用,但对数据绑定的支持很少。这很可惜,因为 WinForms 仍然是许多面向数据业务应用程序的首选平台,而数据绑定是 WinForms 开发的重要组成部分。
如果您使用过传统的 ADO.NET 数据类,您应该熟悉用于创建 DataSet
类以及在设计时使用它们的 DataSource
、DataMember
或 Binding
属性将控件绑定到这些类的 Visual Studio 工具。
坏消息是,这种丰富的运行时设计支持不适用于 Entity Framework 数据源。即使是简单的绑定场景也需要代码。少量代码足以实现非常基本的数据绑定,但要获得您可能习惯的全面绑定体验(例如,包括自动排序、过滤和分层绑定),则需要大量工作。
好消息是,这项支持可以相对轻松地添加,并且结果可以封装到一个可重用组件中,从而为 WinForms 应用程序启用丰富的绑定场景。这就是本文介绍的 EFWinForms 库的目标。该库包含两个组件:
EntityDataSource
:一个封装实体数据模型并将其元素公开为适合绑定的数据源的组件,并提供全面的设计时支持。EntityBindingNavigator
:一个控件,用于为视图提供导航,以及用于添加和删除记录以及保存或取消数据库更改的 UI。
EntityDataSource 组件
EntityDataSource
组件在传统的 ADO.NET 编程中扮演着 DataSet
和 BindingSource
的角色。
要使用它,您首先需要创建一个实体数据模型(这是任何 ADO.NET Entity Framework 项目的第一步,相当于在传统的 ADO.NET 编程中创建 DataSet
类)。
数据模型创建完成后,您可以将 EntityDataSource
组件拖到窗体上,并将其 ObjectContextType
属性设置为实体数据模型表示的 ObjectContext
类型(注意:如果您使用的是该项目的 EF6 版本,请使用 DbContextType
属性而不是 ObjectContextType
)。完成此操作后,EntityDataSource
组件将实例化一个对象上下文,并使用反射查找上下文中所有可用的数据源。然后,这些数据源将通过 IListSource
实现暴露给设计器。
之后,您可以将控件添加到窗体,并像往常一样使用它们的 DataSource
、DataMember
或 Binding
属性将它们绑定到 EntityDataSource
。单个 EntityDataSource
提供对模型中所有表和视图的访问,并且保存更改只需调用 SaveChanges
方法。
示例
理解 EntityDataSource
工作原理的最佳方法是查看一些示例。接下来的几节将介绍如何使用 EntityDataSource
实现四种典型的绑定场景。
所有场景都使用相同的实体数据模型,该模型基于传统的 NorthWind 数据库。
创建实体数据模型(通用步骤)
要使用 ADO.NET Entity Framework,您必须创建一个 ADO.NET Entity Data Model。这是包含概念数据模型以及加载和保存概念模型与数据库之间所需的基础结构层。
要创建实体数据模型,请右键单击 Visual Studio 中的项目资源管理器树,然后选择“Add | New Item…”选项。这将打开“Add New Item”对话框,如下面的图像所示。选择“ADO.NET Entity Data Model”,可选地为模型命名,然后单击窗体底部的“Add”按钮。
这将打开“Entity Data Model Wizard”对话框,如下面的图像所示。第一步允许您选择是想从现有数据库创建模型还是从空模型创建。选择第一个选项,然后单击“Next”。
下一步是选择将定义模型的数据库。您可以选择现有连接,或使用“New Connection”按钮创建新连接。在我们的示例中,我们将创建一个到 SQL Server 版本的 Northwind 数据库的连接。
数据库文件名为“NorthWnd.MDF”,并包含在示例中。
选择数据库后,向导将提示您选择要包含在实体数据模型中的表、视图和存储过程。在我们的示例中,我们将仅选择所有表,如下面的图像所示。
单击“Finish”按钮生成模型。这将在您的项目中添加两个项:“Model1.edmx”文件,其中包含 ADO.NET Entity Framework 用于指定模型的 XML 定义;以及一个关联的“Model1.Designer.cs”文件,其中包含生成的代码,包括用于访问数据的 ObjectContext
以及 Product
、Employee
等数据类。
打开 edmx 文件会显示 Entity Data Model Designer,它允许您检查和编辑模型。它还允许您随时重新生成模型,这在数据库架构发生更改或您改变主意要包含哪些表和视图时很重要。
“Model1.Designer.cs”文件中自动生成的数据类都声明为部分类。这允许您通过在单独的文件中添加自定义业务逻辑来扩展它们,而如果您决定从数据库重新生成模型,这些文件将不会被修改。
此时,您可以开始如下方式使用数据模型:
public Form1()
{
InitializeComponent();
using (var ctx = new NORTHWNDEntities())
{
dataGridView1.DataSource = ctx.Products.ToList();
}
}
该代码创建了一个 ObjectContext
来公开模型中的数据,构建一个包含所有产品的列表,并在网格中显示该列表。您可以编辑产品,如果我们没有释放上下文对象,就可以通过调用 ctx.SaveChanges()
将更改保存到数据库。
但是,如果您运行代码,您会注意到一些严重的限制:您无法对数据进行排序或过滤,您无法从列表中添加或删除项,当然,您也无法通过网格的列编辑器在设计时自定义网格列。
这些限制是由于用作数据源的列表只是数据的快照。因此,虽然列表中的对象是“活动的”,但列表本身不是。在这种情况下,WinForms 自动为您创建的 IBindingList
只提供最基本的功能。
创建网格视图(带 AutoLookup)
为了解决这些限制,请将 EntityDataSource
组件添加到窗体,并使用属性窗口将其 ObjectContextType
属性设置为“Sample.NORTHWNDEntities”,如下面的图像所示(注意:如果您使用的是该项目的 EF6 版本,请使用 DbContextType
属性而不是 ObjectContextType
)。
EntityDataSource
组件使用 ObjectContextType
值创建一个对象上下文,用于生成数据模型中所有元素的视图。
现在,将 DataGridView
控件添加到窗体,并使用属性窗口将 DataSource
属性设置为 EntityDataSource
组件,将 DataMember
属性设置为“Products”,如下所示。
此时,网格将自动创建列来公开 Product
类中的属性。您可以使用网格的列设计器重新排列列、设置它们的宽度、标题、对齐方式、格式等。
如果您现在运行项目,您将看到网格已自动填充,并且您可以执行所有预期的任务,包括编辑、排序以及添加或删除项。
这一切都有效,因为 EntityDataSource
组件已将产品列表透明地封装到 EntityBindingList
中,该类实现了 IBindingListView
接口并支持排序、过滤、添加和删除项。
保存更改
编辑数据后,您最终可能会希望将更改保存回数据库。这非常容易,感谢 ADO.NET Entity Framework。为了说明这一点,请向窗体添加三个按钮,将其 Text
属性设置为“Save”、“Cancel”和“Refresh”,并将以下处理程序附加到 Click
事件。
// save/cancel/refresh changes in the data source
void _btnSave_Click(object sender, EventArgs e)
{
entityDataSource1.SaveChanges();
}
void _btnCancel_Click(object sender, EventArgs e)
{
entityDataSource1.CancelChanges();
}
void _btnRefresh_Click(object sender, EventArgs e)
{
entityDataSource1.Refresh();
}
代码是不言自明的。第一个按钮将所有更改保存回数据库。第二个按钮通过重新获取数据并覆盖任何更改来取消更改,第三个按钮重新获取数据但保留任何更改。
但是,还有一项重要细节缺失:在将更改保存到数据库时,您必须准备好处理异常。典型情况是违反数据库约束的更改。不幸的是,对于这类异常没有通用的解决方案。它们的性质取决于数据库架构和应用程序本身。
无论您计划如何处理可能的异常,第一步都是捕获它们。为此,您可以将 try/catch
块添加到 SaveChanges
调用周围,或者您可以将处理程序附加到 EntityDataSource
组件的 DataError
事件。以下是我们的示例应用程序如何处理保存数据时可能出现的错误:
// report any errors
void entityDataSource1_DataError(object sender, DataErrorEventArgs e)
{
MessageBox.Show("Error Detected:\r\n" + e.Exception.Message);
entityDataSource1.CancelChanges();
e.Handled = true;
}
该代码发出警告,取消更改,并将 Handled
参数设置为 true
,表示错误已处理,不应抛出任何异常。
使用查找字典来表示相关实体
为了完成第一个示例,让我们探索一个常见场景。Product
类有两个属性 - Category
和 Supplier
- 分别代表相关实体。默认情况下,这些属性不包含在网格中,但您可以使用网格的列编辑器创建这些列。下图显示了如何做到这一点:
问题在于网格不知道如何表示相关实体,因此它仅使用 ToString
方法,结果是两个只包含“Sample.Category”和“Sample.Supplier”的只读列。
但您真正想要的是一个显示类别和供应商名称的列,最好是带有编辑器,允许您通过从列表中选择来编辑类别和供应商。这通常是通过编写代码来创建和绑定自定义列(如果您使用 DataGridView
控件,则为 DataGridViewComboBoxColumn
)来完成的。
由于这是一个非常常见的场景,EntityDataSource
组件提供了一个名为 AutoLookup
的扩展属性。此属性会自动添加到窗体上的任何 DataGridView
或 C1FlexGrid
控件(C1FlexGrid
是一个流行的网格控件,比 DataGridView
快得多,功能也更丰富)。
请注意,尽管 EntityDataSource
组件支持 C1FlexGrid
,但 EFWinForms 程序集不依赖于 C1FlexGrid 程序集。这是通过使用“dynamic
”关键字实现的,该关键字本质上依赖于反射在运行时绑定属性。可以使用相同的机制来扩展 EntityDataSource
组件以支持其他网格。
下图显示了如何在 DataGridView
上启用 AutoLookup
属性:
在任何网格上启用 AutoLookup
属性后,EntityDataSource
组件将自动扫描网格中的列,以用基于“数据映射”的可编辑列替换绑定到相关实体的任何常规列,该数据映射包含可能的相关实体列表以及每个实体的显示值。
下图显示了在我们的产品网格上将 AutoLookup
设置为 true 的效果:
注意“Category”和“Supplier”列现在如何显示类别和供应商名称,以及如何通过从列表中选择来为产品选择新的供应商。
此时,您可能想知道 EntityDataSource
是如何选择相关实体的哪个字段应该显示在网格上的。这是使用以下算法完成的:
- 如果类实现了
ToString
方法(而不是简单地继承它),则使用ToString
实现来表示实体。 - 否则,如果类包含类型为
string
且名称中包含“Name”的属性,则使用该属性来表示实体。 - 否则,如果类包含类型为
string
且名称中包含“Description”的属性,则使用该属性来表示实体。 - 如果以上任何一项都不适用,则无法为该类创建数据映射。
第一条规则是最通用和最灵活的。例如,Northwind 数据库中的 Employee
类具有 FirstName
和 LastName
属性。两者都可以用来在列表中表示实体,但理想情况下,您想同时使用两者。为此,我们只需覆盖 Employee
类中的 ToString
方法并构建我们想要使用的字符串表示:
/// <summary>
/// Add a field with the employee's full name
/// </summary>
public partial class Employee
{
public string FullName
{
get { return string.Format("{0} {1}", FirstName, LastName); }
}
public override string ToString()
{
return FullName;
}
}
注意这如何扩展 ADO.NET Entity Framework 向导创建的默认类。如果您决定重新生成实体数据模型,我们的新 FullName
属性和 ToString
实现将不受影响。
在 Northwind 数据库中,Employee
是唯一需要对数据映射进行特殊考虑的类。所有其他类都有诸如“CustomerName
”或“CategoryDescription
”之类的属性,这些属性会被 EntityDataSource
自动使用并产生期望的效果。
第一个示例到此结束。在接下来的所有示例中,我们将 AutoLookup
属性设置为 true
以用于所有网格。
创建主-详细视图
许多数据模型都有包含相关实体列表的实体。例如,类别包含产品列表,订单包含订单明细,依此类推。这种关系通常表示为两个网格:主网格显示“容器”实体,详细网格显示当前选定的主体的相关实体列表。
在 WinForms 中创建主-详细绑定非常容易。为了说明这一点,让我们向窗体(在一个新的选项卡页中)添加两个网格。顶部的网格将显示类别,底部的网格将显示当前选定类别中的产品。
要创建绑定,请选择顶部的网格,将 DataSource
属性设置为 EntityDataSource
组件,将 DataMember
属性设置为“Categories”,如下所示。
接下来,选择底部的网格,再次将 DataSource
属性设置为 EntityDataSource
组件。这次,将 DataMember
属性设置为“Categories.Products”,如下所示。
请注意,详细网格的 DataMember
属性设置为“Categories.Products”,而不是直接设置为“Products”。这会导致详细网格与主网格上的当前选择自动同步。
与以前一样,您可以使用网格的列编辑器来自定义列,并可以使用 AutoLookup
扩展属性来显示相关实体(例如,每个产品的 Supplier
)。
如果再次运行项目,您将看到我们的主-详细页面按预期工作。当您浏览顶部网格中的类别时,您将在底部网格中看到相应的 [产品]。
创建窗体视图(带 EntityBindingNavigator)
网格非常有用,因为它们允许您编辑、组织和导航数据。但在许多情况下,自定义的窗体样式布局可以提供更好的用户体验。在这些情况下,您需要为用户提供一种导航数据的方法。
WinForms 提供了一个 BindingNavigator
控件来处理这个问题。BindingNavigator
与 BindingSource
组件配合使用,提供记录数、导航到第一条、上一条、下一条和最后一条记录的按钮,以及添加和删除记录的按钮。
EFWinForms 程序集包含一个 EntityBindingNavigator
控件,它提供类似的功能,但它针对 EntityDataSource
组件。除了导航功能外,EntityBindingNavigator
控件还包含用于保存或取消更改的按钮。
要创建窗体视图,请首先将 EntityBindingNavigator
控件添加到页面,并将其 DataSource
属性设置为窗体上已有的 EntityDataSource
,将 DataMember
属性设置为“Orders”。这将允许用户选择当前订单、添加或删除订单、将更改保存回数据库或取消更改。
接下来,让我们向窗体添加一些绑定的文本框。首先添加一个“Order Date”标签和一个旁边的文本框。选择文本框,然后在属性窗口的(DataBindings)/(Advanced)节点旁边的省略号按钮。
这将打开“Formatting and Advanced Binding”对话框。在“Binding”下拉列表中选择“Orders.OrderDate”节点,如下图所示。
选择绑定后,使用相同的对话框选择要用于显示订单日期的格式。
重复此过程以添加另外三个绑定的文本框:
- “Customer”,绑定到“Orders.Customer.CompanyName”,
- “Employee”,绑定到“Orders.Employee.FullName”,以及
- “Amount”,绑定到“Orders.Amount”。
请注意,Order
类尚未具有 Amount
属性。我们将通过像之前扩展 Employee
类一样来扩展 Order
类来创建此属性。这是实现:
/// <summary>
/// Add a field with total order amount.
/// </summary>
public partial class Order
{
// calculate amount for this order
public decimal Amount
{
get
{
var q = from od in this.Order_Details select od.Amount;
return q.Sum();
}
}
}
/// <summary>
/// Add a field with order detail amount.
/// </summary>
public partial class Order_Detail
{
// calculate amount for this order detail
public decimal Amount
{
get { return Quantity * UnitPrice * (1 - (decimal)Discount); }
}
}
该代码使用 LINQ 计算每个 Order_Detail
和每个 Order
的总金额。
最后,向窗体添加一个网格,并使用属性窗口将其 DataSource
属性设置为 EntityDataSource
组件,将 DataMember
属性设置为“Orders.Order_Details”。这是分层绑定的另一个示例。当用户使用 EntityBindingNavigator
控件导航到订单时,网格将自动显示当前订单的详细信息。
使用网格的列编辑器添加一个绑定到订单明细的 Product
属性的列,并确保将网格的 AutoLookup
属性设置为 true
。
如果现在运行项目,您将看到一个类似于下图的屏幕:
这个单一视图演示了 EFWinForms 库的大部分功能:EntityDataSource
和 EntityBindingNavigator
组件、EntityDataSource
的 AutoLookup
功能、简单和分层绑定。所有这些都是在不编写任何代码的情况下完成的(除了数据扩展,它们实际上不是 UI 的一部分)。
创建图表视图(带筛选)
既然我们已经介绍了网格和文本框,下一个示例将展示如何使用绑定创建基于动态筛选器的图表。
我们的图表将显示产品单价。为避免显示过多数据,我们将允许用户指定图表中应包含的最低单价。
此示例的第一步是创建一个新的 EntityDataSource
组件。这是必要的,因为默认情况下,每个 EntityDataSource
只公开表的单个视图。如果我们对样本中一直使用的 EntityDataSource
上的产品表应用筛选器,那么筛选器也将应用于所有其他示例。在某些情况下,这可能是可取的,但在此处不是。我们希望筛选器仅应用于图表。
添加新的 EntityDataSource
到窗体后,请记住将其 ObjectContextType
属性设置为“Sample.NORTHWNDEntities”,就像我们之前所做的那样。
为了在设计时更轻松地将数据绑定到图表,我们还将向窗体添加一个常规的 BindingSource
组件,并将其 DataSource
属性设置为我们刚刚添加到窗体的新的 EntityDataSource
组件,将其 DataMember
属性设置为“Products”。BindingSource
组件还通过属性窗口更轻松地应用筛选器。
还可以通过设置 Name
属性为“chartBindingSource”,Filter
属性为“(Not Discontinued) And UnitPrice > 30”,Sort
属性为“UnitPrice”来初始化 BindingSource
。
添加新的 EntityDataSource
和 BindingSource
组件到窗体后,让我们添加新的 UI,它包含一个文本框(用户将在此输入要绘制的最低单价)、一个图表控件和一个网格以显示正在绘制的数据。
要绑定图表,请首先将其 DataSource
属性设置为我们刚刚添加到窗体的 BindingSource
组件。然后选择图表,并使用省略号按钮打开 Series
属性的编辑器。
这将打开系列编辑器,我们将使用它来设置系列 XValueMember
属性为“ProductName”,YValueMember
为“UnitPrice”。
接下来,通过将网格的 DataSource
属性设置为新的 BindingSource
来绑定网格,就像我们对图表所做的那样。请注意,在这种情况下,我们不必设置 DataMember
属性,因为绑定源已经专门用于产品表。使用网格的列编辑器来自定义数据的显示方式,并设置 AutoLookup
属性为 true,以防您想在网格上显示相关实体(例如,产品供应商)。
为了启用筛选,我们将向两个文本框事件添加处理程序:
// update filter when the user edits the text box
void _txtMinPrice_Validated(object sender, EventArgs e)
{
ApplyFilter();
}
void _txtMinPrice_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == 13)
{
ApplyFilter();
e.Handled = true;
}
}
这是实际应用筛选的方法:
// apply the filter
void ApplyFilter()
{
// never show discontinued products
var filter = "(Not Discontinued)";
// apply minimum price condition
var minPrice = _txtMinPrice.Text.Trim();
if (!string.IsNullOrEmpty(minPrice))
{
double d;
if (!double.TryParse(minPrice, out d))
{
MessageBox.Show("Invalid Minimum Unit Price, please try again.");
}
else
{
filter += string.Format(" and (UnitPrice >= {0})", minPrice);
}
}
// set the filter
chartBindingSource.Filter = filter;
}
该方法构建一个字符串表达式,该表达式首先指定我们不想显示任何已停产的产品,然后添加价格必须大于或等于用户指定的最低价格的条件。最后,该方法将生成的表达式应用于 BindingSource
组件的 Filter
属性。
在我们的图表准备好之前,必须添加另一个事件处理程序:
// update chart when list changes
void chartBindingSource_ListChanged(object sender, ListChangedEventArgs e)
{
chart1.DataBind();
}
与侦听数据源更改的网格不同,必须在数据更改时显式更新图表。这可能是优化功能或错误。无论哪种方式,调用 DataBind
方法都会更新图表。
您现在可以再次运行项目,查看更改最低单价值对图表的影响:默认情况下,筛选器设置为显示 UnitPrice
大于或等于 30 美元的产品。
如果您将最低单价提高到 50 美元并按 Enter 键,图表将立即显示更改,并且只剩下五种产品。
请注意,网格也会被筛选,并且如果您对网格数据进行排序,图表将更改以反映新的排序顺序。
创建自定义视图(带 LINQ)
Entity Framework 的一个伟大之处在于它与 LINQ 配合得非常好,LINQ 在创建自定义数据视图方面提供了极大的灵活性和生产力。
您可以将任何 LINQ 查询的结果转换为列表并将其用作绑定源,但正如我们之前讨论过的,这有一些限制。例如,以这种方式创建的视图无法排序或筛选。
EntityDataSource
类也可以在此情况下提供帮助。它有一个 CreateView
方法,该方法接受任何 IEnumerable
并将其转换为支持排序和筛选的 IBindingListView
。
为了说明此功能,让我们创建一个带有 DataGridView
控件的附加选项卡。与以前使用数据模型中已定义的视图的示例不同,此示例将显示基于 LINQ 查询创建的视图。这是代码:
void ShowLinq()
{
// some LINQ query
var q = from Order o in entityDataSource1.EntitySets["Orders"]
select new
{
OrderID = o.OrderID,
ShipName = o.ShipName,
ShipAddress = o.ShipAddress,
ShipCity = o.ShipCity,
ShipCountry = o.ShipCountry,
Customer = o.Customer.CompanyName,
Address = o.Customer.Address,
City = o.Customer.City,
Country = o.Customer.Country,
SalesPerson = o.Employee.FirstName + " " + o.Employee.LastName,
OrderDate = o.OrderDate,
RequiredDate = o.RequiredDate,
ShippedDate = o.ShippedDate,
Amount =
(
from Order_Detail od in o.Order_Details
select (double)od.UnitPrice * od.Quantity * (1 - od.Discount)
).Sum()
};
// create BindingList (sortable/filterable)
var bindingList = entityDataSource1.CreateView(q);
// assign BindingList to grid
_gridLINQ.DataSource = bindingList;
}
大部分代码是一个 LINQ 查询,它组合了来自订单、客户、员工和订单明细的数据,以生成一个类似于数据库中“Invoices”查询的视图。该查询包含一个嵌套查询,该查询为每个订单添加订单明细的总金额。
查询结果显示在网格上,该网格可以像往常一样进行排序或筛选。
使用数据层分离关注点
当本文首次发布时,许多读者立即指出示例应用程序同时包含数据访问层和 UI,这是一种不良设计。我同意,在大多数应用程序中,这种分离是件好事。将数据层与 UI 分离有许多优点,例如:
- 改进组织、清晰度和更轻松的维护。
- 数据层可以在多个应用程序中重用。
- 限制对数据库的访问可提高数据库的完整性和安全性。
考虑到这一点,Visual Studio 中诱人的“Add Data Source”菜单似乎不太有吸引力。它们允许您将数据层直接添加到应用程序中,并将其绑定到 UI 元素,而无需任何分离,这对于快速创建应用程序非常有用,但通常会导致维护上的麻烦。
本节展示了如何使用 EntityDataSource
组件创建可重用的数据层,这些数据层封装了所有数据访问并为实现为单独程序集的 UI 层提供数据。结果是一个更清晰、更安全、更易于维护的体系结构。
下面描述的解决方案包含在 SeparateConcerns.zip 文件中。该解决方案包含三个项目:
- EFWinForms:这是原始应用程序中包含的同一个类。它定义了数据层使用的
EntityDataSource
类。 - DataLayer:这是数据访问层。
DataSource
类包含与数据库访问相关的所有代码,包括实体数据模型和连接字符串。 - SampleApplication:这是 UI 层。它实现了一个主-详细窗体,该窗体使用
DataLayer
类进行所有数据访问。数据绑定工作方式与原始示例完全相同,但此项目对数据库一无所知(没有连接字符串、数据模型等)。
请注意,这仍然是一个纯粹的 WinForms 应用程序。它不使用 WCF、Web 服务等。它只是原始主题的一个小变体,表明您可以轻松地将数据访问层与 UI 分离,如果这适合您的应用程序,并且仍然可以享受 Entity Framework、WinForms 和数据绑定的好处。
实现数据层
数据层项目实现了 DataSource
类,该类实现如下:
public partial class DataSource : Component, IListSource
{
// ** fields
EFWinforms.EntityDataSource _ds;
const string DATABASEFILE = @"c:\util\database\northwnd.mdf";
const string CONNECTIONSTRING =
@"metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res:" +
@"//*/Model1.msl;provider=System.Data.SqlClient;" +
@"provider connection string=""Data Source=.\SQLEXPRESS;" +
@"AttachDbFilename=" + DATABASEFILE + ";" +
@"Integrated Security=True;Connect Timeout=30;" +
@"User Instance=True;MultipleActiveResultSets=True""";
// ** ctors
public DataSource()
{
InitializeComponent();
// check that the database file exists
if (!System.IO.File.Exists(DATABASEFILE))
{
throw new Exception("Database file not found. " +
"This sample requires the NorthWind database at " + DATABASEFILE);
}
// create entity data source
_ds = new EFWinforms.EntityDataSource();
// create object context (specifying the connection string)
_ds.ObjectContext = new northwndEntities(CONNECTIONSTRING);
}
public DataSource(IContainer container) : this()
{
container.Add(this);
}
// ** object model
public void SaveChanges()
{
// optionally perform any logic/validation before saving ...
// save the changes if everything is OK
_ds.SaveChanges();
}
// ** IListSource
bool IListSource.ContainsListCollection
{
get { return true; }
}
System.Collections.IList IListSource.GetList()
{
return ((IListSource)_ds).GetList();
}
}
DataSource
类继承自 Component
,这意味着它可以添加到窗体中。它实现了 IListSource
接口,这意味着它可以作为富绑定源使用。
DataSource
构造函数实例化一个 EntityDataSource
,并使用 ObjectContext
属性将其绑定到实体数据模型。请注意,在这种情况下,连接字符串指定了数据库的绝对位置。该类检查数据库文件是否已正确安装,如果找不到则抛出异常。运行项目之前,应将“northwnd.mdf”文件复制到“c:\util\database”文件夹。这是原始示例中使用的相同数据库文件。(提示:外观难看的连接字符串常量是从 App.Config 文件复制的。)
DataSource
的对象模型由单个 SaveChanges
方法组成,该方法通过调用底层 EntityDataSource
组件来实现。如果要让客户端能够保存对数据库的更改(请记住,客户端现在除了数据层提供的之外,不再能访问数据库),则必须这样做。该方法实现可用于在将更改提交到数据库之前执行任何必要的验证。
最后,IListSource
接口通过将所有调用委托给底层 EntityDataSource
组件来实现。
这就是整个数据层。在实际应用程序中,您可以通过使用实体数据模型进行自定义,方法是使用设计器或编写自定义业务逻辑,就像我们在原始应用程序中所做的那样。
实现 UI 层
UI 层是一个单独的 WinForms 应用程序,它由下面的单个窗体组成:
窗体包含两个网格和一个 DataSource
组件。DataSource
组件是由数据层项目实现的,如上所述。
网格通过以下属性绑定到数据源(在设计时按通常方式设置):
// top grid: categories
dataGridView1.DataSource = dataSource1;
dataGridView1.DataMember = "Categories";
// bottom grid: products for the currently selected category
dataGridView2.DataSource = dataSource1;
dataGridView2.DataMember = "Categories.Products";
请注意,数据绑定机制的工作方式与原始示例完全相同。但这次 UI 对数据库或连接字符串一无所知。数据层程序集可以在其他项目中重用,并与 UI 项目分开维护。
限制
我认为 EFWinForms 库是一个有趣的项目,它使在 WinForms 中使用 ADO.NET Entity Framework 变得更加容易(我希望在阅读完本文后您也同意)。
但是,在当前状态下,它有一些限制,您在使用它进行项目之前应该考虑一下:
- 内存要求:
EntityDataSource
类不包含任何类型的服务器端筛选,或任何形式的智能缓存或内存管理。如果您有一个包含一百万条记录的表,它将把所有记录加载到内存中,并且在组件被释放之前不会丢弃它们。 - 上下文范围:每个
EntityDataSource
实例封装一个ObjectContext
及其包含的所有数据。您不能轻松地在整个应用程序中共享一个上下文。例如,如果您的应用程序使用多个窗体,那么每个窗体通常会有一个EntityDataSource
和它自己的数据副本。一个上下文中应用于对象的更改不会被其他上下文可见,除非数据已保存到数据库并且上下文已刷新。 - LINQ 支持有限:正如最后一个示例所示,您可以使用
EntityDataSource
配合任意 LINQ 语句。但这些视图有些有限,例如,它们不允许您添加或删除记录。
如果您对这些限制中的任何一项感到担忧,有一些选择。您可以选择改进该库(包含完整的源代码,任何更正或改进都将不胜感激)。
另一个选择是使用一些商业产品,它们的功能比 EFWinForms 强大得多,并包含智能数据缓存、虚拟数据源和多平台支持等功能。下面的参考文献部分包含指向其中一些产品的链接。(其中一个产品,ComponentOne Studio for Entity Framework,由我工作的公司生产,并与本文内容密切相关。)
Entity Framework 5
本文是使用 Microsoft Entity Framework 的 4.x 版本编写的。从那时起,Microsoft 发布了 5 版本,它默认随 Visual Studio 2012 一起安装,但也可以与 Visual Studio 2010 一起使用。
Entity Framework 5 版本在添加和更改方面有一些新增功能。从本文的角度来看,最明显的区别是 EF4 创建的模型有一个 ObjectContext
对象来表示数据库,以及 ObjectSet<T>
集合来表示数据库表。相比之下,EF5 模型有一个 DbContext
对象来表示数据库,以及 DBSet<T>
集合来表示数据库表。
EFWinForms 库的原始版本基于 ObjectContext
和 ObjectSet
类,因此无法与新的 EF5 模型一起使用。
幸运的是,旧类和新类之间的映射很简单。我只花了我大约 30 分钟的时间,用 EF5 创建了一个新版本的
注意:我在使用上下文的 Refresh
方法时遇到了一些问题。起初,EF5 似乎完全忽略了 RefreshMode
参数的值。结果(经过一些额外的研究和一些读者的帮助)发现,当您调用 Refresh
方法时,EF5 不会自动检测更改。可以通过在调用 Refresh
之前调用 DbContext.ChangeTracker.DetectChanges
方法来解决此问题。下面的代码显示了新的调用:
///
/// Refreshes this set's view by re-loading from the database.
///
public void RefreshView()
{
if (_list != null && Query != null)
{
// call this before Refresh (required in EF5 but not in EF4!)
_ds.DbContext.ChangeTracker.DetectChanges();
// refresh and make sure client wins
var ctx = ((System.Data.Entity.Infrastructure.IObjectContextAdapter) _ds.DbContext).ObjectContext;
ctx.Refresh(RefreshMode.ClientWins, Query);
// show changes
_list.Refresh();
}
}
我已经更新了代码来解决这个问题,现在 CancelChanges
和 RefreshView
方法似乎都像在 EF4 版本代码中一样工作。
Entity Framework 6
几位读者问我关于 Entity Framework 6 的问题,Microsoft 于 2013 年 10 月发布了 EF6。EF6 比之前的版本有一些显著的改进。您可以在此处找到有关新功能的详细信息:
http://blogs.msdn.com/b/adonet/archive/2013/10/17/ef6-rtm-available.aspx
将项目从 EF5 升级到 EF6 非常简单直接。基本上,它需要使用 nuget 在项目中安装 EF6(它将自动替换 EF5),使用 EF6 设计时工具重新生成数据模型,并调整一些命名空间。您可以在此处找到一个不错的详细指南:
http://msdn.microsoft.com/en-US/data/upgradeEF6
我按照此指南中的步骤升级了本文中包含的示例,并在几分钟内使其正常工作。我甚至不必更改代码,除了命名空间的调整,这是一个惊喜。我将该项目的 EF6 版本添加到项目附带的下载文件中。希望您喜欢。
参考文献
以下链接指向有关 ADO.NET Entity Framework 的文章或书籍:
- Entity Framework简介
- Entity Framework 4 在 WinForms 开发中的技巧
- Entity Framework的简单示例
- 性能与 Entity Framework
- Introducing ADO.NET Entity Framework
- Use an Entity Framework Entity as a WinForms Data Source
- Programming Entity Framework
以下链接指向一些支持或扩展 ADO.NET Entity Framework 的商业产品。该列表并不详尽,也不构成推荐或认可。它仅作为您自己研究的起点,并说明了围绕 ADO.NET Entity Framework 快速增长的丰富生态系统:
- ComponentOne Studio for Entity Framework
- CodeFluent Entities
- CodeSmith PLinqO for Entity Framework
- DevArt Entity Developer
- IdeaBlade DevForce
- LLBLGen Pro
历史
- 2011 年 7 月 8 日:更新了示例。原始示例包含对第三方程序集的引用。该程序集未使用,可以安全删除,但它可能会阻止某些人运行项目。附件中更新的示例版本删除了该引用。
- 2011 年 7 月 10 日:更新了示例。其中一个选项卡中的 EntityNavigator 隐藏了所有按钮,此版本已修复。
- 2011 年 7 月 17 日:更新了代码以修复与分层绑定相关的几个小问题。
- 2011 年 7 月 18 日:修复了与设计时使用和设置
ObjectContext
属性相关的几个小问题。 - 2011 年 7 月 19 日:添加了一个部分,描述了如何将数据层与 UI 层分离,并添加了一个新示例来演示这一点。
- 2013 年 7 月 25 日:添加了一个部分,描述了如何将该库与 Entity Framework 5 结合使用,它随 Visual Studio 2012 一起提供。
- 2014 年 2 月 5 日:添加了一个部分,描述了如何将该库与 Entity Framework 6 结合使用,它包含在 Visual Studio 2012 和 2013 中。