Dino Esposito
Wintellect
2003 年 8 月
适用于:
Microsoft® ASP.NET
摘要:了解为 ASP.NET Web 页面建立的事件模型,以及 Web 页面转变为 HTML 过程中的各个阶段。ASP.NET HTTP 运行时负责管理对象管道,这些对象首先将请求的 URL 转换成 Page 类的具体实例,然后再将这些实例转换成纯 HTML 文本。本文将探讨那些作为页面生命周期标志的事件,以及控件和页面编写者如何干预并改变标准行为。(本文包含一些指向英文站点的链接。)
目录
简介
真正的 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"/>
扩展名可以与处理程序类相关联,并且更多是与处理程序工厂类相关联。在所有情况下,负责处理请求的 HttpApplication 对象都会获得一个实现 IHttpHandler 接口的对象。如果根据 HTTP 处理程序来解析关联的资源/类,则返回的类将直接实现接口。如果资源被绑定到处理程序工厂,则还需要额外的步骤。处理程序工厂类实现 IHttpHandlerFactory 接口,此接口的 GetHandler 方法将返回一个基于 IHttpHandler 的对象。
HTTP 运行时是如何结束这个循环并处理页面请求的?ProcessRequest 方法在 IHttpHandler 接口中非常重要。通过对代表被请求页面的对象调用此方法,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 处理程序的基类。
还有一种情况,HTTP 处理程序也不是基于 Page 的,即在应用程序配置文件的 <pages> 部分中,包含了 PageBaseType 属性的重新定义。
<pages PageBaseType="Classes.MyPage, mypage" />
PageBaseType 属性指明包含页面处理程序的基类的类型和程序集。从 Page 导出的这个类可以自动赋予处理程序扩展的自定义方法和属性集。
页面的生命周期完全识别 HTTP 页面处理程序类后,ASP.NET 运行时将调用处理程序的 ProcessRequest 方法来处理请求。通常情况下,无需更改此方法的实现,因为它是由 Page 类提供的。
此实现将从调用为页面构建控件树的 FrameworkInitialize 方法开始。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 方法 | |
处理回发事件 | 由控件定义的任意回发事件 | 任意实现 IPostBackDataHandler 接口的控件中的 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 事件。
页面中可能会有一些控件,当其某个敏感属性在两个不同的请求中被修改时,需要完成特定的任务。例如,如果 TextBox 控件的文本在客户端被修改,则此控件将触发 TextChanged 事件。每个控件在其一个或多个属性被修改为客户端输入的值时都可以决定触发相应的事件。对于这些更改对其非常关键的控件,控件实现 IPostBackDataHandler 接口,此接口的 LoadPostData 方法是在 Load 事件后立即调用的。通过对 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 期刊的“Cutting Edge”专栏撰写文章。要与他联系,请向 dinoe@wintellect.com 发送电子邮件。
一、服务器脚本基础介绍
首先,我们先复习一下Web服务器页面的基本执行方式:
1、 客户端通过在浏览器的地址栏敲入地址来发送请求到服务器端
2、 服务器接收到请求之后,发给相应的服务器端页面(也就是脚本)来执行,脚本产生客户端的响应,发送回
客户端
3、 客户端浏览器接收到服务器传回的响应,对Html进行解析,将图形化的网页呈现在用户面前
对于服务器和客户端的交互,通常通过下面几种主要方式:
1、 Form:这是最主要的方式,标准化的控件来获取用户的输入,Form的提交将数据发送给服务器端处理
2、 QueryString:通过在Url后面带参数达到将参数传送给服务器,这种方式其实跟Get方式的Form是一样的
3、 Cookies:这是一种比较特殊的方式,通常用于用户身份的确认
二、ASP.Net简介
传统的服务器脚本语言,如ASP、JSP等,编写服务器脚本的方式大同小异,都是在Html中嵌入解释或编译执行的
代码,由服务器平台执行这些代码来生成Html;对于这类似的脚本,页面的生存周期实际上很简单,就是从开头
至末尾,执行完所有的代码,当然用Java编写的Servlet可以编写更复杂的代码,但是从结构上看,和JSP没什么
区别。
ASP.Net的出现,打破了这种传统;ASP.Net采用了CodeBehind技术和服务器端控件,加入了服务器端的事件的概
念,改变了脚本语言编写的模式,更加贴近Window编程,使Web编程更加简单、直观;但是我们要看到,ASP.Net
本身并没有改变Web编程的基本模式,只是封装了一些细节、提供了一些易用的功能,使代码更容易编写和维护;
从某种程度上来说,将服务器端执行的方式复杂化了,这就是我们今天要讨论的主体:ASP.Net Web Page的生存
周期。
三、ASP.Net请求处理模式
我们说,ASP.Net的Web Page并没有脱离Web编程的模式,所以它仍然是以 请求->接收请求->处理请求->发送响
应 这样的模式在工作,每一次与客户端的交互都会引发一次新的请求,所以一个Web Page的生命周期是以一次请
求为基础的。
当IIS收到客户端的请求的时候,会将请求交给aspnet_wp这个进程来处理,这个进程会查看请求的应用程序域是
否存在,如果不存在则会创建一个,然后会创建一个Http运行时(HttpRuntime)来处理请求,这个运行时“为当
前应用程序提供一组 ASP.NET 运行时服务”(摘自MSDN)。
HttpRuntime在处理请求的时候,会维护一系列的应用程序实例,也就是应用程序的Global类(global.asax)的
实例,这些实例在没有请求的时候,会存放在一个应用程序池中(实际上应用程序池由另一个类来维护,
HttpRuntime只是简单的调用),每接收到一个请求,HttpRuntime都会获取一个闲置的实例来处理请求,这个实
例在请求结束前不会处理其他的请求,处理完毕之后,它又会回到池中,“一个实例在其生存期内被用于处理多
个请求,但它一次只能处理一个请求。”(摘自MSDN)
当应用程序实例处理请求的时候,它会创建请求页面类的实例,执行它的ProcessRequest方法来处理请求,这个
方法也就是Web Page生命周期的开始。
四、Aspx页面与CodeBehind
在深入了解页面的生命周期之前,我们先来探讨一些Aspx与CodeBehind之间的关系。
<%@ Page language="c#" Codebehind="WebForm.aspx.cs" Inherits="MyNamespace.WebForm" %>
相信使用过CodeBehind技术的朋友,对ASPX顶部的这句话应该是非常熟悉了,我们来一项一项的分析它:
Page language="c#" 这个就不用多说了吧
Codebehind="WebForm.aspx.cs" 这一句表示绑定的代码文件
Inherits="MyNamespace.WebForm" 这句非常重要,它表示页面继承的类名称,也就是CodeBehind的代码文件中的
类,这个类必须从System.Web.WebControls.Page派生
从上面我们可以分析出,实际上CodeBehind中的类就是页面(ASPX)的基类,到这里,可能有些朋友要问了,在
编写ASPX的时候,完全是按照ASP的方式,在Html中嵌入代码或者嵌入服务器控件,没有看到所谓“类”的影子
啊?
这个问题实际上并不复杂,各位使用ASP.Net编程的朋友可以到你们的系统盘:
\WINDOWS\Microsoft.NET\Framework\<版本号>\Temporary ASP.NET Files这个目录下,这个下面就放了所有本机
上存在的ASP.Net应用程序的临时文件,子目录的名称就是应用程序的名称,然后再下去两层(为了保证唯一,
ASP.Net自动产生了两层子目录,并且子目录名称是随机的),然后我们会发现有很多类似:“yfy1gjhc.dll”、
“xeunj5u3.dll”这样的链接库以及“komee-bp.0.cs”、“9falckav.0.cs”这样的源文件,实际上这就是ASPX
被ASP.Net动态编译后的结果,打开这些源文件我们可以发现:
public class WebForm_aspx : MyNamespace.WebForm, System.Web.SessionState.IRequiresSessionState
这就印证了我们前面的说法,ASPX是代码绑定类的子类,它的名称是ASPX文件名加上“_aspx”后缀,通过研究这
些代码我们可以发现,实际上所有aspx中定义的服务器控件都是在这些代码中生成的,然后动态产生这些代码的
时候,把原来在ASPX中嵌入的代码写在了相应的位置。
当某个页面第一次被访问的时候,Http运行时就会使用一个代码生成器去解析ASPX文件并生成源代码并编译,然
后以后的访问就直接调用编译后的dll,这也是为什么ASPX第一次访问的时候非常慢的原因。
解释了这个问题,我们再来看另一个问题。我们在使用代码绑定的时候,在设计页面拖一个控件,然后切换到代
码视图,就可以直接在Page_Load中使用这个控件了,既然控件是在子类中产生的,那为什么在父类中可以直接使
用呢?
实际上我们可以发现,每当用VS.Net拖一个控件到页面上,代码绑定文件中总是会类似这样的添加一个声明:
protected System.Web.WebControls.Button Button1;
我们可以发现这个字段被声明成protected,而且名字与ASPX中控件的ID一致,仔细想一想,这个问题就迎刃而解
了。我们前面提到ASPX的源代码是被生成器动态生成和编译的,生成器会产生动态生成每一个服务器控件的代
码,在生成的时候,它会检查父类有没有声明这个控件,如果声明了,它会添加类似下面的一句代码:
this.DataGrid1 = __ctrl;
这个__ctrl就是生成该控件的变量,这时候它就把控件的引用赋给了父类中相应的变量,这也是为什么父类中的
声明必须为protected(实际上也可以为public),因为要保证子类能够调用。
然后在执行Page_Load的时候,因为这时候父类的声明已经被子类中的初始化代码赋了值,所以我们就可以使用这
个字段来访问对应的控件,了解了这些,我们就不会犯在代码绑定文件中的构造器里使用控件,造成空引用的异
常的错误了,因为构造器是最先执行的,这时候子类的初始化还没有开始,所以父类中的字段是空值,至于子类
是什么时候初始化我们放到后面讨论。
五、页面生存周期
现在回到第三个标题中讲到的内容,我们讲到了HttpApplication的实例接收请求,并创建页面类的实例,实际上
这个实例也就是动态编译的ASPX的类的一个实例,上一个标题中我们了解到ASPX实际上是代码绑定中类的子类,
所以它继承了所有的protected方法。
现在我们来看看VS.Net自动生成的CodeBehind类的代码,以此来开始我们对页面生命周期的探讨:
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN:该调用是 ASP.NET Web 窗体设计器所必需的。
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// 设计器支持所需的方法 - 不要使用代码编辑器修改
/// 此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.DataGrid1.ItemDataBound += new System.Web.UI.WebControls.DataGridItemEventHandler
(this.DataGrid1_ItemDataBound);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
这个就是使用VS.Net产生的Page的代码,我们来看,这里面有两个方法,一个是OnInit,一个是
InitializeComponent,后者被前者调用,实际上这就是页面初始化的开始,在InitializeComponent中我们看到
了控件的事件声明和Page的Load声明。
下面是从MSDN中摘录的一段描述和一个页面生命周期方法和事件触发的顺序表:
“每次请求 ASP.NET 页时,服务器就会加载一个 ASP.NET 页,并在请求完成时卸载该页。页及其包含的服务器
控件负责执行请求并将 HTML 呈现给客户端。虽然客户端和服务器之间的通讯是无状态的和断续的,但是必须使
客户感觉到这是一个连续执行的过程。”
“这种连续性假象是由 ASP.NET 页框架、页及其控件实现的。回发后,控件的行为必须看起来是从上次 Web 请
求结束的地方开始的。虽然 ASP.NET 页框架可使执行状态管理相对容易一些,但是为了获得连续性效果,控件开
发人员必须知道控件的执行顺序。控件开发人员需要了解:在控件生命周期的各个阶段,控件可使用哪些信息、
保持哪些数据、控件呈现时处于哪种状态。例如,在填充页上的控件树之前控件不能调用其父级。”
“下表提供了控件生命周期中各阶段的高级概述。有关详细信息,请点击表中的链接。”
阶段
控件需要执行的操作
要重写的方法或事件
初始化
初始化在传入 Web 请求生命周期内所需的设置。请参阅处理继承的事件。
Init 事件(OnInit 方法)
加载视图状态
在此阶段结束时,就会自动填充控件的 ViewState 属性,详见维护控件中的状态中的介绍。控件可以重写
LoadViewState 方法的默认实现,以自定义状态还原。
LoadViewState 方法
处理回发数据
处理传入窗体数据,并相应地更新属性。请参阅处理回发数据。
注意 只有处理回发数据的控件参与此阶段。
LoadPostData 方法
(如果已实现 IPostBackDataHandler)
加载
执行所有请求共有的操作,如设置数据库查询。此时,树中的服务器控件已创建并初始化、状态已还原并且窗体
控件反映了客户端的数据。请参阅处理继承的事件。
Load 事件
(OnLoad 方法)
发送回发更改通知
引发更改事件以响应当前和以前回发之间的状态更改。请参阅处理回发数据。
注意 只有引发回发更改事件的控件参与此阶段。
RaisePostDataChangedEvent 方法
(如果已实现 IPostBackDataHandler)
处理回发事件
处理引起回发的客户端事件,并在服务器上引发相应的事件。请参阅捕获回发事件。
注意 只有处理回发事件的控件参与此阶段。
RaisePostBackEvent 方法
(如果已实现 IPostBackEventHandler)
预呈现
在呈现输出之前执行任何更新。可以保存在预呈现阶段对控件状态所做的更改,而在呈现阶段所对的更改则会丢
失。请参阅处理继承的事件。
PreRender 事件
(OnPreRender 方法)
保存状态
在此阶段后,自动将控件的 ViewState 属性保持到字符串对象中。此字符串对象被发送到客户端并作为隐藏变量
发送回来。为了提高效率,控件可以重写 SaveViewState 方法以修改 ViewState 属性。请参阅维护控件中的状
态。
SaveViewState 方法
呈现
生成呈现给客户端的输出。请参阅呈现 ASP.NET 服务器控件。
Render 方法
处置
执行销毁控件前的所有最终清理操作。在此阶段必须释放对昂贵资源的引用,如数据库链接。请参阅 ASP.NET 服
务器控件中的方法。
Dispose 方法
卸载
执行销毁控件前的所有最终清理操作。控件作者通常在 Dispose 中执行清除,而不处理此事件。
UnLoad 事件(On UnLoad 方法)
从这个表里面我们可以清楚的看到一个Page从装载到卸载之间调用的方法和触发的时间,接下来我们就深入的对
其进行一些分析。
看了上面的表,细心的朋友可能要问了,既然OnInit是页面生命周期的开始,而我们在上一讲中谈到控件在子类
中被创建,那么在这里实际上在InitializeComponent方法中我们已经可以使用父类中声名的字段了,那么就意味
着子类的初始化更在这之前?
在第三个标题中我们讲到了页面类的ProcessRequest才是真正意义上的页面声明周期的开始,这个方法是由
HttpApplication调用的(其中调用的方式比较复杂,有机会单独撰文来讲解),一个Page对请求的处理就是从这
个方法开始,通过反编译.Net类库来查看源代码,我们发现在System.Web.WebControls.Page的基类:
System.Web.WebControls.TemplateControl(它是页面和用户控件的基类)中定义了一个
“FrameworkInitialize”虚拟方法,然后在Page的ProcessRequest中最先调用了这个方法,在生成器生成的ASPX
的源代码中我们发现了这个方法的踪影,所有的控件都在这个方法中被初始化,页面的控件树就在这个时候产
生。
接下来的事情就简单了,我们来逐步分析页面生命周期的每一项:
1、 初始化
初始化对应Page的Init事件和OnInit方法。
如果要重写,MSDN推荐的方式是重载OnInti方法,而不是增加一个Init事件的代理,这两者是有差别的,前者可
以控制调用父类OnInit方法的顺序,而后者只能在父类的OnInit后执行(实际上是在OnInit里面被调用的)。
2、 加载视图状态
这是个比较重要的方法,我们知道,对于每次请求,实际上是由不同的页面类实例来处理的,为了保证两次请求
间的状态,ASP.Net使用了ViewState,关于ViewState的描述,请参考本人的另一篇文章:
http://expert.csdn.net/Expert/topic/1558/1558798.xml?temp=.2561609
LoadViewState方法就是从ViewState中获取上一次的状态,并依照页面的控件树的结构,用递归来遍历整个树,
将对应的状态恢复到每一个控件上。
3、 处理回发数据
这个方法是用来检查客户端发回的控件数据的状态是否发生了改变。方法的原型:
public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
postDataKey是标识控件的关键字(也就是postCollection中的Key),postCollection是包含回发数据的集合,
我们可以重写这个方法,然后检查回发的数据是否发生了变化,如果是则返回一个True,“如果控件状态因回发
而更改,则 LoadPostData 返回 true;否则返回 false。页框架跟踪所有返回 true 的控件并在这些控件上调
用 RaisePostDataChangedEvent。”(摘自MSDN)
这个方法是System.Web.WebControls.Control中定义的,也是所有需要处理事件的自定义控件需要处理的方法,
对于我们今天讨论的Page来说,可以不用管它。
4、 加载
加载对应Load事件和OnLoad方法,对于这个事件,相信大多数朋友都会比较熟悉,用VS.Net生成的页面中的
Page_Load方法就是响应Load事件的方法,对于每一次请求,Load事件都会触发,Page_Load方法也就会执行,相
信这也是大多数人了解ASP.Net的第一步。
Page_Load方法响应了Load事件,这个事件是在System.Web.WebControl.Control类中定义的(这个类是Page和所
有服务器控件的祖宗),并且在OnLoad方法中被触发。
很多人可能碰到过这样的事情,写了一个PageBase类,然后在Page_Load中来验证用户信息,结果发现不管验证是
否成功,子类页面的Page_Load总是会先执行,这个时候很可能留下一些安全性的隐患,用户可能在没有得到验证
的情况下就执行了子类中的Page_Load方法。
出现这个问题的原因很简单,因为Page_Load方法是在OnInit中被添加到Load事件中的,而子类的OnInit方法中是
先添加了Load事件,然后再调用base.OnInit,这样就造成了子类的Page_Load被先添加,那么先执行了。
要解决这个问题也很简单,有两种方法:
1) 在PageBase中重载OnLoad方法,然后在OnLoad中验证用户,然后调用base.OnLoad,因为Load事件是在OnLoad
中触发,这样我们就可以保证在触发Load事件之前验证用户。
2) 在子类的OnInit方法中先调用base.OnInit,这样来保证父类先执行Page_Load
5、 发送回发更改通知
这个方法对应第3步的处理回发数据,如果处理回发数据返回True,页面框架就会调用此方法来触发数据更改的事
件,所以自定义控件的回发数据更改事件需要在此方法中触发。
同样这个方法对于Page来说,没有太大的用处,当然你也可以在Page的基础上自己定义数据更改的事件,这当然
也是可以的。
6、 处理回发事件
这个方法是大多数服务器控件事件引发的地方,当请求中包含控件事件触发的信息时(服务器控件的事件是另一
个论题,我会在不久将来另外撰文讨论),页面控件会调用相应控件的RaisePostBackEvent方法来引发服务器端
的事件。
这里又引出一个常见的问题:
经常有网友问,为什么修改提交后的数据并没有更改
多数的情况都是他们没有理解服务器事件的触发流程,我们可以看出,触发服务器事件是在Page的Load之后,也
就是说页面会先执行Page_Load,然后才会执行按钮(这里以按钮为例)的点击事件,很多朋友都是在Page_Load
中绑定数据,然后在按钮事件中处理更改,这样做有一个毛病,Page_Load永远都是在按钮事件之前执行,那么意
味着数据还没来得及更改,Page_Load中的数据绑定的代码就先执行了,原有的数据又赋给了控件,那么执行按钮
事件的时候,实际上获得的是原有的数据,那么更新当然就没有效果了。
更改这个问题也非常简单,比较合理的做法是把数据绑定的代码写成一个方法,我们假设为BindData:
private void BindData()
{
//绑定数据
}
然后修改PageLoad:
private void Page_Load( object sender,EventArgs e )
{
if( !IsPostBack )
{
BindData(); //在页面第一次访问的时候绑定数据
}
}
最后在按钮事件中:
private Button1_Click( object sender,EventArgs e )
{
//更新数据
BindData();//重新绑定数据
}
7、 预呈现
最终请求的处理都会转变为发回服务器的响应,预呈现这个阶段就是执行在最终呈现之前所作的状态的更改,因
为在呈现一个控件之前,我们必须根据它的属性来产生Html,比如Style属性,这是最典型的例子,在预呈现之
前,我们可以更改一个控件的Style,当执行预呈现的时候,我们就可以把Style保存下来,作为呈现阶段显示
Html的样式信息。
8、 保存状态
这个阶段是针对加载状态的,我们多次提到,请求之间是不同的实例在处理,所以我们需要把本次的页面和控件
的状态保存起来,这个阶段就是把状态写入ViewState的阶段。
9、 呈现
到这里,实际上页面对请求的处理基本就告一段落了,在Render方法中,会递归整个页面的控件树,依次调用
Render方法,把对应的Html代码写入最终响应的流中。
10、处置
实际上就是Dispose方法,在这个阶段会释放占用的资源,例如数据库连接。
11、卸载
最后,页面会执行OnUnLoad方法触发UnLoad事件,处理在页面对象被销毁之前的最后处理,实际上ASP.Net提供这
个事件只是设计上的考虑,通常资源的释放都会在Dispose方法中完成,所以这个方法也变成鸡肋了。
我们简单的介绍了页面的生存周期,对于服务器端事件的处理做了不太深入的讲解,今天主要是想大家了解页面
执行的周期,对于服务器控件的事件和生存期我会在后续在写一些文章来探讨。
这些内容是我在学习ASP.Net的时候对Page研究的一些心得,具体的细节没有很详细的探讨,更多的内容请大家参
考MSDN,但是我举了一些初学者常犯的错误和出现错误的原因,希望可以给大家带来启发。