MVC页面声命周期
开篇:上一篇我们了解了所谓的请求处理管道,在众多的事件中微软开放了19个重要的事件给我们,我们可以注入一些自定义的业务逻辑实现应用的个性化设计。本篇,我们来看看WebForm模式下的页面生命周期。
(1)Part 1:前奏
(2)Part 2:核心
(3)Part 3:管道
(4)Part 4:WebForm页面生命周期
(5)Part 5:MVC页面声命周期
在前面对于请求处理管道的介绍中,我们已经了解了一个ASP.NET WebForm页面请求事件的整体流程。那么,在其中一个最重要的部分就是ASP.NET Page页面,但是我们并没有对其进行详细讨论。因此,我们在此深入地了解一下ASP.NET页面事件。
每一个ASP.NET Page页都有2个部分:一个部分是在浏览器中进行显示的部分,它包含了HTML标签、viewstate形式的隐藏域 以及 在HTML input中的数据。当这个页面被提交到服务器时,这些HTML标签会被创建到ASP.NET控件,并且viewstate还会和表单数据绑定在一起。另一个部分是在xxx.cs文件中的进行业务逻辑操作的部分,一旦你在后置代码中得到所有的服务器控件,你可以执行和写入你自己的逻辑并呈现给客户浏览器。
其中,后台代码类是前台页面类的父类,前台页面类则是后台代码类的子类。这一点,可以通过查看每个aspx文件中的头部,我们都会看到以下的一句代码:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="FirstPage.aspx.cs" Inherits="WebFormDemo.FirstPage" %>
其中CodeBehind这个属性定义了此aspx页面的专属后台代码文件的名称,而Inherits这个属性则定义了此aspx页面所要继承的父类的名称(这也可以简单地说明,aspx页面会单独生成一个类,与后台代码类不重合在一起)。因此,aspx.cs就是aspx的后置处理代码,负责处理aspx中<%%>和runat="server"的内容。
现在这些HTML控件会作为ASP.NET控件存活在服务器上,ASP.NET会触发一系列的事件,我们也可以在这些事件中注入自定义逻辑代码。根据你想要执行什么样的任务/逻辑,我们需要将逻辑合理地放入这些事件之中。
TIP:大部分的开发者直接使用Page_Load来干所有的事情,但这并不是一个好的思路。因此,无论是填充控件、设置ViewState还是应用主题等所有发生在页面加载中的所有事情。因此,如果我们能够在合适的事件中放入逻辑,那么毫无疑问我们代码将会干净很多。
顺序 | 事件名称 | 控件初始化 | ViewState可用 | 表单数据可用 | 什么逻辑可以写在这里? |
1 | Init | No | No | No | 注意:你可以通过使用ASP.NET请求对象访问表单数据等,但不是通过服务器控件。 动态地创建控件,如果你一定要在运行时创建;任何初始化设置;母版页及其设置。在这部分中我们没有获得viewstate、提交的数据值及已经初始化的控件。 |
2 | Load View State | Not guaranteed | Yes | Not guaranteed | 你可以访问View State及任何同步逻辑,你希望viewstate被推到后台代码变量可以在这里完成。 |
3 | PostBackdata | Not guaranteed | Yes | Yes | 你可以访问表单数据。任何逻辑,你希望表单数据被推到后台代码变量可以在这里完成。 |
4 | Load | Yes | Yes | Yes | 在这里你可以放入任何你想操作控件的逻辑,如从数据库填充combox、对grid中的数据排序等。这个事件,我们可以访问所有控件、viewstate、他们发送过来的值。 |
5 | Validate | Yes | Yes | Yes | 如果你的页面有验证器或者你想为你的页面执行验证,那就在这里做吧。 |
6 | Event | Yes | Yes | Yes | 如果这是通过点击按钮或下拉列表的改变的一个回发,相关的事件将被触发。与事件相关的任何逻辑都可以在这里执行。 PS:这个事件想必很多使用WebForm的开发人员都很常用吧,是否记得那些Button1_Click(Object sender,EventArgs e)? |
7 | Pre-render | Yes | Yes | Yes | 如果你想对UI对象做最终的修改,如改变属性结构或属性值,在这些控件保存到ViewState之前。 |
8 | Save ViewState | Yes | Yes | Yes | 一旦对服务器控件的所有修改完成,将会保存控件数据到View State中。 |
9 | Render | Yes | Yes | Yes | 如果你想添加一些自定义HTML到输出,可以在这里完成。 |
10 | Unload | Yes | Yes | Yes | 任何你想做的清理工作都可以在这里执行。 |
前面我们简单地了解了一下ASP.NET Page的页面事件,现在我们来通过Reflector反编译一下一个demo程序集,来感受一下ASP.NET Page的页面生命周期。
(1)假如我们有以下的名为Index的一个aspx页面:
(2)Index所对应的后台代码如下:
这里,我们来重点关注一下这个方法:我们可以通过写入以下代码,然后在aspx中<% GetDllInfo(); %>调用,它显示了我们这个ASP.NET项目所属的程序集在哪个位置?
protected void GetDllInfo() { Response.Write("页面类名称:"+this.GetType() + "<br/>"); Response.Write("程序集地址:"+this.GetType().Assembly.Location + "<br/>"); Response.Write("父类的名称:"+this.GetType().BaseType + "<br/>"); Response.Write("程序集地址:"+this.GetType().BaseType.Assembly.Location + "<br/>"); }
浏览页面,会显示以下结果:通过下图可以看到,我们的Index这个页面会生成一个ASP.index_aspx的类,其父类是Index。
通过上面显示的路径找到dll,并拖到反编译工具(ILSpy或者Reflector,前者开源免费,后者已经收费,但天朝,你懂的。)进行查看。通过下图可以看出,页面类aspx是后台代码类所绑定的子类,它的名称是aspx文件名加上“_aspx”后缀。因此,这里也就解释了为什么在aspx中要访问的方法必须是public和protected的访问修饰符才可以。
从上面可以看出,页面类继承自后置代码类,而后置代码类又继承自Page类。我们从上一篇管道可以知道,在请求处理管道的第8个事件中创建了Page类对象,那么我们去看看Page类。
Page类继承自TemplateControl,顾名思义,Page类是否就是一个模板控件呢?再看看TemplateControl类:
果不其然,其父类是Control类,Page就是一个封装过的大控件!那么,我们在Page中拖的那些runat="server"的服务器控件,又是保存在哪里的呢?
原来,在Control父类中,有一个Controls的属性,它是一个控件的集合:Page中的所有控件,都会存在于这个集合中。
从上一篇请求处理管道中,我们知道在第11和第12个事件之间会调用Page类对象的ProcessRequest方法进入页面生命周期。那么我们来看看这个ProcessRequest方法:
从图中可以看出,这个方法中首先通过调用页面类对象(我们请求的页面都是继承于Page类的)重写的FrameworkInitialize方法开始我们经常听到的构造控件树的过程。下面我们转到index_aspx这个页面类重写的FrameworkInitialize方法中取看看是否是进行了构造页面控件树的操作:
看到这里,我们不由地想问,什么是页面控件树?在一个aspx页面中,runat="server"的控件集合构成了如下图所示的一棵页面控件树,他们被一一实例化,并依据层级关系存储到了controls集合中。
了解了什么是页面控件树,现在我们看看是如何来构造这棵树的,通过查看BuildControlTree方法,发现它调用了多个名为BuildControlX的方法,依次实例化我们页面中所需的控件,并添加到控件集合中(这里其实是将这些服务器控件作为子控件添加到页面(页面本身就是一个大的控件)中,在树形结构中Page就是一个根节点,而那些Page中的控件则是Page的孩子节点)。
那么,这些BuildControlX(X代表数字)方法又在做些什么事呢?我们可以通过查看一个BuildControl方法,看看如何打造HtmlForm的:
可以看出,在构造HtmlForm控件的过程中,不仅为其设置了ID(_ctrl.ID="formIndex"),还为其指定了渲染方法(通过设置委托_ctrl.SetRenderMethodDelegate())。又因为我们拖了一个TextBox和Button在其中,于是在实例化HtmlForm这个控件的途中,又去实例化TextBox和Button对象,并将其作为HtmlForm的子节点,形成一个层级关系。
现在重新回到Page类的ProcessRequest方法中,在创建页面控件树完成之后,开始进入一个ProcessRequestMain方法,这个方法则真正地开启了页面生命周期之门。
private void ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.ProcessRequestMain(includeStagesBeforeAsyncPoint, includeStagesAfterAsyncPoint); ...... }
我们经常在Page_Load方法中使用Page.IsPostBack属性来判断请求是否是回发,那么它是在哪里设置的呢?原来,在ProcessRequestMain方法中:
接下来就是初始化操作了,初始化操作分为了三个阶段:预初始化、初始化(使用递归方式)、初始化完成。
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.PerformPreInit(); ...... this.InitRecursive(); ...... this.OnInitComplete(); ...... }
预初始化主要利用App_Themes目录中的内容进行初始化主题,并应用模板页。
这里我们主要看看初始化操作,通过查看源代码,可以看出,该方法通过递归调用子控件的初始化方法,完成了控件集合中所有控件的初始化操作。
internal virtual void InitRecursive(Control namingContainer) { ...... int count = this._controls.Count; for (int i = 0; i < count; i++) { Control control = this._controls[i]; control.UpdateNamingContainer(namingContainer); if (((control._id == null) && (namingContainer != null)) && !control.flags[0x40]) { control.GenerateAutomaticID(); } control._page = this.Page; control.InitRecursive(namingContainer); } ...... }
再看看初始化方法中都做了哪些初始化操作,细细一看,原来就是为其动态地生成一个ID(control.GenerateAutomaticID()),然后将该控件的page指针指向当前Page页等。PreLoad 预加载在 Load 事件之前对页或控件执行处理,
初始化完成之后,ASP.NET会通过IsPostBack判断是否是第一次请求,如果不是,那么首先会加载ViewState并对回发的数据进行处理。
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { if(this.IsPostBack) { ...... this.LoadAllState(); ...... this.ProcessPostData(this._requestValueCollection, true); ...... } }
至于ViewState是什么?又不了解的朋友,可以浏览我的另一篇博文:ASP.NET WebForm温故知新:ViewState,这里就不再赘述。这里LoadAllState方法主要是将隐藏域中的_VIEWSTATE通过解码获取控件的状态与数据信息,而ProcessPostData方法则是进行了两个部分的操作:一是将刚刚获取到的各个控件的状态与数据信息填充到页面控件树中所对应的各个控件中去,二是对比控件状态是否发生了改变?比如被点击了?被触发了某个事件(例如TextChanged、SelectedIndexChanged等)?如有触发事件,则把需要触发事件的控件放到一个集合当中去。
处理完ViewState后,就开始进行正式地加载操作了,如下代码所示:
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.OnPreLoad(EventArgs.Empty); ...... this.LoadRecursive(); ...... }
在正式加载过程中也分为了两个部分,一个是PreLoad预加载,另外一个则是重头戏Load加载(通过方法名可以推断,该方法是通过递归方式调用加载的)。首先,调用了OnPreLoad方法进行预加载操作,如果我们需要在 Load 事件之前对页或控件(这时页面控件树已经构造完成)执行处理,就可以使用该事件。通过查看源代码,在PreLoad方法中会遍历一个PreLoad事件集合(我们可以自定义注入我们想要的事件),然后依次执行委托所持有的事件。
protected virtual void OnPreLoad(EventArgs e) { EventHandler handler = (EventHandler) base.Events[EventPreLoad]; if (handler != null) { handler(this, e); } }
PreLoad之后就是重头戏,也是我们最为熟悉的Load了,在调用LoadRecursive()方法进入Load事件。
internal virtual void LoadRecursive() { if (this._controlState < ControlState.Loaded) { if (this.AdapterInternal != null) { this.AdapterInternal.OnLoad(EventArgs.Empty); } else { this.OnLoad(EventArgs.Empty); } } if (this._controls != null) { string errorMsg = this._controls.SetCollectionReadOnly("Parent_collections_readonly"); int count = this._controls.Count; for (int i = 0; i < count; i++) { this._controls[i].LoadRecursive(); } this._controls.SetCollectionReadOnly(errorMsg); } if (this._controlState < ControlState.Loaded) { this._controlState = ControlState.Loaded; } }
从上面可以看出:ASP.NET页面首先调用自身的OnLoad方法以引发自身的Load事件,接着递归调用 Contorls 集合中各个控件的OnLoad方法以引发它们的Load事件。那么,我们在页面后置代码类中经常使用的Page_Load事件方法是在哪里调用的呢?相信我们都有了答案,就在页面自身的OnLoad方法中。
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { if(this.IsPostBack) { ...... this.ProcessPostData(this._leftoverPostData, false); ...... this.RaiseChangedEvents(); ...... this.RaisePostBackEvent(this._requestValueCollection); ...... } }
加载结束后,会经历第二次的处理回发数据的事件。那么,我们不禁会问,为何还要第二次进行ProcessPostData方法的调用,我们刚刚不是都已经对ViewState进行了解码并对应到了对应控件树中的控件了嘛?这里,我们首先看看下面一段代码:
protected void Page_Load(object sender, EventArgs e) { if (IsPostBack) { TextBox txtTest = new TextBox(); txtTest.Text = "动态创建的TextBox"; formIndex.Controls.Add(txtTest); } }
假如我们要在Page_Load事件中动态地为Form添加一个TextBox控件,那么之前的页面控件树就发生了改变,所以,这里需要进行第二次的ProcessPostData方法,现在豁然开朗了吧。
在第二次处理回发数据之后,会调用RaiseChangedEvents方法触发控件状态改变事件响应方法,例如TextBox_TextChanged、DropDownList_SelectedIndexChanged事件(这些事件中不包括Button_Click这种回发事件)等。查看源代码,通过遍历状态改变了的控件的集合(在第一次进行ProcessPostData时会检查控件的状态是否发生了改变,如果改变了就添加到一个集合中)
internal void RaiseChangedEvents() { if (this._changedPostDataConsumers != null) { for (int i = 0; i < this._changedPostDataConsumers.Count; i++) { Control control = (Control) this._changedPostDataConsumers[i]; if (control != null) { IPostBackDataHandler postBackDataHandler = control.PostBackDataHandler; if (((control == null) || control.IsDescendentOf(this)) && ((control != null) && (control.PostBackDataHandler != null))) { postBackDataHandler.RaisePostDataChangedEvent(); } } } } }
在处理完状态改变事件响应方法后,会调用RaisePostBackEvent方法触发例如按钮控件的回发事件,例如Button_Click回发事件。
private void RaisePostBackEvent(NameValueCollection postData) { if (this._registeredControlThatRequireRaiseEvent != null) { this.RaisePostBackEvent(this._registeredControlThatRequireRaiseEvent, null); } else { string str = postData["__EVENTTARGET"]; bool flag = !string.IsNullOrEmpty(str); if (flag || (this.AutoPostBackControl != null)) { Control control = null; if (flag) { control = this.FindControl(str); } if ((control != null) && (control.PostBackEventHandler != null)) { string eventArgument = postData["__EVENTARGUMENT"]; this.RaisePostBackEvent(control.PostBackEventHandler, eventArgument); } } else { this.Validate(); } } }
通过查看代码,发现通过回传的表单数据中根据__EVENTTARGET与__EVENTARGUMENT进行事件的触发。我们可以通过查看ASP.NET生成的前端HTML代码看到这两个参数:下图是一个设置为AutoPostBack的DropDownList控件,可以发现回发事件都是通过调用_doPostBack这个js代码进行表单的submit,而表单中最重要的两个参数就是eventTarget和eventArgument。
通过浏览器提供的开发人员工具查看数据请求报文,可以看到除了提交form中的input外,还提交了ASP.Net WebForm预置的一些隐藏字段,而这些隐藏字段则是WebForm为我们提供便利的基础。比如EventTarget则记录刚刚提交给服务器的是哪个服务器控件。
事件触发完成之后,加载操作就完成了,这时会调用OnLoadComplete方法进行相关的事件,这里就不再赘述了。
这一阶段就进入了页面生命周期的尾巴,开始最终页面的渲染流程:
private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) { ...... this.PreRenderRecursiveInternal(); ...... this.PerformPreRenderComplete(); ...... this.SaveAllState(); ...... this.OnSaveStateComplete(EventArgs.Empty); ...... this.RenderControl(this.CreateHtmlTextWriter(this.Response.Output)); ...... }
这里我们主要看看PreRender、SaveState和Render三个事件。
既然已经进入了页面渲染阶段,为何还要有一个PreRender预呈现阶段?通过查找资料,我们发现微软这么设计是为了给开发者提供一个最后一次更改页面控件状态或数据的机会,也就说:你可以再在这里注入一个逻辑,最后一次改变控件值,或者统一地改变控件状态为某个指定状态。
然后就是SaveState,这个很好理解,也就说:刚刚给了你最后一次更改的机会结束后,我就要保存最终的ViewState了。这里需要注意的是:服务器在向浏览器返回html之前,对ViewState中的内容是进行了Base64编码的;
最后就是Render,进行最终的页面呈现了,换句话说:就是拼接形成HTML字符串。在这个阶段,Page 对象会遍历页面控件树并在每个控件上递归地调用此方法。所有 ASP.NET Web 服务器控件都有一个用于写出发送给浏览器的控件标记的 Render 方法。通过对源代码进行追踪,可以看到以下代码:
internal void RenderChildrenInternal(HtmlTextWriter writer, ICollection children) { if ((this.RareFields != null) && (this.RareFields.RenderMethod != null)) { writer.BeginRender(); this.RareFields.RenderMethod(writer, this); writer.EndRender(); } else if (children != null) { foreach (Control control in children) { control.RenderControl(writer); } } }
在Render过程中,会判断当前控件是否含有子控件集合,如果有,那么遍历各个子控件的Render方法进行HTML的渲染。可以想象,从页面控件树的根节点调用Render方法,会依次递归调用其所有子节点的Render方法,从而得到一个完整的HTML代码。
那么,Render方法结束后,生成的HTML代码保存到了哪里呢?原来,Render方法的输出会写入Page类对象的 Response 属性的 OutputStream 中,这就是最终的输出流作为响应报文通过HTTP协议返回给浏览器端了。
自此,狭义上的页面生命周期就结束了,但广义上的页面声明周期事件还未结束,还会经历一个UnLoad事件,该事件首先针对每个控件发生,继而针对该页发生。在控件中,使用该事件对特定控件执行最后清理,如关闭控件特定数据库连接。对于页自身,使用该事件来执行最后清理工作,如:关闭打开的文件和数据库连接,或完成日志记录或其他请求特定任务。总而言之,Unload就是进行最后的清理工作,释放资源。
一篇文章下来,已耗费了好多时间,如果你觉得对你有用,那就麻烦点个推荐吧。如果你觉得本文很烂,那点个反对也是可以的。后面Part 5会探秘ASP.NET MVC的页面生命流程,今天就此停笔,谢谢!
(1)农村出来的大学生,《ASP.NET网页请求处理全过程(反编译)》:http://www.cnblogs.com/poorpan/archive/2011/09/25/2190308.html
(2)我自己,《【翻译】ASP.NET应用程序和页面声明周期》:http://www.cnblogs.com/edisonchou/p/3958305.html
(3)Shivprasad koirala,《ASP.NET Application and Page Life Cycle》:http://www.codeproject.com/Articles/73728/ASP-NET-Application-and-Page-Life-Cycle
(4)碧血轩,《ASP.NET页面生命周期》:http://www.cnblogs.com/xhwy/archive/2012/05/20/2510178.html
(5)木宛城主,《ASP.NET那点不为人知的事儿》:http://www.cnblogs.com/OceanEyes/archive/2012/08/13/aspnetEssential-1.html
(6)千年老妖,《ASP.NET页面生命周期》:http://www.cnblogs.com/hanwenhuazuibang/archive/2013/04/07/3003289.html
(7)MSDN,《Page事件》:http://msdn.microsoft.com/zh-cn/library/system.web.ui.page_events(v=vs.80).aspx
PS:背景音乐 from 张国荣 电影英雄本色中的插曲 《当年情》