ASP.NET 动态数据

5 分钟内构建数据驱动型企业网站

James E.Henry

多年以来,开发人员一直忙于完成一些枯燥的任务,那就是构建具有创建、读取、更新和删除 (CRUD) 功能的数据层以供 UI 使用。我至今还能回想起 10 多年前的一个项目:我需要编写代码,以便自动从 Rational Rose 对象模型生成业务层和数据层。当时的工作量非常大。

几年后,Microsoft 推出了 Microsoft .NET Framework 和 DataSet(数据集)对象。利用数据集设计器,您可以将强类型化的数据集连接到 SQL Server 等后端数据库,然后在整个应用程序中使用数据集。这最大程度地减少了直接使用 SQL 的需要,但仍需要手动创建网页以显示数据库中的数据。

又过了几年,Microsoft 推出了实体框架。这种对象关系映射 (ORM) 框架使用 LINQ 来抽象数据库架构,并将其作为概念架构呈现给应用程序。就像数据集一样,这种技术还能最大程度地减少直接使用 SQL 的需要。但是,仍然需要手动创建网页以显示数据。

现在,Microsoft 推出了 ASP.NET 动态数据,它结合了实体框架和 ASP.NET 路由,让应用程序能够响应实际上并不存在的 URL。利用这些功能,您在几分钟内就能创建可用于生产的数据驱动型网站。本文将为您介绍具体的创建过程。

入门

让我们假设以下情况:我要为一个名为 Adventure Works 的虚构公司构建 Intranet 网站。公司能够通过这个网站管理员工信息。

公司的数据存储在 Microsoft SQL Server 2008 数据库中。(您可以从 msftdbprodsamples.codeplex.com 下载和安装该数据库。)

接下来,打开 Microsoft Visual Studio 2010 并创建一个新的 ASP.NET 动态数据实体网站 C# 项目。

ASP.NET 动态数据实体网站项目类型利用了 ASP.NET 路由和实体框架的优势,可让您快速创建数据驱动型网站。若要使用这个功能,您需要在项目中添加一个实体框架数据模型。为此,请在“网站”菜单中选择“添加新项”。在“添加新项”对话框中,选择 ADO.NET 实体数据模型。将其命名为 HumanResources.edmx。

Visual Studio 随后会提示您将模型加至 App_Code 文件夹中。选择“是”允许其进行此项更改。创建网站项目时,Visual Studio 会动态编译 App_Code 文件夹中的所有代码。至于实体数据模型,Visual Studio 会自动为数据上下文和实体分别生成局部类。在本例中,Visual Studio 将代码放到一个名为 HumanResources.Designer.cs 的文件中。

接下来,将出现“实体数据模型向导”,如图 1 所示。选择“从数据库生成”并单击“下一步”。

图 1 启动实体数据模型向导

接下来,选择一个与 Adventure Works 数据库的连接。如果不存在任何连接,您需要创建一个新的连接。图 2 显示了与名为 Dev\BlueVision 的 SQL Server 实例上的 AdventureWorks 的连接。

图 2 配置数据连接

在下一页上,您可以选择 Human Resources 架构中的所有表。架构的名称显示在括号中。图 3 显示了一些选中的表。

图 3 在 Visual Studio 中为 Human Resources 架构选择表

单击“完成”后,Visual Studio 会通过您为项目选择的表自动生成实体。图 4 显示了 Visual Studio 通过数据库架构生成的七个实体。Visual Studio 使用数据库中的外键约束来创建各个实体之间的关系。例如,Employee 实体与 JobCandidate 实体之间建立了一对多关系。这表示一个员工可以成为公司内多个职位的候选人。

图 4 Visual Studio 中通过数据库表生成的实体

另外请注意,EmployeeDepartmentHistory 实体联接了 Employee 和 Department 实体。如果 EmployeeDepartmentHistory 表仅包含用于联接 Employee 和 Department 表所需的字段,则 Visual Studio 将直接忽略 EmployeeDepartmentHistory 实体。这样就可以直接在 Employee 和 Department 实体之间进行导航。

使用 ASP.NET 路由

利用 ASP.NET 路由,应用程序可以响应实际上并不存在的 URL。例如,两个 URL(http://mysite/Employee 和 http://mysite/Department)可以被定向到页面 http://mysite/Template.aspx。页面本身随后从该 URL 提取信息,以确定显示员工列表还是显示部门列表,两个视图均采用了相同的显示模板。

路由就是一个映射到 ASP.NET HTTP 处理程序的 URL 模式。处理程序负责确定如何将实际的 URL 映射到已解释的模式。ASP.NET 动态数据使用名为 DynamicDataRouteHandler 的路由处理程序来解释 URL 模式中的 {table} 和 {action} 占位符。例如,处理程序可以使用以下 URL 模式:

http://mysite/{table}/{action}.aspx

该模式随后可用于解释 URL http://mysite/Employee/List.aspx。

Employee 将作为 {table} 占位符处理,而 List 则作为 {action} 占位符处理。 处理程序随后可以显示一系列 Employee 实体示例。 DynamicDataRouteHandler 使用 {table} 占位符确定要显示的实体名称,使用 {action} 参数确定用于显示实体的页面模板。

Global.asax 文件包含一个名为 RegisterRoutes 的静态方法,当应用程序第一次启动时会调用该方法,如下所示:

public static void RegisterRoutes(RouteCollection routes) {
  // DefaultModel.RegisterContext(typeof(YourDataContextType), 
  //   new ContextConfiguration() { ScaffoldAllTables = false });
  routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
    Constraints = new RouteValueDictionary(
    new { action = "List|Details|Edit|Insert" }),
    Model = DefaultModel
  });
}

您需要做的第一件事就是取消注释调用 DefaultModel 实例的 RegisterContext 方法的代码。 此方法在 ASP.NET 动态数据中注册实体框架数据上下文。 您需要按照如下所示修改代码行:

DefaultModel.RegisterContext(
    typeof(AdventureWorksModel.AdventureWorksEntities), 
    new ContextConfiguration() { ScaffoldAllTables = true });

第一个参数用于指定数据上下文的类型,在本例中为 AdventureWorksEntities。将 ScaffoldAllTables 属性设置为 True 后,您就可以在网站上查看所有实体。如果将此属性设置为 False,就需要将 ScaffoldTable(true) 特性应用到要在网站上查看的每个实体。(实际上,您应当将 ScaffoldAllTables 属性设置为 False,以防止将整个数据库意外暴露给最终用户。)

RouteCollection 类的 Add 方法用于将新的 Route 实例添加到路由表中。有了 ASP.NET 动态数据,Route 实例实际上就是一个 DynamicDataRoute 实例。DynamicDataRoute 类在内部将 DynamicDataRouteHandler 用作处理程序。传递给 DynamicDataRoute 的构造函数的参数代表处理程序应当用于处理实际 URL 的模式。还设置了一个约束,用于将 {action} 的值限制为 List、Details、Edit 或 Insert。

理论上来说,如果您要使用 ASP.NET 动态数据,所需要做的就是这些了。如果您要生成并运行应用程序,则需要参考图 5 中所示的页面。

图 5 基本的 ASP.NET 动态数据站点

支持元数据

您应当注意的一点是,实体的名称与它们表示的表名称是相同的。在代码或数据库中,这没有问题,但是在 UI 中却不行。

通过 ASP.NET 动态数据,更改显示的实体名称很容易,只需将 DisplayName 特性应用到代表实体的类即可。因为实体类包含在一个由 Visual Studio 自动重新生成的文件中,所以您应当在单独的代码文件中更改局部类的代码。因此,请将一个名为 Metadata.cs 的新代码文件添加到 App_Code 文件夹中。然后,将以下代码添加到该文件中:

using System;
  using System.Web;
  using System.ComponentModel;
  using System.ComponentModel.DataAnnotations;

  namespace AdventureWorksModel {
    [DisplayName("Employee Addresses")]
    public partial class EmployeeAddress { }
  }

然后重新生成并运行该应用程序。 您会注意到 EmployeeAddresses 变成了 Employee Addresses。

同样,您也应当将 EmployeeDepartmentHistories、EmployeePayHistories 和 JobCandidates 的名称更改为更恰当的名称。

接下来,单击 Shifts 链接。 这将显示一个员工轮班列表。 我将分别把 StartTime 和 EndTime 的名称更改为 Start Time 和 End Time。

还有一个问题,Start Time 和 End Time 的值会同时显示日期和时间。 而在本例中,用户实际上只需要查看时间,因此我将把 Start Time 和 End Time 值的格式定义为仅显示时间。 您可以通过 DataType 特性为字段指定更具体的数据类型,例如 EmailAddress 或 Time。 将 DataType 特性应用到适当的字段。

首先,打开 Metadata.cs 文件并添加以下类定义:

[MetadataType(typeof(ShiftMetadata))]
public partial class Shift { }
public partial class ShiftMetadata { }

请注意,MetadataType 特性被应用到了 Shift 类。 通过这个特性,您可以指定一个单独的类,为实体的字段包含其他元数据。 您无法将其他元数据直接应用到 Shift 类的成员,因为无法将同一个类成员添加到多个局部类中。 例如,在 HumanResources.Designer.cs 中定义的 Shift 类已经拥有一个名为 StartTime 的字段。 因此,您无法将同一字段添加到 Metadata.cs 中定义的 Shift 类中。 此外,您不应当手动修改 HumanResources.Designer.cs,因为该文件将由 Visual Studio 重新生成。

利用 MetadataType 特性,您可以指定一个完全不同的类,以便将元数据应用到字段中。 将以下代码添加到 ShiftMetadata 类中:

[DataType(DataType.Time)]
  [Display(Name = "Start Time")]
  public DateTime StartTime { get; set; }

DataType 特性用于指定 Time 枚举的值,以指明 StartTime 字段的格式应当为时间值。其他枚举的值还包括 PhoneNumber、Password、Currency 和 EmailAddress 等等。Display 特性用于指定当字段显示在列表中时,应当显示为字段的列的文本;以及当字段以编辑或只读模式显示时,应当显示为字段的标签的文本。

接下来,重新生成并运行该应用程序。图 6 显示了单击 Shifts 链接的结果。

图 6 使用 MetadataType 定义修订过的 Shifts 页面

您可以添加类似的代码,以更改 EndTime 字段的外观。

现在,让我们来看看 JobCandidates 链接。Employee 列显示 NationalIDNumber 字段的值。这可能没什么用。尽管 Employee 数据库表不具备员工名称这样的字段,但它有一个员工登录信息字段,那就是 LoginID。此字段也许可以提供有关本网站用户的更有用的信息。

为此,我会再一次修改元数据代码,以便所有 Employee 列都显示 LoginID 字段的值。打开 Metadata.cs 文件并添加以下类定义:

[DisplayColumn("LoginID")]
public partial class Employee { }

DisplayColumn 特性指定来自实体的字段名称,用于表示该实体的实例。图 7 显示了带有 LoginID 的新列表。

图 7 利用 LoginID 来标识 Employee

边栏:.NET Framework 4 中的特性更改

对于 Microsoft .NET Framework 4,我们建议您使用 Display 特性,而非 .NET Framework 3.5 中的 DisplayName 特性。框架中显然仍有 DisplayName,但只要能够使用 Display 特性,我们就不建议您使用 DisplayName。

我们建议您使用 Display 而非 DisplayName 的原因有两个。首先,Display 特性支持本地化,而 DisplayName 特性则不支持。

其次,您可以通过 Display 特性控制一切。例如,您可以控制字段文本以各种方式显示(提示框、标题等等);字段是否显示为筛选器;以及字段是否在基本框架中显示(AutoGenerate=false 为不显示)。

因此,尽管本文的示例中显示的代码是完全有效的,我们还是建议您将 DisplayName 和 ScaffoldColumn 替换为 Display 特性。在类级别,您仍然需要使用 ScaffoldTable 和 DisplayName 特性。

您最好采纳这些建议,因为 Microsoft 的其他团队支持 DataAnnotations 命名空间(WCF RIA 服务),所以这样做可以确保代码与他们的代码兼容。

Employee 实体的 ContactID 字段实际上引用的是 Contact 表中的一行,该表是数据库中 Person 架构的一部分。因为我未从 Person 架构添加任何表,所以 Visual Studio 允许直接编辑 ContactID 字段。为了强制保证关系完整性,我将禁止对此字段进行编辑,但是仍然允许显示此字段以供参考。打开 Metadata.cs 文件并应用以下 MetadataType 特性,以便修改 Employee 类:

[DisplayColumn("LoginID")]
[MetadataType(typeof(EmployeeMetadata))]
public partial class Employee { }

然后按照以下方式定义 EmployeeMetadata 类:

public partial class EmployeeMetadata {
  [Editable(false)]
  public int ContactID { get; set; }
}

Editable 特性用于指定 UI 中的字段是否可以编辑。

接下来,我将向 EmployeePayHistory 实体添加元数据,将 Rate 字段显示为 Hourly Rate,并将该字段的值格式化为货币。 将以下类定义添加到 Metadata.cs 文件中:

[MetadataType(typeof(EmployeePayHistoryMetadata))]
public partial class EmployeePayHistory { }
public partial class EmployeePayHistoryMetadata {
  [DisplayFormat(DataFormatString="{0:c}")]
  [Display(Name = "Hourly Rate")]
  public decimal Rate { get; set; }
}

自定义模板

Visual Studio 项目包含一个名为 FieldTemplates 的文件夹。 此文件夹包含一些用户控件,用于编辑各种数据类型的字段。 默认情况下,ASP.NET 动态数据将字段与用户控件相关联,控件的名称与字段的关联数据类型的名称相同。 例如,Boolean.ascx 用户控件包含用于显示布尔字段的 UI,而 Boolean_Edit.ascx 用户控件则包含用于编辑布尔字段的 UI。

您也可以选择将 UIHint 特性应用到字段,以确保使用不同的用户控件。 我将添加一个自定义字段模板,以显示一个 Calendar 控件,用于编辑 Employee 实体的 BirthDate 字段。

在 Visual Studio 中,将一个新的动态数据字段项添加到项目中,并将其命名为 Date.ascx。 Visual Studio 会自动将另外一个名为 Date_Edit.ascx 的文件添加到 FieldTemplates 文件夹中。 我首先使用以下标记替换 Date_Edit.ascx 页面的内容:

<asp:Calendar ID="DateCalendar" runat="server"></asp:Calendar>

然后使用图 8 中显示的完整类定义修改 Date_Edit.ascx.cs 文件。

图 8 自定义 Date_EditField 类

public partial class Date_EditField : System.Web.DynamicData.FieldTemplateUserControl {
  protected void Page_Load(object sender, EventArgs e) {
    DateCalendar.ToolTip = Column.Description;
  }

  protected override void OnDataBinding(EventArgs e) {
    base.OnDataBinding(e);

    if (Mode == DataBoundControlMode.Edit && 
        FieldValue != null) {
      DateTime date = DateTime.MinValue;
      DateTime.TryParse(FieldValue.ToString(), out date);
      DateCalendar.SelectedDate = 
        DateCalendar.VisibleDate = date;
    }
  }
    
  protected override void ExtractValues(
    IOrderedDictionary dictionary) {
    dictionary[Column.Name] = ConvertEditedValue(
      DateCalendar.SelectedDate.ToShortDateString());
  }

  public override Control DataControl {
    get {
      return DateCalendar;
    }
  }
}

我将重写 OnDataBinding 方法,将 Calendar 控件的 SelectedDate 和 VisibleDate 属性设置为 FieldValue 字段的值。 FieldValue 字段派生自 FieldTemplateUserControl,表示将要呈现的数据字段的值。 我还要修改 ExtractValues 重写方法,将对 SelectedDate 属性所做的所有更改都保存到一个“字段名称-值”对的字典中。 ASP.NET 动态数据将使用此字典中的值来更新底层数据源。

接下来,我需要通知 ASP.NET 动态数据,为 BirthDate 字段使用 Date.ascx 和 Date_Edit.ascx 字段模板。 有两种方法可以实现此目标。 首先,我可以按照以下方式应用 UIHint 特性:

[UIHint("Date")]
public DateTime BirthDate { get; set; }

或者,我也可以按照以下方式应用 DateType 特性:

[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }

DataType 特性将数据类型名称映射到用户控件名称,从而实现自动映射。UIHint 特性可让您在字段类型名称与用户控件名称不匹配时拥有更强的控制能力。图 9 显示了编辑员工的结果。

图 9 自定义的员工编辑表单

如果您更改选定员工的出生日期并单击“更新”,新的数据将保存到数据库中。

PageTemplates 文件夹包含一些页面模板,用于为实体呈现适当的视图。默认情况下,支持五个页面模板:List.aspx、Edit.aspx、Details.aspx、Insert.aspx 和 ListDetails.aspx。List.aspx 页面模板用于呈现 UI,以表格数据的形式显示实体。Details.aspx 页面模板用于呈现实体的只读视图,而 Edit.aspx 页面模板则用于显示实体的可编辑视图。Insert.aspx 页面用于呈现带有默认字段值的可编辑视图。ListDetails.aspx 页面模板让您查看一系列实体,并在单独的页面上显示选定实体的详细信息。

在前文中,我提到 ASP.NET 动态数据会检查为路由定义的 {action} 参数的值,从而自动将 URL 请求路由到适当的页面。例如,如果 ASP.NET 动态数据确定 {action} 参数的值为 List,它将使用 List.aspx 页面模板来显示实体列表。您可以更改现有的页面模板或向文件夹中添加一个新的模板。如果您添加新的模板,则必须确保将该模板添加到 Global.asax 文件的路由表中。

EntityTemplates 文件夹包含一些模板,用于以只读、编辑和插入模式显示实体实例。在默认情况下,此文件夹包含三个模板:Default.ascx、Default_Edit.ascx 和 Default_Insert.ascx,它们分别以只读、编辑和插入模式显示实体实例。若要创建仅适用于特定实体的模板,只需向文件夹中添加一个新的用户控件并将其命名为实体集的名称即可。例如,如果您将名为 Shifts.ascx 的新用户控件添加到文件夹中,ASP.NET 动态数据将使用此用户控件为 Shift 实体(Shifts 实体集)呈现只读模式。同样,Shifts_Edit.ascx 和 Shifts_Insert.ascx 分别为 Shift 实体呈现编辑和插入模式。

对于每一个实体列表,ASP.NET 动态数据使用实体的外键字段、布尔字段和枚举字段来构建筛选器列表。筛选器作为 DropDown 控件添加到列表页面中,如图 10 所示。

图 10 在页面中包含数据筛选器

对于布尔筛选器,DropDownList 控件只包含三个值:All、True 和 False。对于枚举筛选器,DropDownList 控件包含所有枚举的值。对于外键筛选器,DropDownList 控件包含所有不同的外键值。筛选器被定义为 Filters 文件夹中的用户控件。在默认情况下,只存在三种用户控件:Boolean.ascx、Enumeration.ascx 和 ForeignKey.ascx。

赶紧行动起来吧!

尽管这只是一种虚构的情况,您还是看到了,完全有可能在几分钟内就创建一个完全可以运行的 Human Resources 网站。随后,我将添加元数据和自定义字段模板以增强 UI。

ASP.NET 动态数据提供了现成的功能,让您可以快速建立并运行一个站点。但是,它还是完全可自定义的,因此满足不同开发人员和组织的需求。ASP.NET 动态数据支持 ASP.NET 路由,这使您可以重复利用 CRUD 操作的页面模板。如果您不得不频繁执行乏味的任务,以便在每个 Web 应用程序项目中实现 CRUD 页面,那么 ASP.NET 动态数据就可以让您的工作倍加轻松。

James Henry  是 BlueVision LLC 的独立软件开发人员,BlueVision LLC 是一家专门提供 Microsoft 技术咨询的公司。他出版了两本书:《Developing Business Intelligence Solutions Using Information Bridge and Visual Studio .NET》(Blue Vision,2005)和《Developing .NET Custom Controls and Designers Using C#》(Blue Vision,2003)。您可以通过电子邮件 msdnmag@bluevisionsoftware.com 与他联系。

衷心感谢以下技术专家对本文的审阅:Scott Hunter