五、页面生存周期
现在回到第三个标题中讲到的内容,我们讲到了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。
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,但是我举了一些初学者常犯的错误和出现错误的原因,希望可以给大家带来启发。
查看本文来源