使用 nHydrate 绑定网页






4.71/5 (5投票s)
通用地将 UI 控件绑定到生成的对象。
引言
将网页连接到后端数据库可能很繁琐。您需要使用某种数据访问层 (DAL),然后将其映射到页面上的字段。这是重复且容易出错的代码。有许多 ORM 工具允许您与数据库进行交互,但这无助于加载页面上的控件、提示和验证器。本文将探讨使用 nHydrate
作为 DAL,它提供了大量的元数据,从而摆脱了手动绑定网页的某些繁琐工作。
什么是 nHydrate?
首先简要解释一下 nHydrate
,这是一个允许您以模型驱动的方式(或领域驱动设计,DDD)开发软件的平台。您设计一个包含实体和关系的 模型。有生成器读取模型并根据该模型创建代码。当您需要更改应用程序设计时,只需更改模型并重新生成。数据库安装程序控制数据库更改和版本控制。您永远不会触碰数据库。数据库不是模型。您有一个真正的模型。它确实可以减少我们在执行重复任务时所有人都会犯的错误。
Entity Framework 生成的 DAL 提供了元数据,可用于自动化我们在网页上执行的许多任务。我们将看到如何以编译时检查的方式创建提示和验证器,以及将数据绑定到控件。使用编译器是一个很大的优势。我总是尝试尽可能多地实现功能。如果您通过脚本或其他机制在标记中添加代码,那么在更改数据模型或执行许多其他活动时都会出现问题。代码为王;编译器会检查它!
此示例依赖于我围绕模型编写的一些代码。nHydrate
模型用于创建与数据库交互的 DAL。生成的 Entity Framework 层在其周围有附加的扩展、属性和元数据,以方便快速编写应用程序。我的“ORMMap
”类执行了我使网页正常工作所需的所有通用功能。此类不属于生成的框架,因为它非常特定于我的实现。您选择绑定或映射用户界面的方式是特定的,取决于您的技术,例如 Web Forms、MVC、WinForms 等。您需要编写一些连接代码,就像我所做的一样。但是,DAL 周围的元数据使这一切变得容易。
此示例的作用
- 根据模型元数据动态地在页面上插入验证器
- 必填验证器
- 范围验证器
- 正则表达式验证器
- 验证文本框中的数据类型,如整数
- 根据元数据设置文本框的最大长度
- 从数据库加载下拉列表
- 加载分页网格
- 处理数据分页
- 将各种 UI 控件绑定到 Entity Framework 对象
- 验证保存并显示错误
- 消除 ViewState
在此示例中,我可以绑定标签、文本框、复选框、单选按钮、列表和下拉列表。我还可以填充下拉列表、复选列表和单选列表。我的主要窗体上的代码非常少,屏幕仍然具有加载/保存功能和验证。
我编写的连接代码只有几个方法。第一个允许您加载组合框。在页面初始化中,我添加了列表加载代码。这有一个额外的好处,就是我不需要 viewstate
。事实上,示例中的所有页面都关闭了 viewstate
。我们在此处绑定方式根本不需要 viewstate
。让我们先来看一个“Region
”对象的添加/编辑页面。我们的模型有 3 个实体:Customer
、Country
和 Region
。Customer
具有 [0..1] Countries
和 [0..1] Regions
。这是一个非常简单的模型,用于演示如何绑定带有关联的对象。
我的项目有两个基页,我从这两个基页派生所有页面。第一个是 BasePage
,它什么也不做。它仅仅是一个基类。第二个是 BasePersistablePage
,它用于编辑页面,并具有加载和保存的核心功能。它有三个主要方法:SetupBindings
、CreateObject
和 SaveData
。编辑页面重写这些方法以添加所需的任何自定义代码来绑定。下面是我主要站点对象的类图。您可以看到一个编辑页面和一个列表页面及其基类。 “ORMMap
”对象被展开以显示其方法。在此类中,没有处理特定对象的代码(列表 Region
或 Customer
)。这个类可以从任何页面使用,来处理任何生成的对象。

请记住,这些页面不是生成框架的一部分。没有理由使用我的语法或代码。我创建了“ORMMap
”类和基页来抽象出加载和保存 nHydrate
对象的功能。我之所以提到这一点,是因为它可能对您来说是一种奇怪的语法,而且我见过足够多的代码生成器,我知道它们使用奇怪且繁琐的语法让我感到困惑。nHydrate
是纯粹的 Entity Framework。如果您使用过 EF,那么我代码中使用的对象就不会有任何陌生之处。
我的页面
下面是两个屏幕截图,第一个是 Region
对象列表。第二个是 Customer
的编辑页面。我包含了客户编辑屏幕,因为它最复杂,并且包含依赖对象。屏幕非常简单,它只是一个允许数据输入的控件列表。

让我们从 Region
对象开始,因为它非常简单。它有一个 ID
和一个 Name
属性。这是经典的查找表。数据库中充满了这样的表,用于州、国家、地区等。它们只有一个主键和一个名称。当用户编辑这些对象之一时,他实际上只编辑名称,因为主键从不显示或编辑。在区域编辑页面中,我重写了“SetupBindings
”方法。数据库上下文被传递给我,所以我用它来查找我想要绑定的实际对象。我从 URL 字符串中获取主键。这个示例网站使用类似“/regionedit.aspx?id=2
”的 URL 结构。这使得用户可以轻松地将页面加入书签。在代码中,我还更改了标题,以向用户指示这是一个创建操作还是编辑操作。
请注意最后一行代码。这将名为“txtName
”的文本框绑定到区域对象。所有生成的实体都有关联的枚举,它们映射到它们的字段。这允许您编写可以通过枚举使用特定字段的方法。它们始终同步,因为它们是从您的 nHydrate
模型生成的,就像对象本身一样。这一行代码是将 Region
对象的“Name
”字段映射到文本框。这一行代码将 EF 对象双向绑定到 UI 控件。这里没有魔法字符串。我没有将字段“Name
”放在引号中(就像 dataset
一样)。我使用了 Region 对象提供的枚举。
由于只有一个 textbox
,因此映射 textbox
只需要一行代码。您需要为每个控件写一行代码。当然,还有针对每种控件类型的许多重载。您可以为其他 .NET 控件或您购买的第三方控件添加更复杂的重载。只需为一个新的控件类型添加一个重载,并添加处理新控件类型所需的任何逻辑。
protected override void SetupBindings(EFExampleEntities context)
{
int id = this.Request.GetURLInteger("id");
var item = context.Region.Where(x => x.RegionId == id).FirstOrDefault();
lblHeader.Text = "Edit Region";
if (item == null)
{
lblHeader.Text = "New Region";
item = CreateObject(context) as Region;
}
this.Mapper.Map(txtName, item, Region.FieldNameConstants.Name);
}
页面上的代码仅此而已。页面上有一个“CreateObject
”方法,当基页需要创建对象时会被调用,但这只是默认创建一个新对象并将其添加到上下文中。如果您在添加此对象类型时有更复杂的业务规则需要执行,您可以在此处添加它或调用该逻辑。
我的整个 Region
编辑页面大约有 50 行代码。我认为这确实很少。请记住,模型中定义的任何必要验证都会自动添加。
Customer
编辑页面当然更复杂一些,但结构相同。我们必须加载其他控件,所以看看它的代码也有意义。在这段代码中,我们看到了“InitializeList
”方法的使用。调用此方法以使用用户可以选择的值列表加载下拉列表。我们只需传入控件、要绑定的列表以及将用作文本和值的字段。所有这些都在 OnInit
事件中加载,因此在我的示例中不需要 viewstate
。稍后在代码中,我们将每个控件单独绑定到特定对象的相应属性。在此示例中,所有字段都绑定到同一个对象;但是,这并非必需。我们可以轻松地将一些 UI 控件绑定到主对象或其他任何我们想要的 EF 对象的关联对象。
protected override void SetupBindings(EFExampleEntities context)
{
int id = this.Request.GetURLInteger("id");
var item = context.Customer.Where(x => x.UserId == id).FirstOrDefault();
lblHeader.Text = "Edit Customer";
if (item == null)
{
lblHeader.Text = "New Customer";
item = CreateObject(context) as Customer;
}
//Load the dropdowns
this.Mapper.InitializeList(cboCountry,
context.Country.OrderBy(x => x.Name).ToList(),
Country.FieldNameConstants.Name.ToString(),
Country.FieldNameConstants.CountryId.ToString());
this.Mapper.InitializeList(cboCustomerType, context.CustomerType.OrderBy
(x => x.Name).ToList(), CustomerType.FieldNameConstants.Name.ToString(),
CustomerType.FieldNameConstants.CustomerTypeId.ToString());
this.Mapper.InitializeList(cboRegion, context.Region.OrderBy(x => x.Name).ToList(),
Region.FieldNameConstants.Name.ToString(),
Region.FieldNameConstants.RegionId.ToString());
//Bind the fields
this.Mapper.Map(txtFirstName, item,
Customer.FieldNameConstants.FirstName, lblFirstName);
this.Mapper.Map(txtLastName, item,
Customer.FieldNameConstants.LastName, lblLastName);
this.Mapper.Map(txtCity, item, Customer.FieldNameConstants.City, lblCity);
this.Mapper.Map(txtAddress, item, Customer.FieldNameConstants.Address, lblAddress);
this.Mapper.Map(txtCode, item, Customer.FieldNameConstants.Code, lblCode);
this.Mapper.Map(txtCompany, item,
Customer.FieldNameConstants.CompanyName, lblCompany);
this.Mapper.Map(txtEmail, item, Customer.FieldNameConstants.Email, lblEmail);
this.Mapper.Map(txtFax, item, Customer.FieldNameConstants.Fax, lblFax);
this.Mapper.Map(txtPhone, item, Customer.FieldNameConstants.Phone, lblPhone);
this.Mapper.Map(txtPostalCode, item,
Customer.FieldNameConstants.PostalCode, lblPostalCode);
this.Mapper.Map(cboCountry, item, Customer.FieldNameConstants.CountryId, lblCountry);
this.Mapper.Map(cboCustomerType, item,
Customer.FieldNameConstants.CustomerTypeId, lblCustomerType);
this.Mapper.Map(cboRegion, item, Customer.FieldNameConstants.RegionId, lblRegion);
}
现在让我们看一下列表页面。Region 列表页面显示了一个分页的区域列表,您可以在其中选择编辑或删除项目。显示列表分页的一个大问题。现在您无需担心这个问题,因为 nHydrate
为您消除了大部分的繁重工作。以下代码从 URL string
中获取页面偏移量和每页记录数,并使用它从数据库中提取数据列表。我正在使用一个我编写的 PagingURL
类来将 URL 解析成我关心的部分。分页对象保存页面偏移量和每页记录数,并将此信息传递到数据查询方法。当此调用返回时,分页对象上有一个名为“TotalRecords
”的附加属性,其中包含匹配 where 条件的所有记录的计数,从而允许您在屏幕上构建一个带有当前页、总页数、每页记录数和总记录数的页码控件。
一旦返回数据页,我只需将其直接绑定到 grid
。我构建了一个在列表页面上使用的分页控件,它只需获取返回的分页信息并构建一个风格化的页码。这就是构建列表/编辑数据录入网站所需要的一切。
using (var context = new EFExampleEntities())
{
var url = new PagingURL(this.Request.Url.AbsoluteUri);
var paging = new Widgetsphere.EFCore.DataAccess.Paging
(url.PageOffset, url.RecordsPerPage);
grdItem.DataSource = context.Region.GetPagedResults(x => x.Name, paging);
grdItem.DataBind();
PagingControl1.Populate(paging);
}
现在我们来看看如何从生成的对象中提取元数据。“ORMMap
”类实际上只是一个控件列表,我们将其与 Entity Framework 对象上的字段关联起来。每个 nHydrate
Entity Framework 对象都有一个关联的元数据类,可以通过属性获取,并在运行时用于拉取模型信息。常见的元数据包括显示名称、正则表达式和必填项。
显示名称是您为字段定义的友好名称。例如,“FirstName
”在数据库中可能是“First Name”,而“PostalCode
”可能是“Zip code”。这可用于提示和验证控件。要从生成的对象中获取显示名称,您可以使用以下代码。传入的实体是基 nHydrate
EF 对象。所有生成的对象都派生自此类型,因此此例程将适用于所有这些对象。
private string GetDisplayName(Enum field)
{
var context = new EFExampleEntities();
var metadata = context.GetMetaData(context.GetEntityFromField(field));
var a = metadata.GetType().GetField(field.ToString()).GetCustomAttributes
(typeof(System.ComponentModel.DataAnnotations.DisplayAttribute),
true).FirstOrDefault();
if (a == null) return field.ToString();
else return ((System.ComponentModel.DataAnnotations.DisplayAttribute)a).Name;
}
如果在模型中没有定义显示名称,则使用数据库名称。您可以执行相同的操作来获取验证、正则表达式和必填(非空)属性。
private bool IsNullable(Enum field)
{
var context = new EFExampleEntities();
var metadata = context.GetMetaData(context.GetEntityFromField(field));
var a = metadata.GetType().GetField(field.ToString()).GetCustomAttributes
(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute),
true).FirstOrDefault();
return (a != null);
}
private string GetRegularExpression(Enum field)
{
var context = new EFExampleEntities();
var metadata = context.GetMetaData(context.GetEntityFromField(field));
var a = metadata.GetType().GetField(field.ToString()).GetCustomAttributes
(typeof(System.ComponentModel.DataAnnotations.RegularExpressionAttribute),
true).FirstOrDefault();
if (a == null) return string.Empty;
else return ((System.ComponentModel.DataAnnotations.RegularExpressionAttribute)a).
Pattern;
}
请注意,模式很简单,就是获取对象的元数据类,该元数据类可以通过所有生成对象上的扩展方法检索。元数据类包含跟踪对象的所有字段,以及定义这些数据点(如必填、表达式、最小-最大范围等)的属性。您可以使用描述符类来构建复杂的 UI 结构,其中包含适用于您模型中所有实体的非特定代码。您可以将其抽象到一个通用程序集中,供所有项目使用。
另一个重要且节省时间的函数点是发出验证器。UI 开发中的一个常见问题是知道哪些字段需要验证,以及随着 DBA 更改数据库或项目经理更改业务规则时如何跟上。在所有其他开发过程中,很容易忘记更改某些任意屏幕,特别是因为这些是业务规则而不是错误。现在,一个简单而优雅的解决方案已经到来。只需使用元数据即可在页面上发出验证。如果有人更改了 nHydrate
模型并重新生成,那么验证器现在是模型驱动的,因此它们将自行重新配置,无需代码更改。
动态验证器
以下例程返回一个验证器列表,您可以将其添加到窗体的控件集合中。只需调用此方法即可获取一个列表以动态添加到您的页面。
public virtual IEnumerable<system.web.ui.control> GetValidators()
{
var retval = new List<system.web.ui.control>();
foreach (var control in _controlList.Keys)
{
var e = _controlList[control];
//Determine if we need a required field validator
if (NeedsValidatorRequired(e.Entity, e.Field,
e.CanHaveRequiredValidation, e.ForceRequiredValidation))
{
var r1 = new RequiredFieldValidator
{
ControlToValidate = control.UniqueID,
Display = ValidatorDisplay.None,
ErrorMessage = "The '" + GetDisplayName
(e.Entity, e.Field) + "' is required!"
};
retval.Add(r1);
}
//Determine if we need a regular expression validator
if (NeedsValidatorExpression(e.Entity, e.Field))
{
var r1 = new RegularExpressionValidator
{
ControlToValidate = control.UniqueID,
Display = ValidatorDisplay.None,
ErrorMessage = "The '" + GetDisplayName
(e.Entity, e.Field) + "' is not the correct format!",
ValidationExpression = GetRegularExpression
(e.Entity, e.Field)
};
retval.Add(r1);
}
}
return retval;
}
请注意,它会检查字段是否为必填项,如果是,则发出一个必需验证器。然后,它确定 Entity.Field
是否有验证表达式,如果有,则发出一个正则表达式验证器。您可以根据需要使其更具体。这两个只是一些非常容易添加的。另外请记住,所有这些都是通用代码。它不是针对任何一个特定对象(如 Country
或 Customer
)的。它适用于您可以构建的任何模型中的所有对象。
这个简单的例子演示了如何用很少的代码创建相当健壮的数据录入屏幕。整个想法是使用元数据来驱动您的应用程序。这在使用模型驱动开发时要容易得多。如果您的模型支持额外的元数据属性,而不仅仅是 ORM 映射器,那么您可以以相当通用的方式处理相当复杂的场景。
这将帮助您开始使用模型的 Web UI 开发。这种技术可以使您的标记保持较小。我们没有手动添加验证器,甚至没有提到文本框的 MaxLength
属性已设置。这确保用户无法输入过大的数据。可以在元数据中定义的信息可以在运行时作为代码和标记发出,从而减少您编写的代码以及相关的错误。
历史
- 2011年6月1日:初始帖子