ASP.NET Web 页面生命历程中的一天
Dino Esposito Wintellect
2003 年 8 月
适用于: Microsoft ASP.NET
摘要:了解围绕 ASP.NET Web 页构建的事件模型,以及一个 Web 页面在其转变为 HTML 的历程中的各个阶段。ASP.NET HTTP 运行时控制对象管线,对象管线首先将所请求的 URL 转换为一个页面类的活动实例,然后将其转换为普通 HTML 文本。本文将探讨一个页面的生存周期中的各个特征事件,并了解控件和页面编写者如何介入其中以改变其标准行为。(6 页打印页)
本页内容
简介
真正的 Page 类
页面生存周期
执行的各个阶段
小结
简介
Microsoft Internet 信息服务 (IIS) 所收到的对某 Microsoft ASP.NET 页面的每个请求都被移交给 ASP.NET HTTP 管线。HTTP 管线由一系列托管对象组成,这些对象按顺序处理该请求,并完成从 URL 到普通 HTML 文本的转换。HTTP 管线的入口点是 HttpRuntime 类。ASP.NET 基础结构为辅助进程中所承载的每个 AppDomain 创建此类的一个实例(请注意,该辅助进程为当前正在运行的每个 ASP.NET 应用程序维护一个不同的 AppDomain)。
HttpRuntime 类从内部池中选取一个 HttpApplication 对象,并让其处理该请求。HTTP 应用程序管理器所完成的主要任务就是找出将实际处理该请求的类。如果请求 .aspx 资源,则处理程序就是一个页面处理程序 — 即某个继承自 Page 的类的一个实例。资源类型和处理程序类型之间的关联关系存储于该应用程序的配置文件中。更准确地说,在 machine.config 文件的 <httpHandlers> 部分中定义默认的一组映射关系。然而,应用程序也可以在本地的 web.config 文件中自定义自己的 HTTP 处理程序列表。下面的程序行举例说明了定义用于 .aspx 资源的 HTTP 处理程序的代码。
<add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory"/>
扩展名可关联到一个处理程序类,或者更普遍地关联到一个处理程序工厂 (handler factory) 类。在所有情况下,负责处理请求的 HttpApplication 对象都会获得一个实现IHttpHandler 接口的对象。如果根据 HTTP 处理程序来解析关联资源/类,那么所返回的类将直接实现该接口。如果资源绑定到处理程序工厂,则需要另外一个步骤。处理程序工厂类实现IHttpHandlerFactory 接口,而该接口的 GetHandler 方法返回一个基于 IHttpHandler 的对象。
HTTP 运行时如何能完成整个循环并处理页面请求呢?IHttpHandler 接口特别提供了 ProcessRequest 方法。通过对代表所请求页面的对象调用此方法,ASP.NET 基础结构启动相应过程,从而针对浏览器生成输出。
真正的 Page 类
特定页面的 HTTP 处理程序类型取决于 URL。当首次调用 URL 时,将构建一个新类并将该类动态地编译成一个程序集。用于检查 .aspx 来源的语法分析过程的输出结果就是该类的源代码。该类被定义为 ASP 命名空间的一部分,并被赋予一个与原始 URL 相似的名称。例如,如果 URL 终结点是 page.aspx,则类名称为 ASP.Page_aspx。但是,也可通过编程设置 @Page指令的 ClassName 属性来控制类的名称。
HTTP 处理程序的基类是 Page。此类定义了所有页面处理程序所共享的方法和属性的最小集合。Page 类中实现 IHttpHandler 接口。
在某些情况下,实际处理程序的基类并非 Page,而是一个不同的类。例如,如果使用了代码隐藏,就会出现这种情况。代码隐藏是一种开发方法,它将页面所需的代码封装到一个单独的 C# 或 Microsoft Visual Basic.NET 类中。页面的代码就是一组事件处理程序和帮助器方法,用以实际创建该页面的行为。可以利用 <script runat=server> 标记将这种代码定义为内联代码,或者也可将其放到一个外部类 — 代码隐藏类中。代码隐藏类是一种继承自 Page 的类,但这种类具有一些额外的方法因而比较特殊。如果指定,代码隐藏类就用作 HTTP 处理程序的基类。
还有一种情况,即当应用程序配置文件的 <pages> 部分中重新定义了 PageBaseType 属性时,HTTP 处理程序也不是基于 Page 的。
<pages PageBaseType="Classes.MyPage, mypage" />
PageBaseType 属性指出了包含页面处理程序的基类的类型以及程序集。派生自 Page 的这个类可自动给处理程序赋予一组自定义和扩展的方法和属性。
页面生存周期
一旦完全确定 HTTP 页面处理程序类,ASP.NET 运行时就调用该处理程序的 ProcessRequest 方法以处理请求。通常情况下,无需更改此方法的实现方式,因为它是由 Page 类提供的。
此实现方法一开始就调用 FrameworkInitialize 方法,以此建立页面的控件树。此方法是 TemplateControl 类(Page 类本身就是从该类派生出来的)的一个受保护的虚拟成员。任何针对 .aspx 资源而动态生成的处理程序都重写 FrameworkInitialize。在此方法中,该页面的完整控件树得以构建。
接下来,ProcessRequest 使该页面经历若干阶段:初始化,加载视图状态信息和回发数据,加载页面的用户代码并执行回发服务器端事件。随后,该页面进入呈现模式:收集更新后的视图状态;生成 HTML 代码然后将其发送到输出控制台。最后,卸载页面,并认为已完成对该请求的处理。
在各个阶段中,页面都会激发一些 Web 控件和用户定义的代码所能截获并处理的事件。其中的一些事件是嵌入式控件专用的,因而并不能在 .aspx 代码级进行处理。
如果页面想要处理某个事件,它应该显式地注册相应的处理程序。然而,为了向后兼容早期的 Visual Basic 编程风格,ASP.NET 也支持一种隐式的事件挂起形式。在默认情况下,页面将尝试把特定的方法名与事件匹配起来;如果找到匹配的方法,就认为该方法是该事件的处理程序。ASP.NET 提供了六个方法名的特定识别。它们是Page_Init、Page_Load、Page_DataBind、Page_PreRender 和 Page_Unload。这些方法被当作是 Page 类所提供的相应事件的处理程序。HTTP 运行时将自动把这些方法与页面事件绑定起来,这样一来,开发人员就不必编写所需的粘接代码。例如,名为 Page_Load 的方法与页面的 Load 事件绑定,就像已编写以下代码一样。
this.Load += new EventHandler(this.Page_Load);
这种自动识别特殊名称的功能由 @Page 指令的 AutoEventWireup 属性控制。如果将该属性设置为 false,则任何想要处理某个事件的应用程序都需显式地连接到该页面事件。如果页面不使用自动事件关联功能,就不必进行额外的操作以匹配各名称和事件,从而其性能也稍有提升。应该注意的是,所有的 Microsoft Visual Studio.NET 项目在创建时都禁用了AutoEventWireup 属性。然而,此属性的默认设置为 true,意味着诸如 Page_Load 等方法会被识别并被绑定到相关的事件。
页面的执行过程包括下面表格中所列的一系列阶段,并以具有一些应用程序级事件和/或受保护且可重写的方法为特征。
表格 1. ASP.NET 页面生存周期中的关键事件
阶段 |
页面事件 |
可重写方法 |
---|---|---|
页面初始化 |
Init |
|
加载视图状态 |
LoadViewState |
|
处理回发数据 |
实现 IPostBackDataHandler 接口的任何控件中的 LoadPostData 方法 |
|
加载页面 |
Load |
|
回发更改通知 |
实现 IPostBackDataHandler 接口的任何控件中的 RaisePostDataChangedEvent 方法 |
|
处理回发事件 |
控件所定义的任何回发事件 |
实现了 IPostBackEventHandler 接口的任何控件的 RaisePostBackEvent 方法 |
页面呈现前阶段 |
PreRender |
|
保存视图状态 |
SaveViewState |
|
呈现页面 |
Render |
|
卸载页面 |
Unload |
在页面级上,以上所列的某些阶段是不可见的,并仅影响服务器控件编写者和那些凑巧要创建从 Page 派生的类的开发人员。页面向外界发送的活动信号仅包括Init、Load、PreRender、Unload 以及嵌入式控件所定义的所有回发事件。
执行的各个阶段
页面生存周期中的第一个阶段是初始化。这一阶段的标志就是 Init 事件,在成功创建页面的控件树后,对应用程序激发这个事件。换而言之,当 Init 事件发生时,在 .aspx 源文件中静态声明的所有控件都已实例化并取其默认值。控件可挂起 Init 事件,以便初始化在传入的 Web 请求的生存周期中所需的任何设置。例如,此时控件可以加载外部模板文件或设置各个事件的处理程序。应该注意到,这时还没有视图状态信息可供使用。
在初始化之后,页面框架立即加载该页面的视图状态。所谓视图状态就是一些名称/值对的集合,控件和页面本身可将那些对所有 Web 请求都必须始终有效的任何信息存储在其中。视图状态表示页面的调用上下文。一般情况下,其中包含上次在服务器中处理该页面时各控件的状态。首次在会话中请求页面时,视图状态为空。在默认情况下,视图状态被存储在一个隐藏字段中,而该字段是自行添加到页面中的。该字段名称为 __VIEWSTATE。通过重写 LoadViewState 方法(Control 类的一个受保护且可重写的方法)组件开发人员可控制如何还原视图状态以及如何将其内容映射到内部状态。
有些方法(如 LoadPageStateFromPersistenceMedium 及其相对的 SavePageStateToPersistenceMedium)可用于将视图状态加载并保存到别的存储介质(例如会话、数据库或服务器端的文件)中。与 LoadViewState 不同,上述方法仅在派生自 Page 的各个类中才可使用。
一旦还原了视图状态,页面树中的各个控件的状态就与浏览器上次呈现该页面时这些控件所处的状态相同。下一步包括更新这些控件的状态以加入客户端的变更。回发数据处理阶段使各个控件有机会更新其状态,以便准确地反映相应的 HTML 元素在客户端的状态。例如,一个服务器 TextBox 控件对应的 HTML 元素是 <input type=text>。在回发数据阶段,TextBox 控件将检索 <input> 标记的当前值并用它刷新其内部状态。每个控件负责从已发送的数据中提取相应值,并更新其某些属性。TextBox 控件将更新其 Text 属性,而 CheckBox 控件将刷新其 Checked 属性。服务器控件和 HTML 元素之间的匹配关系由二者的 ID 确定。
在回发数据处理阶段结束时,页面中的所有控件都根据客户端上所输入的更改来更新原先的状态。此时,对页面激发 Load 事件。
如果在处理两个不同的请求时某个敏感的属性被修改,则页面上可能有些控件需要完成某些任务。例如,如果在客户端修改了某个文本框控件的文本,则该控件激发 TextChanged 事件。如果利用来自客户端的值对该控件的一个或多个属性进行修改,每个控件可以决定激发一个适当的事件。对控件而言,如果这些更改是至关重要的,则这些控件实现 IPostBackDataHandler接口,在 Load 事件之后立即调用该接口的 LoadPostData 方法。通过编写 LoadPostData 方法的代码,一个控件可以确认自最近一次请求以来是否发生了任何关键的更改,并激发自己的更改事件。
页面生存周期内的关键事件就是:它被调用来执行与客户端上所激发的某个事件相关联的服务器端代码。当用户单击某个按钮时,页面回发数据。已发送值的集合中包含该按钮(该按钮启动整个操作)的 ID。如果已知该控件实现了 IPostBackEventHandler 接口(按钮和链接按钮将实现此接口),则页面框架调用 RaisePostBackEvent 方法。此方法所进行的操作取决于相应控件的类型。对于按钮和链接按钮,此方法查找 Click 事件处理程序并运行相关的委托。
在处理回发事件后,页面就准备进行呈现。这一阶段的标志是 PreRender 事件。各个控件可利用这个很好的时机,以便执行任何需要在保存视图状态和呈现输出结果的前一刻完成的最后一些更新操作。下一个状态为 SaveViewState,在这一状态中所有控件以及页面本身可以刷新自己的 ViewState 集合的内容。所得到的视图状态随后得以序列化、进行哈希运算、进行 Base64 编码并关联到 __VIEWSTATE 隐藏字段。
通过重写 Render 方法,即可更改各个控件的呈现机制。该方法获取一个 HTML 编写器对象,并使用该对象聚集所有将针对该控件生成的 HTML 文本。Page 类的 Render 方法的默认实现方式包括对所有成员控件的递归调用。对于每个控件,页面都调用 Render 方法并将 HTML 输出放入高速缓存。
一个页面的最后生存标志就是 Unload 事件,该事件在页面对象被解除之前发生。在此事件中,您应该释放可能占用的任何关键资源(例如,文件、图形对象、数据库连接)。
终于,在此事件之后,浏览器收到 HTTP 响应数据包并显示页面。
小结
ASP.NET 页面对象模型由于使用了事件机制而特别具有创新意义。Web 页面由各种控件构成,这些控件既形成一个丰富的基于 HTML 的用户界面,又通过事件与用户进行交互。在 Web 应用程序的上下文中建立事件模型是一项极具挑战性的任务。令人惊奇的是,利用服务器端代码可以处理客户端生成的事件,而且这种方法的输出看起来与 HTML 页面相同,只是经过了适当的修改。
要掌握这个模型,重要的一点是要理解页面生存周期中的不同阶段,以及 HTTP 运行时如何实例化和使用页面对象。
关于作者
Dino Esposito 是一位定居于意大利罗马的培训讲师和顾问。作为 Wintellect 团队的成员之一,Dino 专门研究 ASP.NET 和 ADO.NET,主要在欧洲和美国从事教学和咨询工作。特别地,Dino 还负责管理 Wintellect 的 ADO.NET 课件,并为 MSDN Magazine的 "Cutting Edge" 专栏撰写文章。如果希望与 Dino 联系,可发送电子邮件至 dinoe@wintellect.com。