使用 ObjectContainerDatasource 的 MVP 设计中的 ASP.NET 数据绑定





4.00/5 (1投票)
如何利用 Web Client Software Factory 中的 ObjectContainerDatasource 在保持 MVP 架构的同时执行数据绑定。提供了 Entity Framework、LINQ to SQL 和 ADO.NET 的示例。
引言
作为开发者,我猜想我们大多数人都追求这两个目标:
- 编写更好的代码。
- 编写更少的代码。
有时,这两个目标是相辅相成的;例如,重构代码可以使其更易于维护(=更好)和可重用(=更少代码)。在其他时候,它们又会相互冲突:应用成熟的面向对象原则可以使我们的代码更健壮、更易于测试,但通常会增加其复杂性以及我们必须编写的代码量。同样,ASP.NET 提供了许多自动化数据绑定任务的工具,但这些工具通常与更复杂的架构不太兼容。幸运的是,微软为我们提供了 Web Client Software Factory,其中包含用于构建企业应用程序的有用控件和方法,可以简化许多常见任务。在本文中,我将探讨如何使用 WCSF 附带的 ObjectContainerDataSource
在使用模型视图表示器 (MVP) 架构构建的应用程序的上下文中处理数据绑定。
背景
我无意详细解释 MVP 模式。CodeProject 上已经有 其他 文章 涵盖了该模式及其在 ASP.NET 中的用法。但是,也许值得一提的是,这与最近发布的 ASP.NET MVC 框架不同,尽管这两个模式有一些相似之处。虽然现在有了新的 MVC 框架,但手动实现 MVP 解决方案可能看起来不值得投入时间,但这样做可以让我们利用大量现有的 Web Forms 技能和控件,而这些是我们迁移到 MVC 时不得不放弃的。
问题
ASP.NET 为我们提供了各种各样的数据绑定控件,可以大大加快开发过程。然而,这些控件与数据源模型紧密相关;虽然手动管理数据绑定过程当然是可能的,但远不如方便。不幸的是,开箱即用的数据源控件中没有一个真正适合 MVP 等架构;唯一接近的是 ObjectDataSource
,但最终它也不太合适。虽然我们可以将其连接到使用页面的表示器对象,但这需要额外的努力,并且会使视图比我们期望的更活跃;最终,我们希望视图被动地接受来自表示器的数据,而不是主动地索取。
解决方案
Web Client Software Factory 提供的 ObjectContainerDataSource
充当数据的被动管道,公开一个 DataSource
属性,我们可以从表示器填充该属性。当数据被更新、删除和插入时,它会引发事件,我们可以处理这些事件将对象送回表示器进行处理。因为它充当标准数据源控件,所以我们可以将其与 FormView
、GridView
等数据绑定控件一起使用,并利用这些强大控件及其提供的简化 API。
Using the Code
示例项目包含一个简单的应用程序来显示员工详细信息。它并非旨在成为实现 MVP 模式的参考应用程序,而是为了演示 OCDS 如何简化数据绑定,因此应被视为一种相当朴素的实现。ASPX 页面充当视图,并包含一个配置为执行更新、删除和插入的 ListView
。其中包含三个简单的数据存储库:Entity Framework、LINQ to SQL 和手动 ADO.NET 实现,可以通过 web.config 自由切换。为了实现这一点,定义了一个员工实体接口,三个数据存储库中的具体类都实现了该接口。
public interface IEmployee
{
int EmployeeID { get; set; }
string FirstName { get; set; }
string LastName { get; set; }
decimal? Salary { get; set; }
int? DepartmentID { get; set; }
bool Active { get; set; }
DateTime? DateOfBirth {get; set; }
IDepartment Department { get; }
}
同样,为员工所属的部门定义了一个接口。
public interface IDepartment
{
int DepartmentID { get; }
string Name { get; }
}
EF 和 LINQ to SQL 存储库通过部分类实现这些,用于自动生成的设计器文件。
视图实现了以下接口。我选择通过事件让视图与表示器通信,但其他实现也是可能的。
public interface IEmployeeListView
{
IList<IEmployee> Employees { set; }
IList<IDepartment> Departments { set; }
event EventHandler ItemUpdated;
event EventHandler ViewLoaded;
event EventHandler ItemInserting;
event EventHandler ItemInserted;
event EventHandler ItemDeleted;
event EventHandler ItemActioning;
IEmployee SelectedEmployee { get; }
IEmployee NewEmployee { get; set; }
}
对于视图实现,需要对 WCSF 中的 Microsoft.Practices.Web.UI.WebControls
进行引用,并且我们需要在页面中引用它以使用 OCDS。
<%@ Register Assembly="Microsoft.Practices.Web.UI.WebControls"
Namespace="Microsoft.Practices.Web.UI.WebControls"
TagPrefix="pp" %>
然后将控件放在页面上。
<pp:ObjectContainerDataSource runat="server"
ID="dsEmployees" DataObjectTypeName="System.Object" />
<pp:ObjectContainerDataSource runat="server"
ID="dsDepartments" DataObjectTypeName="System.Object" />
你可能想知道 DataObjectTypeName="System.Object"
是做什么用的。与 ObjectDataSource
的工作方式类似,在执行数据操作时,OCDS 会创建一个指定类型的实例,并将其绑定到控件的数据应用到其属性。该控件要求我们为 DataObjectTypeName
属性指定一个值,OCDS 会进行检查以确保它可以创建指定类型的实例。在我们的例子中,由于我们使用接口来处理数据,并且无法创建接口的具体实例,因此我们无法直接指定要使用的类型。然而,正如我们稍后将看到的,我们根本不会使用 DataSource
的类型创建功能。就本示例而言,我们可以将任何有效类型放入 DataObjectTypeName
,这并不重要。
数据通过页面上的属性推送到数据源。
public IList<IEmployee> Employees
{
set
{
dsEmployees.DataSource = value;
}
}
public IList<IDepartment> Departments
{
set
{
dsDepartments.DataSource = value;
}
}
这些在每次页面加载时由表示器填充。为了减少数据库活动,我在示例应用程序中实现了一个极其简单的缓存机制,利用了 ASP.NET 缓存。数据绑定控件像使用任何其他数据源一样使用数据源。
<asp:ListView runat="server" ID="lvEmployees" DataSourceID="dsEmployees"
DataKeyNames="EmployeeID" InsertItemPosition="LastItem">
<asp:DropDownList runat="server" ID="ddDepartments"
SelectedValue='<%#Bind("DepartmentID") %>'
DataValueField="DepartmentID"
DataTextField="Name" DataSourceID="dsDepartments"
AppendDataBoundItems="true">
OCDS 公开了许多我们可以处理的事件。对于每个数据操作,都有 –ing 和 –ed 事件。–ed 事件是数据源尝试创建 DataObjectTypeName
属性中指定的类型时发生的事件,如果我们让它们发生,将会收到错误。我们将处理 –ing 事件,然后告知数据源取消,以确保这些事件永远不会被触发。这些事件都以类似的方式工作,我将在此处通过更新事件来概述该过程。
更新事件是发生魔力的地方。
void dsEmployees_Updating(object sender, ObjectContainerDataSourceUpdatingEventArgs e)
{
var source = sender as ObjectContainerDataSourceView;
SelectedEmployee = (IEmployee)source.Items[lvEmployees.EditIndex];
OnItemActioning(EventArgs.Empty);
Dictionary<string, object> changedValues = new Dictionary<string, object>();
foreach (DictionaryEntry entry in e.OldValues)
{
if(!object.Equals(e.OldValues[entry.Key], e.NewValues[entry.Key]))
changedValues.Add(entry.Key.ToString(), (e.NewValues[entry.Key]));
}
if (changedValues.Count > 0)
Microsoft.Practices.Web.UI.WebControls.Utility.
TypeDescriptionHelper.BuildInstance(changedValues, SelectedEmployee);
OnItemUpdated(EventArgs.Empty);
e.Cancel = true;
}
通过将源强制转换为 ObjectContainerDataSourceView
,我们可以直接访问放入数据源的原始数据,并使用该数据而不是数据源创建的部分重构对象。视图的 SelectedEmployee
属性被设置,然后引发 ItemActioning
事件。此时,Entity Framework 和 LINQ to SQL 存储库将项重新附加到其数据上下文,以便可以开始更改跟踪。ADO.NET 存储库将忽略它。
ObjectContainerDataSourceUpdatingEventArgs
变量公开了三个值字典:Keys
是放入数据绑定控件的 DataKeys
属性的值,OldValues
是更新前窗体包含的值,NewValues
是更新后的值。在此示例中,我忽略了 Keys
;由于我们使用的是原始对象,因此默认情况下它已完全填充。然而,必须在控件的 DataKeys
属性中指定某些内容,否则将会引发异常。然后构建一个已更改值的字典,并使用 BuildInstance
将更改应用于对象。BuildInstance
是一个有用的函数,它接受一个属性名称和值字典,并将它们应用于现有对象。接下来,会引发 ItemUpdated
事件,告知表示器我们已完成对象的操作,表示器可以继续处理它。然后将 Cancel
属性设置为 true
,以告知 OCDS 不要触发更新事件。
插入的工作方式类似,只是表示器提供了一个空的 IEmployee
对象来填充,并且 EventArgs
对象中没有旧值或键。删除只是选择要删除的对象并告诉表示器进行删除。
这样工作极大地简化了与 Entity Framework 和 LINQ to SQL 等更改跟踪数据框架的协作。我们不必创建具有原始值和更新值的对象,而是可以以更自然的方式处理单个对象。如果我几个月前就知道这一点,它就能为我在使用 EF 和 ObjectDataSource
时遇到的许多问题节省时间。此外,视图代码可以相当容易地重构到一个基类中,进一步提高了代码的可重用性。
结束语
通过利用 ObjectContainerDataSource
控件,我们可以在不牺牲应用程序架构太多特性的情况下,享受 ASP.NET 数据绑定模型的优势。然而,认识到我们所做的事情并非没有影响是很重要的。尽管我们没有编写执行此操作的代码,但我们将大量逻辑和责任置于视图中,而这些逻辑和责任不易测试。在更严格的 MVP 实现中,我们不会允许视图自主地进入编辑模式。因此,这种实现不可避免地从 MVP 的被动视图模型转向了监督控制器领域。就我个人而言,我对此决定感到满意;与许多事情一样,我认为这是在保持架构纯粹性和实际交付产品的需求之间取得的折衷,但一如既往,应权衡利弊。
历史
- 2009 年 10 月 12 日 - 初次上传。