1.3 ASP.NET生命周期

关于ASP.NET生命周期是一个比较有深度的话题,但它却又是一个合格ASP.NET程序员所必须了解的。本部分通过如下三方面来分别进行阐述:

❑IIS 6.0的ASP.NET应用程序生命周期。

❑IIS 7.0的ASP.NET应用程序生命周期。

❑ASP.NET页面生命周期。

1.3.1 IIS 6.0的ASP.NET应用程序生命周期

在ASP.NET中,若要对ASP.NET应用程序进行初始化并使它处理请求,必须执行一些处理步骤。此外,ASP.NET只是对浏览器发出的请求进行处理的Web服务器结构的一部分。根据微软MSDN的相关资料,可以把ASP.NET应用程序生命周期分为5个阶段:

第一阶段:用户从Web服务器请求应用程序资源。

ASP. NET应用程序的生命周期以浏览器向Web服务器(对于ASP.NET应用程序,通常为IIS)发送请求为起点。ASP.NET是Web服务器下的ISAPI扩展。Web服务器接收到请求时,会对所请求的文件的文件扩展名进行检查,确定应由哪个ISAPI扩展处理该请求,然后将该请求传递给合适的ISAPI扩展。ASP.NET处理已映射到其上的文件扩展名,如.aspx、.ascx、.ashx和.asmx。

如果文件扩展名尚未映射到ASP.NET,则ASP.NET将不会接收该请求。对于使用ASP.NET身份验证的应用程序,理解这一点非常重要。例如,由于.htm文件通常没有映射到ASP.NET,所以ASP.NET将不会对.htm文件请求执行身份验证或授权检查。因此,即使文件仅包含静态内容,如果希望ASP.NET检查身份验证,也应使用映射到ASP.NET的文件扩展名创建该文件,如采用文件扩展名.aspx。

值得注意的是,如果要创建服务于特定文件扩展名的自定义处理程序,必须在IIS中将该扩展名映射到ASP.NET,还必须在应用程序的Web.config文件中注册该处理程序。

第二阶段:ASP.NET接收对应用程序的第一个请求。

当ASP.NET接收到对应用程序中任何资源的第一个请求时,名为Application Manager的类会创建一个应用程序域。应用程序域为全局变量提供应用程序隔离,并允许单独卸载每个应用程序。在应用程序域中,将为名为HostingEnvironment的类创建一个实例,该实例提供对有关应用程序的信息(如存储该应用程序的文件夹的名称)的访问,如图1-22所示。

figure_0057_0041

图 1-22 ASP.NET接收对应用程序的第一个请求的流程图

第三阶段:为每个请求创建ASP.NET核心对象。

创建了应用程序域并对Hosting Environment对象进行了实例化之后,ASP.NET将创建并初始化核心对象,如HttpContext、HttpRequest和HttpResponse。HttpContext类包含特定于当前应用程序请求的对象,如HttpRequest和HttpResponse对象。HttpRequest对象包含有关当前请求的信息,包括Cookie和浏览器信息。HttpResponse对象包含发送到客户端的响应,包括所有呈现的输出和Cookie,这些内容将在后文做详细讲解。

第四阶段:将HttpApplication对象分配给请求。

初始化所有核心应用程序对象之后,将通过创建HttpApplication类的实例启动应用程序。如果应用程序具有Global.asax文件,则ASP.NET会创建Global.asax类(从HttpApplication类派生)的一个实例,并使用该派生类表示应用程序。

第一次在应用程序中请求ASP.NET页或进程时,将创建HttpApplication的一个新实例。不过,为了尽可能提高性能,可对多个请求重复使用HttpApplication实例。创建HttpApplication的实例时,将同时创建所有已配置的模块,如图1-23所示。

figure_0058_0042

图 1-23 将HttpApplication对象分配给请求的流程图

第五阶段:由HttpApplication管线处理请求。

在处理该请求时将由HttpApplication类执行以下事件:

❑对请求进行验证,将检查浏览器发送的信息,并确定其是否包含潜在恶意标记。

❑如果已在Web.config文件的UrlMappingsSection节中配置了任何URL,则执行URL映射。

❑引发BeginRequest事件。

❑引发AuthenticateRequest事件。

❑引发PostAuthenticateRequest事件。

❑引发AuthorizeRequest事件。

❑引发PostAuthorizeRequest事件。

❑引发ResolveRequestCache事件。

❑引发PostResolveRequestCache事件。

❑根据所请求资源的文件扩展名,选择实现IHttpHandler的类,对请求进行处理。如果该请求针对从Page类派生的对象(页),并且需要对该页进行编译,则ASP.NET会在创建该页的实例之前对其进行编译。

❑引发PostMapRequestHandler事件。

❑引发AcquireRequestState事件。

❑引发PostAcquireRequestState事件。

❑引发PreRequestHandlerExecute事件。

❑为该请求调用合适的IHttpHandler类的ProcessRequest方法。

❑引发PostRequestHandlerExecute事件。

❑引发ReleaseRequestState事件。

❑引发PostReleaseRequestState事件。

❑如果定义了Filter属性,则执行响应筛选。

❑引发UpdateRequestCache事件。

❑引发PostUpdateRequestCache事件。

❑引发EndRequest事件。

❑引发PreSendRequestHeaders事件。

❑引发PreSendRequestContent事件。

关于上面的这些事件,将在1.5节进行讲解。

1.3.2 IIS 7.0的ASP.NET应用程序生命周期

其实,在IIS 7.0集成模式下的请求同样会经历5个阶段:

1)发出一个对应用程序资源的请求。

2)统一管道接收对应用程序的第一个请求。

3)将为每个请求创建响应对象。

4)将HttpApplication对象分配给请求。

5)由HttpApplication管线处理请求。

这5个阶段类似于在IIS 6.0中对ASP.NET资源的请求所经历的阶段。IIS 7.0和IIS 6.0的处理阶段之间的主要区别在于ASP.NET如何与IIS服务器集成。

在IIS 6.0中,有两个请求处理管道:一个管道用于本机代码ISAPI筛选器和扩展组件;另一个管道用于托管代码应用程序组件,如ASP.NET。

而在IIS 7.0中,ASP.NET运行时与Web服务器集成,这样就有了一个针对所有请求的统一的请求处理管道。集成管道是一种统一的请求处理管道,它同时支持本机代码和托管代码模块。实现IHttpModule接口的托管代码模块可访问该请求管道中的所有事件。例如,托管代码模块可用于ASP.NET网页(.aspx文件)和HTML页(.htm或.html文件)的ASP.NET窗体身份验证。即使IIS和ASP.NET将HTML页视为静态资源,情况也是如此。对于ASP.NET开发人员,集成管道有以下益处:

1)集成管道引发由HttpApplication对象公开的所有事件,这使现有的ASP.NET HTTP模块可在IIS 7.0集成模式下工作。

2)在Web服务器级别、网站级别或Web应用程序级别,都可配置本机代码和托管代码模块。这包括用于会话状态、Forms身份验证、配置文件以及角色管理的内置ASP.NET托管代码模块。此外,可以为所有请求启用或禁用托管代码模块,无论请求是否针对ASP.NET资源(如aspx文件)。

3)可以在管道中的任何阶段调用托管代码模块。这包括在对请求进行任何服务器处理之前,在所有服务器处理都已发生之后,或者两者间的任何阶段。

4)可以通过应用程序的Web.config文件注册模块,也可以启用或禁用模块。

除此之外,在IIS 7.0中还包含多个额外的应用程序事件,如MapRequestHandler事件。

1.3.3 ASP.NET页面生命周期

ASP. NET页面运行时,此页也将经历一个生命周期,在生命周期中将执行一系列处理步骤。这些步骤包括初始化、实例化控件、还原和维护状态、运行事件处理程序代码以及进行呈现等。了解页生命周期非常重要,因为这样做你就能在生命周期的合适阶段编写代码,以达到预期效果。此外,如果你要开发自定义控件,就必须熟悉页面生命周期,以便正确进行控件初始化,使用视图状态数据填充控件属性以及运行任何控件行为代码。

一般来说,一个ASP.NET页面要经历下面概述的各个阶段。

1.浏览器提出请求

浏览器提出请求发生在页面生命周期开始之前。浏览器请求页时,ASP.NET将确定是否需要分析和编译页(从而开始ASP.NET生命周期),或者是否可以在不运行页的情况下发送页的缓存版本以进行响应。

2.页面框架初始化

ASP. NET在这个阶段开始创建页面,它产生你在.aspx页面里用标签定义的所有控件。此外,如果页面不是第一次被请求,即只是一次回送,ASP.NET将反序列化视图状态信息并把它们应用到所有控件上。Page.Init事件在这个阶段将被触发。

Page. Init在所有控件都已初始化且已应用所有外观设置后引发,使用该事件来读取或初始化控件属性。

3.用户代码初始化

Page. Load事件在这个处理阶段被触发,不管页面是第一次被请求还是作为回发的一部分被请求,Page.Load事件总会触发。但这样就出现了一个问题:怎样来判断页面是第一次加载还是后续的加载?

其实,针对上面的问题可以使用检查页面的IsPostBack属性来确定页面的当前状态,页面第一次请求时它的值为“false”。示例如下:


if(!Page.IsPostBack)

{

}


4.验证

在验证期间,将调用所有验证程序控件的Validate方法,此方法将设置各个验证程序控件和页面的IsValid属性。关于验证控件将在第4章做详细的讲解。

5.事件处理

在这个阶段,页面已经被完全装载且通过验证。ASP.NET将触发在上次回发后发生的所有事件。一般来说,ASP.NET事件有如下两类型:

1)立即反映事件。包括单击“提交”按钮或者其他按钮、图片区域、链接等,它们是调用JavaScript的_doPostBack()方法来触发一次回发。

2)变化事件。包括改变控件的选择或者文本框中的文本。如果控件的AutoPostBack属性设置为“true”,这些事件立即发生;否则它们将会在页面下次返回时发生。

例如,假设如下一个页面:


<%@Page Language="C#"AutoEventWireup="true"CodeBehind="Default.aspx.cs"

Inherits="_1_2.Default"%>

<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

<title></title>

</head>

<body>

<form id="form1"runat="server">

<div>

<asp:TextBox ID="txt_Text"runat="server"EnableViewState="false"></asp:Label>

<br/>

<asp:Button ID="bt_Test"runat="server"Text="测试按钮"onclick="bt_Test_Click"/>

</div>

</form>

</body>

</html>


当修改完“txt_Text”文本框里的文本后,单击“测试按钮”。这时ASP.NET会触发如下事件(事件的先后顺序如下):Page.Init、Page.Load、TextBox.TextChanged、Button.Click(bt_Test_Click)、Page.PreRender、Page.Unload。

为了让你能够更好地掌握这些事件的触发,下面将通过一个示例来模拟这些事件的发生。

6.呈现

在呈现之前,会针对该页和所有控件保存视图状态。在呈现阶段中,页会针对每个控件调用Render方法,它会提供一个文本编写器,用于将控件的输出写入页面的Response属性的OutputStream中。在这个阶段,页面和控件对象仍然可用,因此你可以执行最终的步骤,如在视图状态中保存额外的信息等。除此之外,一些数据绑定工作还有可能在呈现阶段之后发生。

7.清除

在页面生命周期的最后阶段,页面呈现为HTML。页面呈现后,真正的清除开始并触发Page.Unload事件。这时,页面对象仍然可以用,但是最终的HTML已经被呈现且不可修改。

其实,.NET Framework有一个垃圾回收器,它会周期性地释放不再引用的对象所占用的内存。如果要释放任意非托管的资源,要确保在清除阶段显式地将其释放。当垃圾收集器回收页面时,Page.Disposed事件被触发,对于网页来说,一切都已经进行完毕。

1.3.4 用程序来演示ASP.NET页面生命周期

为了能够加深对页面生命周期的理解,接下来将通过一个模拟示例来演示ASP.NET页面生命周期。需要注意的是,这个示例不会解释验证。

先创建一个名为“1-2”的Web应用程序项目,然后在该项目里添加一个页面Default.aspx。页面设计代码如代码清单1-7所示。

代码清单1-7 Default.aspx


<%@Page Language="C#"AutoEventWireup="true"CodeBehind="Default.aspx.cs"

Inherits="_1_2.Default"%>

<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

<title></title>

</head>

<body>

<form id="form1"runat="server">

<div>

<asp:Label ID="lb_message"runat="server"EnableViewState="false"></asp:Label>

<br/>

<asp:Button ID="bt_Test"runat="server"Text="测试按钮"onclick="bt_Test_Click"/>

</div>

</form>

</body>

</html>


在这里需要说明的是,之所以把“lb_message”控件的EnableViewState属性设置为“false”,是因为这样才能够保证每次回送页面时文本被清除,并且显示的文本只响应最近的处理。如果将EnableViewState属性设置为“true”,列表会随着每次回送而增长,显示从第一次请求页面开始所发生的一切活动。

页面设计好之后,就可以在Default.aspx.cs文件里添加响应的模拟事件了。见代码清单1-8。

代码清单1-8 Default.aspx.cs


using System;

using System.Collections.Generic;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

namespace_1_2

{

public partial class Default:System.Web.UI.Page

{

protected void Page_Load(object sender, EventArgs e)

{

this.lb_message.Text+="Page.Load事件触发。<br/>";

if(Page.IsPostBack)

{

this.lb_message.Text+="——Page.IsPostBack为真——<br/>";

}

}

private void Page_Init(object sender, EventArgs e)

{

this.lb_message.Text+="Page.Init事件触发。<br/>";

}

private void Page_PreRender(object sender, EventArgs e)

{

this.lb_message.Text+="Page.PreRender事件触发。<br/>";

}

private void Page_Unload(object sender, EventArgs e)

{

this.lb_message.Text+="Page.Unload事件触发。<br/>";

}

protected void bt_Test_Click(object sender, EventArgs e)

{

this.lb_message.Text+="测试按钮事件触发。<br/>";

}

}

}


在ASP.NET中,虽然页面事件处理程序为private,是可以接受的,但通常都把事件处理程序写成protected方式的,这是为了保持一致和简单。

按“Ctrl+F5”键运行程序,如图1-24所示。

当单击“测试按钮”后,将触发回送和bt_Test_Click事件,即使这个事件引起回送,Page.Init和Page.Load还是首先被触发,如图1-25所示。

figure_0063_0043

图 1-24 首次运行页面

figure_0063_0044

图 1-25 单击“测试按钮”运行的页面