ASP.Net请求处理机制初步探索之旅 - Part 4 WebForm页面生命周期

开篇:上一篇咱们了解了所谓的请求处理管道,在众多的事件中微软开放了19个重要的事件给咱们,咱们能够注入一些自定义的业务逻辑实现应用的个性化设计。本篇,咱们来看看WebForm模式下的页面生命周期。html

(1)Part 1:前奏前端

(2)Part 2:核心web

(3)Part 3:管道数据库

(4)Part 4:WebForm页面生命周期浏览器

(5)Part 5:MVC页面声命周期服务器

1、ASP.Net Page的两个重要部分

  在前面对于请求处理管道的介绍中,咱们已经了解了一个ASP.NET WebForm页面请求事件的总体流程。那么,在其中一个最重要的部分就是ASP.NET Page页面,可是咱们并无对其进行详细讨论。所以,咱们在此深刻地了解一下ASP.NET页面事件。app

  每个ASP.NET Page页都有2个部分:一个部分是在浏览器中进行显示的部分,它包含了HTML标签、viewstate形式的隐藏域 以及 在HTML input中的数据。当这个页面被提交到服务器时,这些HTML标签会被建立到ASP.NET控件,而且viewstate还会和表单数据绑定在一块儿。另外一个部分是在xxx.cs文件中的进行业务逻辑操做的部分,一旦你在后置代码中获得全部的服务器控件,你能够执行和写入你本身的逻辑并呈现给客户浏览器。ide

  其中,后台代码类是前台页面类的父类,前台页面类则是后台代码类的子类。这一点,能够经过查看每一个aspx文件中的头部,咱们都会看到如下的一句代码:工具

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="FirstPage.aspx.cs" Inherits="WebFormDemo.FirstPage" %>

  其中CodeBehind这个属性定义了此aspx页面的专属后台代码文件的名称,而Inherits这个属性则定义了此aspx页面所要继承的父类的名称(这也能够简单地说明,aspx页面会单独生成一个类,与后台代码类不重合在一块儿)。所以,aspx.cs就是aspx的后置处理代码,负责处理aspx中<%%>和runat="server"的内容。post

  如今这些HTML控件会做为ASP.NET控件存活在服务器上,ASP.NET会触发一系列的事件,咱们也能够在这些事件中注入自定义逻辑代码。根据你想要执行什么样的任务/逻辑,咱们须要将逻辑合理地放入这些事件之中。

TIP:大部分的开发者直接使用Page_Load来干全部的事情,但这并非一个好的思路。所以,不管是填充控件、设置ViewState仍是应用主题等全部发生在页面加载中的全部事情。所以,若是咱们可以在合适的事件中放入逻辑,那么毫无疑问咱们代码将会干净不少。

2、ASP.Net Page的页面事件流程

顺序 事件名称 控件初始化 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 任何你想作的清理工做均可以在这里执行。

3、反编译探秘ASP.Net Page页面生命周期

  前面咱们简单地了解了一下ASP.NET Page的页面事件,如今咱们来经过Reflector反编译一下一个demo程序集,来感觉一下ASP.NET Page的页面生命周期。

3.1 准备一个ASP.NET项目

  (1)假如咱们有如下的名为Index的一个aspx页面:

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="headIndex" runat="server">
    <title>Index页</title>
</head>
<body>
    <form id="formIndex" runat="server">
    <div>
        哈哈,我是ASP.Net WebForm,下面看个人表演。
        <br />
        <%
            for (int i = 0; i < 5; i++)
            {
                Response.Write("I am a webform page.<br/>");
            }
        %>
        <br />
        <%= GetServerTime() %>
        <br />
        <asp:TextBox ID="txtDateTime" runat="server"></asp:TextBox>
        <asp:Button ID="btnGetTime"
            runat="server" Text="获取时间" onclick="btnGetTime_Click" />
        <br />
        <% GetDllInfo(); %>
    </div>
    </form>
</body>
</html>
View Code

  (2)Index所对应的后台代码以下:

namespace PageLifeCycleDemo
{
    public partial class Index : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected string GetServerTime()
        {
            string result = "服务器时间:" + DateTime.Now.ToString();
            return result;
        }

        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/>");
        }

        protected void btnGetTime_Click(object sender, EventArgs e)
        {
            txtDateTime.Text = DateTime.Now.ToString();
        }
    }
}
View Code

  这里,咱们来重点关注一下这个方法:咱们能够经过写入如下代码,而后在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。

3.2 反编译生成的临时程序集

  ①将DLL拖到Reflector中进行查看源代码

  经过上面显示的路径找到dll,并拖到反编译工具(ILSpy或者Reflector,前者开源免费,后者已经收费,但天朝,你懂的。)进行查看。经过下图能够看出,页面类aspx是后台代码类所绑定的子类,它的名称是aspx文件名加上“_aspx”后缀。所以,这里也就解释了为何在aspx中要访问的方法必须是public和protected的访问修饰符才能够。

  ②一个大型Control:Page类

  从上面能够看出,页面类继承自后置代码类,然后置代码类又继承自Page类。咱们从上一篇管道能够知道,在请求处理管道的第8个事件中建立了Page类对象,那么咱们去看看Page类。

  Page类继承自TemplateControl,顾名思义,Page类是否就是一个模板控件呢?再看看TemplateControl类:

  果不其然,其父类是Control类,Page就是一个封装过的大控件!那么,咱们在Page中拖的那些runat="server"的服务器控件,又是保存在哪里的呢?

  原来,在Control父类中,有一个Controls的属性,它是一个控件的集合:Page中的全部控件,都会存在于这个集合中。

  ③页面生命周期的入口:Page类的ProcessRequest方法

  从上一篇请求处理管道中,咱们知道在第11和第12个事件之间会调用Page类对象的ProcessRequest方法进入页面生命周期。那么咱们来看看这个ProcessRequest方法:

  从图中能够看出,这个方法中首先经过调用页面类对象(咱们请求的页面都是继承于Page类的)重写的FrameworkInitialize方法开始咱们常常听到的构造控件树的过程。下面咱们转到index_aspx这个页面类重写的FrameworkInitialize方法中取看看是不是进行了构造页面控件树的操做:

  

  ④BuildControlTree:构造页面控件树

  看到这里,咱们不禁地想问,什么是页面控件树?在一个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的子节点,造成一个层级关系。

  ⑤肯定IsPostBack:是否第一次请求该页面

  如今从新回到Page类的ProcessRequest方法中,在建立页面控件树完成以后,开始进入一个ProcessRequestMain方法,这个方法则真正地开启了页面生命周期之门。

private void ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint)
{
    ......                     
   this.ProcessRequestMain(includeStagesBeforeAsyncPoint, includeStagesAfterAsyncPoint);
    ......
}

  咱们常常在Page_Load方法中使用Page.IsPostBack属性来判断请求是不是回发,那么它是在哪里设置的呢?原来,在ProcessRequestMain方法中:

  ⑥初始化操做:PreInit-->Init-->InitComplete

  接下来就是初始化操做了,初始化操做分为了三个阶段:预初始化、初始化(使用递归方式)、初始化完成。

    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 事件以前对页或控件执行处理,

  ⑦加载操做:(LoadState-->ProcessPostData-->)PreLoad-->Load-->

(ProcessPostData-->RaiseChangedEvents-->RaisePostBackEvent-->)LoadComplete  

  • 首先看看(LoadState-->ProcessPostData)

  初始化完成以后,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等)?若有触发事件,则把须要触发事件的控件放到一个集合当中去。

  • 再来看看PreLoad-->Load

  处理完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方法中。

  • 二次经历(ProcessPostData)
        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-->RaisePostBackEvent)

  在第二次处理回发数据以后,会调用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方法进行相关的事件,这里就再也不赘述了。

  • 页面渲染 PreRender-->PreRenderComplete-->SaveState-->SaveStateComplete-->Render

  这一阶段就进入了页面生命周期的尾巴,开始最终页面的渲染流程:

        private void ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint)
        {
            ......
            this.PreRenderRecursiveInternal();
            ......
            this.PerformPreRenderComplete();
            ......
            this.SaveAllState();
            ......
            this.OnSaveStateComplete(EventArgs.Empty);
            ......
            this.RenderControl(this.CreateHtmlTextWriter(this.Response.Output));
            ......
        }

  这里咱们主要看看PreRenderSaveStateRender三个事件。

  既然已经进入了页面渲染阶段,为什么还要有一个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事件,该事件首先针对每一个控件发生,继而针对该页发生。在控件中,使用该事件对特定控件执行最后清理,如关闭控件特定数据库链接。对于页自身,使用该事件来执行最后清理工做,如:关闭打开的文件和数据库链接,或完成日志记录或其余请求特定任务。总而言之,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 张国荣 电影英雄本色中的插曲 《当年情

 

相关文章
相关标签/搜索