一个人建网站,中国企业网是什么级别媒体,巴中市做网站,软件外包开发开篇#xff1a;上一篇我们了解了所谓的请求处理管道#xff0c;在众多的事件中微软开放了19个重要的事件给我们#xff0c;我们可以注入一些自定义的业务逻辑实现应用的个性化设计。本篇#xff0c;我们来看看WebForm模式下的页面生命周期。 #xff08;1#xff09;Par…开篇上一篇我们了解了所谓的请求处理管道在众多的事件中微软开放了19个重要的事件给我们我们可以注入一些自定义的业务逻辑实现应用的个性化设计。本篇我们来看看WebForm模式下的页面生命周期。 1Part 1前奏 2Part 2核心 3Part 3管道 4Part 4WebForm页面生命周期 5Part 5MVC页面声命周期 一、ASP.Net Page的两个重要部分 在前面对于请求处理管道的介绍中我们已经了解了一个ASP.NET WebForm页面请求事件的整体流程。那么在其中一个最重要的部分就是ASP.NET Page页面但是我们并没有对其进行详细讨论。因此我们在此深入地了解一下ASP.NET页面事件。 每一个ASP.NET Page页都有2个部分一个部分是在浏览器中进行显示的部分它包含了HTML标签、viewstate形式的隐藏域 以及 在HTML input中的数据。当这个页面被提交到服务器时这些HTML标签会被创建到ASP.NET控件并且viewstate还会和表单数据绑定在一起。另一个部分是在xxx.cs文件中的进行业务逻辑操作的部分一旦你在后置代码中得到所有的服务器控件你可以执行和写入你自己的逻辑并呈现给客户浏览器。 其中后台代码类是前台页面类的父类前台页面类则是后台代码类的子类。这一点可以通过查看每个aspx文件中的头部我们都会看到以下的一句代码
% Page LanguageC# AutoEventWireuptrue CodeBehindFirstPage.aspx.cs InheritsWebFormDemo.FirstPage % 其中CodeBehind这个属性定义了此aspx页面的专属后台代码文件的名称而Inherits这个属性则定义了此aspx页面所要继承的父类的名称这也可以简单地说明aspx页面会单独生成一个类与后台代码类不重合在一起。因此aspx.cs就是aspx的后置处理代码负责处理aspx中%%和runatserver的内容。 现在这些HTML控件会作为ASP.NET控件存活在服务器上ASP.NET会触发一系列的事件我们也可以在这些事件中注入自定义逻辑代码。根据你想要执行什么样的任务/逻辑我们需要将逻辑合理地放入这些事件之中。 TIP大部分的开发者直接使用Page_Load来干所有的事情但这并不是一个好的思路。因此无论是填充控件、设置ViewState还是应用主题等所有发生在页面加载中的所有事情。因此如果我们能够在合适的事件中放入逻辑那么毫无疑问我们代码将会干净很多。 二、ASP.Net Page的页面事件流程
顺序事件名称控件初始化ViewState可用表单数据可用什么逻辑可以写在这里1InitNoNoNo注意你可以通过使用ASP.NET请求对象访问表单数据等但不是通过服务器控件。 动态地创建控件如果你一定要在运行时创建任何初始化设置母版页及其设置。在这部分中我们没有获得viewstate、提交的数据值及已经初始化的控件。2Load View StateNot guaranteedYesNot guaranteed你可以访问View State及任何同步逻辑你希望viewstate被推到后台代码变量可以在这里完成。3PostBackdataNot guaranteedYesYes你可以访问表单数据。任何逻辑你希望表单数据被推到后台代码变量可以在这里完成。4LoadYesYesYes在这里你可以放入任何你想操作控件的逻辑如从数据库填充combox、对grid中的数据排序等。这个事件我们可以访问所有控件、viewstate、他们发送过来的值。5ValidateYesYesYes如果你的页面有验证器或者你想为你的页面执行验证那就在这里做吧。6EventYesYesYes 如果这是通过点击按钮或下拉列表的改变的一个回发相关的事件将被触发。与事件相关的任何逻辑都可以在这里执行。 PS这个事件想必很多使用WebForm的开发人员都很常用吧是否记得那些Button1_Click(Object sender,EventArgs e) 7Pre-renderYesYesYes如果你想对UI对象做最终的修改如改变属性结构或属性值在这些控件保存到ViewState之前。8Save ViewStateYesYesYes一旦对服务器控件的所有修改完成将会保存控件数据到View State中。9RenderYesYesYes如果你想添加一些自定义HTML到输出可以在这里完成。10UnloadYesYesYes任何你想做的清理工作都可以在这里执行。三、反编译探秘ASP.Net Page页面生命周期 前面我们简单地了解了一下ASP.NET Page的页面事件现在我们来通过Reflector反编译一下一个demo程序集来感受一下ASP.NET Page的页面生命周期。
3.1 准备一个ASP.NET项目 1假如我们有以下的名为Index的一个aspx页面 html xmlnshttp://www.w3.org/1999/xhtml
head idheadIndex runatservertitleIndex页/title
/head
bodyform idformIndex runatserverdiv哈哈我是ASP.Net WebForm下面看我的表演。br /%for (int i 0; i 5; i){Response.Write(I am a webform page.br/);}%br /% GetServerTime() %br /asp:TextBox IDtxtDateTime runatserver/asp:TextBoxasp:Button IDbtnGetTimerunatserver Text获取时间 οnclickbtnGetTime_Click /br /% GetDllInfo(); %/div/form
/body
/html 2Index所对应的后台代码如下 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();}}
} 这里我们来重点关注一下这个方法我们可以通过写入以下代码然后在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的访问修饰符才可以。 ②一个大型ControlPage类 从上面可以看出页面类继承自后置代码类而后置代码类又继承自Page类。我们从上一篇管道可以知道在请求处理管道的第8个事件中创建了Page类对象那么我们去看看Page类。 Page类继承自TemplateControl顾名思义Page类是否就是一个模板控件呢再看看TemplateControl类 果不其然其父类是Control类Page就是一个封装过的大控件那么我们在Page中拖的那些runatserver的服务器控件又是保存在哪里的呢 原来在Control父类中有一个Controls的属性它是一个控件的集合Page中的所有控件都会存在于这个集合中。 ③页面生命周期的入口Page类的ProcessRequest方法 从上一篇请求处理管道中我们知道在第11和第12个事件之间会调用Page类对象的ProcessRequest方法进入页面生命周期。那么我们来看看这个ProcessRequest方法 从图中可以看出这个方法中首先通过调用页面类对象我们请求的页面都是继承于Page类的重写的FrameworkInitialize方法开始我们经常听到的构造控件树的过程。下面我们转到index_aspx这个页面类重写的FrameworkInitialize方法中取看看是否是进行了构造页面控件树的操作 ④BuildControlTree构造页面控件树 看到这里我们不由地想问什么是页面控件树在一个aspx页面中runatserver的控件集合构成了如下图所示的一棵页面控件树他们被一一实例化并依据层级关系存储到了controls集合中。 了解了什么是页面控件树现在我们看看是如何来构造这棵树的通过查看BuildControlTree方法发现它调用了多个名为BuildControlX的方法依次实例化我们页面中所需的控件并添加到控件集合中这里其实是将这些服务器控件作为子控件添加到页面页面本身就是一个大的控件中在树形结构中Page就是一个根节点而那些Page中的控件则是Page的孩子节点。 那么这些BuildControlX(X代表数字)方法又在做些什么事呢我们可以通过查看一个BuildControl方法看看如何打造HtmlForm的 可以看出在构造HtmlForm控件的过程中不仅为其设置了ID_ctrl.IDformIndex还为其指定了渲染方法通过设置委托_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);}......} 再看看初始化方法中都做了哪些初始化操作细细一看原来就是为其动态地生成一个IDcontrol.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方法中。 二次经历ProcessPostDataprivate 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));......} 这里我们主要看看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事件该事件首先针对每个控件发生继而针对该页发生。在控件中使用该事件对特定控件执行最后清理如关闭控件特定数据库连接。对于页自身使用该事件来执行最后清理工作如关闭打开的文件和数据库连接或完成日志记录或其他请求特定任务。总而言之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
3Shivprasad 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
7MSDN《Page事件》http://msdn.microsoft.com/zh-cn/library/system.web.ui.page_events(vvs.80).aspx
偶像的歌 PS背景音乐 from 张国荣 电影英雄本色中的插曲 《当年情》 作者周旭龙
出处http://edisonchou.cnblogs.com/
本文版权归作者和博客园共有欢迎转载但未经作者同意必须保留此段声明且在文章页面明显位置给出原文链接。