前言
ASP.NET页面运行时候,页面将经历一个生命周期,在生命周期中将执行一系列的处理步骤。包括初始化、实例化控件、还原和维护状态、运行时间处理程序代码以及进行呈现。熟悉页面生命周期非常重要,这样我们才能在生命周期的合适阶段编写代码。如果我们能在写代码的时候想着我们现在是在做生命周期的哪一步那将是非常好的。
你可能会说我不清楚还不是一样写代码,反正每次都在Page_load里面写代码 然后页面就出来了我管那么多干什么。所谓知其然如果能知其所以然岂不是更吊?我个人认为做ASP.NET B/S开发只要咱们熟悉ASP.NET页面生命周期,熟悉“请求-处理-响应“模式,对于一时半儿没有触及到的细节,等真正遇到那个点再去理解它也是毫不费力的。
几个代表性的问题
在开始的时候我们先思考几个问题,看看我们在描述完页面生命周期的时候,能不能回答上这几个问题
1.为什么在服务器端能通过this.textbox1.Text获取到用户提交过来的数据?
2.在Page_Load中Response.Write("hello")查看生成的html代码原文件,hello在哪里?为什么?
3.有一个服务器端的按钮,设置了点击事件,该点击事件什么时候执行?是先执行Page_Load事件还是先执行点击事件?
4.为什么在服务器端通过this.textbox1.Text设置值后,客户端就能显示出来?
看过我上一篇博客【深入ASP.NET原理系列】--ASP.NET请求管道、应用程序生命周期、整体运行机制 童鞋可能知道,ASP.NET应用程序周期中PreRequestHandlerExecute事件与PostRequestHandlerExecute事件之间就是我们的页面生命周期了,对于aspx页面就是一系列的打造页面控件树,触发各种页面时间,对于一般处理程序ashx就是直接执行咱们开发者写的ProcessRequest方法了,对于MVC应用程序就是创建控制器工厂,创建控制器对象,调用Action那一套了。
下面主要讲述的就是ASP.NET WebForm中的页面的生命周期了。
我们用反编译工具查看Page类的ProcessRequest方法可以看见先调用了FrameworkInitialize; FrameworkInitialize里面就是打造了页面控件树,然后调用了ProcessRequestMain,就开始了执行整个页面生命周期了(其实就是调用了一系列的事件方法)(可能部分图看不见右边,可在新标签页中打开图片)
1.打造页面控件树
FrameworkInitialize内部调用了_buildControlTree()方法
上图中左边是前台页面的代码,右边是对应 生成的打造控件树的代码。中间截取的是生成表单那一部分的代码。
下面看一张原理图
浏览器的DOM树是根据Html标签生成一个C语言的DOM树,而ASP.NET服务器端是用C#打造的一个控件树,也是按照DOM结构打造的。本质是一样。服务器端所有东西都加到页面对象的控件集合中去了。标签在服务器端有对应的控件对象的时候就用控件对象,没有的时候就使用LiteralControl进行封装。不管是服务器控件还是字符串标签(没有runat="server"的标签)都以控件对象的方式存在前台页面类的控件集合里面。好处就是生成前台页面的html代码的时候,只需要遍历控件集合里面的每一个控件对象的RenderControl方法,每一个控件都会调用自己的Render方法生成对应的Html字符串。那么所有控件的生成的html字符串就还原成一个页面的html代码了。
2.触发PerformPreInit事件
在所有初始化之前初始化了这个事件,这个事件主要是初始化了主题,初始化了母版页
3.触发InitRecursive事件
4.触发LoadAllState()事件
加载页面状态解析ViewState,将页面表单中的ViewState进行反Base64编码,反序列化,存在页面的ViewState属性中
5.触发ProcessPostData(this._requestValueCollection, true)事件
主要做了两件事
1)将表单里提交过来的控件数据设置给页面对象的控件树中对应控件的属性(给前面打造的控件树里面控件给值),这样在服务器端就可以拿到客户端输入的值了。
2)将表单里面提交过来的值与ViewState中控件原来的值进行比对,不同则表示要触发该控件的Change 事件,则同时将该控件放到一个集合(看源码其实就是changedPostDataConsumers)中。在后续执行过程中遍历改集合依次触发对应控件的Change事件。
6.触发LoadRecursive()事件
大名鼎鼎的Page_Load就是在这里执行的。不过是先执行页面本身的Load事件再执行页面控件的Load事件哦,这时候前面给控件赋的值,表单提交过来的数据,ViewState等等都可以使用了,IsPostBack的原理就是判断是否有name为__VIEWSTATE的数据提交过来
7.再次触发ProcessPostData(this._leftoverPostData, false)事件
这个事件我在网上看了很多人说是将第一次遗漏下来的,第一次执行ProcessPostData没有涉及到的控件进行处理,但是并没有说明哪些遗漏下来了。为什么第一次没处理了? 最后Google查到是处理我们开发者在页面的Page_Load方法中添加的控件。在Page_Load中我们可以自己创建控件对象加到页面对应的“C#DOM树中“,如:在Page_Load中写
TextBox txt = new TextBox();txt.ID ="myTxtBox";this.form1.Controls.Add(txt);
这就是把开发者自己创建的控件加在页面的form1的表单中。当然你也可以加上Change事件了创建控件的时候。执行的还是上面那两件事了。则回发的时候可以给开发者在Page_Lod中手动创建的控件还原值。
8.触发RaiseChangedEvents事件
循环遍历changedPostDataConsumers集合中的所有控件,依次执行控件的非点击回传事件,比如文本框的改变事件等
9.触发RaisePostBackEvent(this._requestValueCollection)事件
执行按钮点击回传事件或者验证事件,如果有多个按钮,根据回发过来的按钮的 name来判断触发哪个按钮的事件,或者触发该控件的验证事件
10.触发PerformPreRenderComplete事件
循环遍历控件树中所有的控件,根据每个控件生成对应的Html代码,把服务器控件渲染成普通的html控件。
11.触发事件SaveAllState事件
将服务器端ViewState集合中的内容(开发者自己加的数据或控件状态信息等)序列化然后Base64编码然后设置到客户端隐藏域__ViewState中
12.RenderControl(this.CreateHtmlTextWriter(this.Response.Output))
把要发送到客户端浏览器的内容设置到Response.Output,应用程序将它发送 到客户端浏览器。
看到这里不知道大家是否已经可以清晰地回答开篇提到的几个问题了,其实就是这些事件执行的先后顺序,页面生命周期了。”前人植树,后人乘凉了"
最后附上生命周期ProcessRequest源码