迎接 ASP.NET 4.0

K. Scott Allen

明年发布的 Visual Studio 2010 和 .NET 4.0 将为 ASP.NET 开发人员带来两个成熟的 Web 应用程序生成框架:ASP.NET Web 窗体框架和 ASP.NET MVC 框架。这两个框架均构建在核心 ASP.NET 运行时之上,并且拥有一些新功能,可引领我们进入新的十年。

要涵盖 ASP.NET 的所有新功能,本文的篇幅远远不够,因为两个框架以及基础运行时中的改进数不胜数。所以,我将重点介绍我认为对 Web 窗体和 MVC 很重要的新功能。

ASP.NET Web 窗体的新功能

截止到 Microsoft 发布版本 4 时,ASP.NET Web 窗体已经走过了八个年头。在此期间,该团队坚持不懈地优化着该框架,进行着各种各样的改进。在我的上一篇专栏中,我顺带提到了其中的部分改进,例如,ASP.NET 的核心服务中现在包含一些新类,这些新类简化了对 URL 路由功能的使用;Page 基类上新的 MetaKeywords 和 MetaDescription 属性简化了对窗体上元标记内容的控制。但是这些变化相对较小。

Web 窗体中的重大变化解决了该框架的一些主要缺点。许多开发人员都希望能够更好地控制 Web 窗体及其控件所生成的 HTML,包括 HTML 中发出的客户端标识符。在 4.0 中,ASP.NET 的许多服务器端控件已经过改写,它们生成的 HTML 更容易通过 CSS 控制样式,并且符合传统的 Web 实践。此外,基类中还添加了新的属性,使开发人员能够更好地控制由框架生成的客户端标识符。我将在接下来的部分中重点介绍这些变化。

CSS 友好的 HTML

很难用 CSS 控制样式的服务器控件的一个示例是 ASP.NET 菜单控件。当菜单呈现时,它将发出嵌套的 table 标记,其中包含 cellpadding、cellspacing 和 border 特性。更糟糕的是,菜单控件将样式信息嵌在嵌套表格的单元格中,并且在页面顶端插入内联的样式块。例如,让我们来看看下面的简单菜单定义:

<asp:Menu runat="server" ID="_menu">
    <Items>
        <asp:MenuItem Text="Home" NavigateUrl="~/Default.aspx" />
        <asp:MenuItem Text="Shop" NavigateUrl="~/Shop.aspx" />
    </Items>
</asp:Menu>

在 ASP.NET 3.5 中,这个简单菜单会生成下面的 HTML(为了清晰起见,省略或缩短了某些特性):

<table class="..." cellpadding="0" cellspacing="0" border="0">
    <tr id="_menun0">
            <table cellpadding="0" cellspacing="0" 
                border="0" width="100%">
                    <td style="...">
                        <a class="..." href="Default.aspx">Home</a>
            </table>
</table>

在 ASP.NET 4.0 中,Microsoft 改写了菜单控件,以生成语义清晰的标记。ASP.NET 4.0 中同样的菜单控件将生成下面的 HTML:

<div id="_menu">
    <ul class="level1">
        <li><a class="level1" href="Default.aspx" target="">Home</a></li>

在早期版本的 ASP.NET 中,如果使用控件适配器为控件提供备选的呈现逻辑,也能实现这种 CSS 友好的标记,但是现在,标记在默认情况下就是 CSS 友好的。如果您已经针对 ASP.NET 3.5 生成的 HTML 编写了样式表和客户端脚本,您可以将 web.config 中 pages 部分的 controlRenderingCompatibilityVersion 特性设置为值“3.5”,使该控件生成我们在前面看到的嵌套的 table 标记。此特性的默认值为 4.0。请注意,4.0 中的菜单控件仍然会在页面顶端生成样式块,但是您可以通过将该控件的 IncludeStyleBlock 属性设置为 false,来关闭此功能。

4.0 中的许多其他控件也都是 CSS 友好的。例如,RangeValidator 和 RequiredFieldValidator 等验证控件不再呈现内联样式,FormView、Login 和 Wizard 等模板控件也不再将自己呈现在 table 标记中(仅当您将这些控件的 RenderOuterTable 属性设置为 false,才会呈现在 table 标记中)。其他控件也有了变化。仅举其中一个例子:通过将 RepeatLayout 属性设置为 OrderedList 或 UnorderedList 值(分别强制控件使用 ol 和 li 元素进行呈现),可以强制 RadioButtonList 和 CheckBoxList 控件将其输入呈现在列表元素内。

生成客户端 ID

如果您曾经编写过客户端脚本来操作 DOM,那您可能已经了解用来更改客户端 ID 特性的 ASP.NET 关联。为了确保所有 ID 特性在页面上都是唯一的,ASP.NET 通过连接控件的 ID 属性与附加信息来生成客户端 ID。在服务器上,您可以使用控件的 ClientID 属性来访问所生成的值。

例如,如果控件位于命名容器(像用户控件和母版页一样实现了 INamingContainer 接口的控件)中,则 ASP.NET 在控件的 ID 前加上命名容器的 ID 作为前缀,以此来生成 ClientID 值。对于要呈现重复的 HTML 块的数据绑定控件,ASP.NET 将添加含有连续编号的前缀。如果您查看任何 ASP.NET 页面的源代码,很可能会遇到类似于“ctl00_content_ctl20_ctl00_loginlink”的 ID 值。在为 Web 窗体页编写客户端脚本时,这些生成的值额外增加了难度。

在 Web 窗体 4.0 中,每个控件都增加了一个新的 ClientIDMode 属性。您可以使用此属性来影响 ASP.NET 用于生成控件的 ClientID 值的算法。如果将该值设置为 Static,则 ASP.NET 使用控件的 ID 作为其 ClientID,而不会连接任何字符串或添加任何前缀。例如,以下代码中的 CheckBoxList 将用客户端 ID“checklist”来生成一个 <ol> 标记,而不管该控件显示在页面上的什么位置:

<asp:CheckBoxList runat="server" RepeatLayout="OrderedList" 
                  ID="checklist" ClientIDMode="Static">
    <asp:ListItem>Candy</asp:ListItem>
    <asp:ListItem>Flowers</asp:ListItem>
</asp:CheckBoxList>

在使用值为 Static 的 ClientIDMode 时,您需要确保客户端标识符是唯一的。如果页面上存在重复的 ID 值,一定会破坏任何通过 ID 值来搜索 DOM 元素的脚本。

ClientIDMode 属性还有三个附加的值可供使用。Predictable 值对于实现 IDataBoundListControl 的控件(如 GridView 和 ListView)非常有用。将 Predictable 值与这些控件的 ClientIDRowSuffix 属性联合使用,可通过在 ID 末尾添加特定的后缀值来生成客户端 ID。例如,下面的 ListView 将绑定到一系列 Employee 对象。每个对象都具有 EmployeeID 和 IsSalaried 属性。ClientIDMode 和 ClientIDRowSuffix 属性的组合告诉 CheckBox 生成类似于 employeeList_IsSalaried_10 的客户端 ID,其中 10 表示关联员工的 ID。

<asp:ListView runat="server" ID="employeeList" 
                      ClientIDMode="Predictable"
                      ClientIDRowSuffix="EmployeeID">
            <ItemTemplate>
                <asp:CheckBox runat="server" ID="IsSalaried" 
                              Checked=<%# Eval("IsSalaried") %> />
            </ItemTemplate>
        </asp:ListView>

ClientIDMode 的另一个可能值是 Inherit。在默认情况下,页面上的所有控件都使用值为 Inherit 的 ClientIDMode。Inherit 表示控件将使用与其父控件相同的 ClientIDMode。在前一个代码示例中,ListView 的 ClientIDMode 值为 Predictable,而 CheckBox 从 ListView 继承其 ClientIDMode 值。ClientIDMode 的最后一个可能值是 AutoID。AutoID 使 ASP.NET 使用与 Version 3.5 中一样的算法来生成 ClientID 属性。页面的 ClientIDMode 属性的默认值为 AutoID。因为页面上的所有控件在默认情况下都使用值为 Inherit 的 ClientIDMode,所以将现有 ASP.NET 应用程序转移到 4.0,不会改变运行时用来生成客户端 ID 的算法,除非您对 ClientIDMode 属性进行更改。也可以在 web.config 的 pages 部分中设置此属性,为应用程序中的所有页面提供不同的默认值。

新的项目模板

Visual Studio 2008 中的 Web 应用程序和网站项目模板提供了 Default.aspx 页面、web.config 文件和 App_Data 文件夹。这些起始模板非常简单,如果要在实际的应用程序中使用,需要先做一些附加的工作。Visual Studio 2010 中同样的模板提供了更多基础结构,使您能够使用最新的实践来生成应用程序。图 1 显示了由这些模板生成的全新应用程序的屏幕截图。

图 1 Visual Studio 2010 中的新 Web 应用程序

请注意,新应用程序在默认情况下包含一个母版页 (Site.master)。您在新项目中找到的所有 .aspx 文件都将是内容页面,它们使用 ContentPlaceholder 控件在母版页定义的结构中插入内容。请注意,新项目还在 Content 目录中包含一个样式表 (Site.css)。母版页使用 link 标记来包含此样式表,而在此样式表中,您将发现已经定义了大量样式,用于控制页面正文、标题、主要布局及其他内容的外观。新项目还包含一个 Scripts 目录,该目录中含有最新版本的 jQuery 库,该库是一个由 Microsoft 提供正式支持的开源 JavaScript 框架,并且在安装 Visual Studio 2010 时就附带了该库。

新项目模板及其对母版页和样式表的使用,将帮助开发人员在使用 Web 窗体时向正确的方向前进。图 2 显示了正在运行的新应用程序。Visual Studio 2010 还为网站和 Web 应用程序包含了“空”模板。这些空模板在您使用时不包含任何文件或目录,因此您可以从头开始设计您的应用程序。

图 2 运行新的 ASP.NET 应用程序

另一个有关 ASP.NET 4.0 中的新项目的好消息是,web.config 文件现在几乎是空的。我们习惯于在 ASP.NET 3.5 web.config 文件中看到的大多数配置现在已包含在 machine.config 文件中,该文件存放在 4.0 框架的安装目录中。这包括来自 System.Web.Extensions 目录的控件配置、配置用来支持 Web 服务的 JavaScript 代理的 HTTP 处理程序和模块以及运行于 IIS 7 之下的网站的 system.webserver 部分。

ASP.NET MVC 的新功能

Visual Studio 2010 会给我们带来第二版的 ASP.NET MVC 框架。尽管还很年轻,该框架已经吸引了许多希望框架提供可测试性的 Web 开发人员。第二版的 ASP.NET MVC 一如既往地专注于提高开发人员的工作效率,以及添加基础结构来处理大型的企业级项目。

构建超大型 ASP.NET Web 窗体应用程序的一种方法是将应用程序划分为多个子项目(由 P&P Web Client Composite Library 实现的一种方法)。在 ASP.NET MVC 1.0 中很难采用这种方法,因为它需要处理大量 MVC 约定。MVC 2.0 正式使用“领域”的概念来支持这种情况。领域使您能够将一个 MVC 应用程序划分为多个 Web 应用程序项目,或者划分为一个项目中的多个目录。领域有助于在逻辑上区分同一应用程序的不同部分,从而使其更容易维护。

MVC Web 应用程序的父领域是一个 MVC 项目,该项目将包含 global.asax 以及该应用程序的根级别 web.config 文件。父领域也可以包含通用的内容,例如应用程序级的样式表、JavaScript 库和母版页。子领域也是 MVC Web 应用程序项目,但由于这些项目在运行时存在于父领域项目之下,因此父领域及其子领域将呈现为一个应用程序。

例如,假设有一个大型清单应用程序。除了父领域之外,清单应用程序可能被划分为排序、分发、报告和管理领域。每个领域都存在于一个单独的 MVC Web 项目中,并且每个项目都需要通过包含一个从抽象基类 AreaRegistration 派生的类来注册其例程。在下面的代码中,我们重写了 AreaName 属性,以便返回报告领域的友好名称;并且重写了 RegisterArea 方法,以便定义可在报告领域中使用的例程:

public class ReportingAreaRegistration : AreaRegistration
    public override string AreaName
        get { return "Reporting"; }
    public override void RegisterArea(AreaRegistrationContext context)
        context.MapRoute(
            // route name
            "ReportingDefault",
            // url pattern
            "reporting/{controller}/{action}",
            // route defaults
            new { controller = "Home", action = "Index" },
            // namespaces
            new string[] { "Reporting.Controllers" });            

请注意,我们使用了一个包含命名空间的字符串数组,在查找报告领域的控制器时需要搜索这些命名空间。限制要搜索的命名空间,可使不同的领域具有同名的控制器(例如,应用程序中可以存在多个 HomeController 类)。

DataAnnotations 可提供轻松验证

ASP.NET MVC 中的 DefaultModelBinder 负责将数据从请求环境转移到模型属性中。例如,当模型绑定器看到一个模型带有名为 Title 的属性时,它会在窗体、查询字符串和服务器变量中查找具有匹配名称 (Title) 的变量。但是,除了简单的类型转换以外,模型绑定器不会执行任何验证检查。如果您希望模型对象的 Title 属性仅包含长度不超过 50 个字符的字符串,则必须在以下过程中执行此验证检查:执行控制器操作,实现自定义模型绑定器,或者在模型上实现 IDataErrorInfo 接口。

在 ASP.NET MVC 2.0 中,DefaultModelBinder 将查看模型对象的 DataAnnotation 特性。这些 DataAnnotation 特性使您可以为模型提供验证约束。例如,请考虑下面的 Movie 类:

public class Movie
    [Required(ErrorMessage="The movie must have a title.")]
    [StringLength(50, ErrorMessage="The movie title is too long.")]
    public string Title { get; set; }

Title 特性上的属性告诉模型绑定器:Title 是一个必需字段,并且字符串的最大长度为 50 个字符。当验证失败时,MVC 框架可自动在浏览器中显示 ErrorMessage 文本。其他内置的验证特性包括一个用于检查范围的特性和一个用于匹配正则表达式的特性。

截止到本文定稿时,MVC 运行时仅使用验证特性进行服务器端验证检查。MVC 团队希望在发布 MVC 2.0 时,能够从验证特性生成客户端验证逻辑。使用这些特性来实现服务器端和客户端验证,将使应用程序的可维护性出现重大突破。

模板化帮助器

ASP.NET MVC 2.0 中的模板化帮助器也使用 DataAnnotation 特性。但模板帮助器并不是使用这些属性来实现验证逻辑,而是使用这些属性来实现模型的 UI 显示。模板帮助器现在具有新的 HTML 帮助器方法:DisplayFor 和 EditorFor。这些帮助器方法将基于模型的类型为给定的模型查找模板。例如,让我们使用前面用过的 Movie 类,但是提供一个附加的属性:

public class Movie
    // ...
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

在这种情况下,每一部电影都附带其发布日期,但是没有人会关注一部影片是在几点发布的。在显示此属性时,我们只需要显示日期信息,而不需要显示时间信息。请注意,该属性用 DataType 特性进行了修饰,这表明了我们的意图。

为了正确显示发布日期,我们需要一个显示模板。显示模板只是一个扩展名为 .ascx 的部分视图,它存放在 DisplayTemplates 文件夹中。DisplayTemplates 文件夹本身位于控制器的视图文件夹之下(在这种情况下,该模板仅适用于此控制器的视图)或位于共享视图文件夹中(在这种情况下,该模板可在所有地方使用)。在本例中,模板需要具有名称 Date.ascx,并且其内容如下所示:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.Encode(String.Format("{0:d}", Model)) %>

为了使 MVC 框架使用此模板,在呈现 ReleaseDate 属性时,我们需要使用 DisplayFor 帮助器方法。图 3 显示的代码来自另一个模板:Movie.ascx 显示模板。

图 3 Movie.ascx 显示模板

<%@ Control Language="C#" 
    Inherits="System.Web.Mvc.ViewUserControl<Movie>" %>
    <fieldset>
        <legend>Fields</legend>
            Title:
            <%= Html.LabelFor(m => m.Title) %>
            <%= Html.DisplayFor(m => m.Title) %>
            <%= Html.LabelFor(m => m.ReleaseDate) %>
            <%= Html.DisplayFor(m => m.ReleaseDate) %>
    </fieldset>

请注意,LabelFor 和 DisplayFor 帮助器方法是强类型化的,这可以帮助您在模型重构时传播更改。若要在应用程序的任何位置使用 Movie.ascx 模板来显示电影,我们只需再次使用 DisplayFor 帮助器即可。下面的代码来自针对 Movie 类强类型化的视图:

<asp:Content ID="detailContent" 
             ContentPlaceHolderID="MainContent" 
             runat="server">                    
        Movie:
        <%= Html.DisplayFor(m => m) %>
</asp:Content>

DisplayFor 方法是强类型化的,可将同一个模型用作视图页面,因此 DisplayFor lambda 表达式中的 m 参数的类型为 Movie。在显示电影时,DisplayFor 将自动使用 Movie.ascx 模板(随后该模板使用 DisplayFor 来查找 Date.ascx 模板)。如果我们不使用电影的 ReleaseDate 属性上的 DataType 特性,DisplayFor 不会使用 Date.ascx 模板,而会显示 ReleaseDate 的日期部分和时间部分,但是 DataType 特性会指导框架使用正确的模板。这种强类型化的、嵌套的模板和数据类型批注的概念非常强大,肯定会使工作效率大大提高。

K. Scott Allen* 是 Pluralsight 技术团队的成员,也是 OdeToCode 的创始人。您可以通过 scott@OdeToCode.com 与 Scott 联系,或通过 odetocode.com/blogs/scott 访问其博客。*

衷心感谢以下技术专家,感谢他们审阅了本文:Phil Haack 和 Matthew Osborn

请将您想询问的问题和提出的意见发送至 xtrmasp@microsoft.com