首页 > 代码库 > .NET源码之Page类(二) (转)
.NET源码之Page类(二) (转)
.NET源码之Page类(二)
我们在.Net源码之Page类(一) 已经介绍过了初始化与加载阶段了。今天将介绍余下的部分。由于是从源代码上了解生命周期,所以这里会有大量的代码。建议大家看本篇博客的时候最好能够一边对照源代码,最好能够自己调试一遍。希望大家在平时碰到过这方面的问题的,可以留言,能够从源代码这个阶段去剖析问题的实质。 首先我们来回顾一下初始化与加载阶段之间的那个阶段,我们先拿MSDN上对初始化和加载阶段的有2句话描述来看一下:
现在我们可以理解这2句话了,因为加载视图的这个阶段是在页初始化阶段与页加载阶段之间执行的,所以造成了在页初始化阶段时候回发数据尚未加载,控件属性值尚未还原为视图状态中的值。而加载阶段,表示已经视图加载完毕了,可以从视图状态和控件状态中恢复信息了。 我们来看看在初始化与加载之间的这段加载视图的代码:
大家可以看到一个方法LoadAllState,这个就是用来加载所有视图的。当加载视图完毕后,里面还有一个方法,这个方法是用来控制数据回传。
其中再声明一点的就是ProcessPostData方法将会被执行2次,一次是这里页初始化与页加载之间,另外一次将是页加载与页加载完毕之间执行一次。
由于ViewState这里面的内容还是比较多的,也比较重要,我想在后续中对此单独开篇或者与其他的Session,Application等一起。现在我们只须先记住视图状态的加载是在页初始化和页加载之间执行的。
下面我们开始来说说回发事件这个阶段。代码如下所示,在加载与加载完毕之间执行:
这里面主要发生3个事件,如下所示:
其中ProcessPostData的功能是判断该控件在请求的前后其值有没有发生改变,如果发生改变,则把该控件暂时存放于一个集合中,该集合存储那些在请求前后值发生改变的控件。为什么要存储这些控件呢?因为下一个事件。
RaiseChangedEvents方法其实是触发那些前后值已经改变的控件的值改变事件TextChanged事件。所以上一个事件所存储的控件是为了这个阶段用的。
而如上2个事件,都是接口IPostBackDataHandler的成员,所以他们是成对出现的。如果你想实现自动数据回传并且能够触发TextChanged事件,那么必须实现这个接口。MSDN对此接口的描述:
下面看RaisePostBackEvent事件,这个方法主要实现的是触发回发事件。比如Button的Click事件等。MSDN对此接口IPostBackEventHandler的解析:
如果大家写过自定义控件,并且实现过这2个接口,那么大家对这2个接口的用处就比较熟悉了,如果你还没有去实现过这2个接口,建议现在你自己写个自定义控件试试看吧。 现在我们开始从源代码上一个一个来看这些事件:
ProcessPostData
前面已经提到过了,这个方法在2个地方被调用,一个是页初始化与页加载之间。一个是页加载与页加载完毕之间。现在我们先来看看这个方法,姑且不论为什么需要2个地方被调用。
看了上面的注释,大家也应该看清楚了实现了接口IPostBackDataHandler接口的控件是如何触发此方法LoadPostData的。对于上面这段代码,有2点要注意一下:
1. 如果该控件不是数据回传控件,并且发生了回传事件,那么就会注册该控件,有甚么用呢?
2. 如果该控件是数据回传控件,并且值已经发生改变了,那么也会被添加到集合changedPostDataConsumers中去,有甚么用呢?
第一个是为了执行后面的事件回发,第二个是为了执行值改变事件(Change事件)。
现在来分析下ProcessPostData为什么要执行2次呢?为什么是在加载与加载完毕之间执行呢?其实这里也没甚么,执行2次和为什么要在加载之后再执行呢?这其实都是为了动态加载控件的原因。因为我们再Page_Load里面动态加载控件的时候,那么发生在加载之前的方法,此控件就不会执行到了,所以我们在加载之后再执行了一次这个方法用来处理回发数据。
我们来看看这个接口的另外一个方法是这么被触发的吧!对于RaiseChangedEvents方法如下所示:
处理回发事件的过程大致如下:遍历所有存储在集合(ProcessPostData方法中实现的)中的控件,判断该控件是否实现了IPostBackDataHandler接口,如果实现了该接口,那么就触发这个控件的Changed事件。
我们接着看最后一个事件,处理回发事件的RaisePostBackEvent方法,其中实现了接口IPostBackEventHandler。看看这个方法的主要代码:
其中registeredControlThatRequireRaiseEvent是触发回传事件的控件,在前面的ProcessPostData里面注册的。
所以触发回传事件的大致过程如下:在ProcessPostData中查找并且注册了引发回传的控件,然后在RaisePostBackEvent中触发实现了该接口IPostBackEventHandler接口的控件的方法RaisePostBackEvent.
好了,现在我们已经看完了这3个非常重要的函数。那么对于我们这些有甚么用呢?我们知道了他们的运行原理,对于我们在实现服务器控件的时候,是非常有用的。比如当我们在实现一个自定义的服务器控件,直接继承Control的时候或者WebControl的时候,我们要让这控件自动回传数据等的时候,我们就知道了需要实现接口IPostBackDataHandler,当我们需要执行回传事件的时候,我们需要实现接口IPostBackEventHandler。还有我们需要知道的是这些事件的发生顺序,先执行数据变化事件,再执行回发事件。这几个方法的更大的应用的话在大家平时的开发中肯定会体现出来的。
下面我们就要来看呈现阶段了。看呈现阶段之前,我们来回顾一下前面这几个阶段。其中Init阶段,Load阶段,Event阶段。我们先初始化好所有的控件,然后加载数据,当是回传的时候,执行回传后发生的事件。那么我们就已经完成了我们呈现该页面的准备工作,所以应该开始我们的呈现阶段了。
在呈现阶段之前,还有一个需要提一下,就是方法SaveAllState,就是此时把所有的ViewState进行编码,准备输出到客户端,就是这里开始执行编码的。对于这个话题,我们也是放到后续中与ViewState一起讲解。
对于呈现阶段,与之关系最密切的就是我们的自定义控件的呈现阶段了,那到底他们是怎么样组合成一个Html流的呢?
我现在先简单说一下呈现过程,然后再从代码上来看。这个呈现过程其实跟加载过程类似,先格式化好父控件的Html流,接着执行子控件的。最终拼凑出一个Html,发回给客户端交给浏览器解析执行。
那么现在就从代码上来看看这个过程。
先看呈现的主函数被调用的:
利用Response创建一个HtmlTextWriter对象,那现在这个对象里面到底有甚么呢?我们先来讨论一下这个Response,到了这个阶段,相信前面对这个Response基本上都会有处理过了,那么有些处理就会在这里被体现出来。到底这么体现,就看这个OutPut了。我们来深入分析下,其中OutPut是个TextWriter类型,我们来看看这个类型的定义
是个抽象类,我们查阅源代码后发现,OutPut的实例其实是HttpWriter。HttpWriter实现了几个抽象函数,其中主要的是
以上是一些关于OutPut本身的一些内容,那么OutPut里面是些甚么内容呢?一般来说,现在里面的内容是我们在前面几个生命周期阶段中的Respone.Write出来的内容。当我们查看Response里面的Write方法的时候:
无一例外的都是调用HttpWriter的Write实现(其中_writer就是OutPut在内部的引用)。所以我们经常有时候在Load里面Response.write,然后我们在页面查看原代码的时候,发现我们的输出内容在最前面。就是这个道理,因为这个时候Page控件的Render还没有执行,自然就在你输出内容的后面了。
好了,现在我们可以来看看他们控件的呈现过程了。
这里面有5个关键的函数:
现在我们来说说他们之间的关系。当我们从Page里面调用了函数RenderControl(HtmlTextWriter writer)
调用这个函数其实只是为了调用RenderControl(HtmlTextWriter writer, ControlAdapter adapter)而这个函数的目的其实只是为了不同的浏览器的做不同的解析。这个函数接着会调用函数RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)这个函数的主要目的是调用当前控件的Render方法实现呈现。那么既然呈现了当前控件了,那么他的子控件呢,该怎么呈现呢?我们就以Page为例来说明把,我们先来查看Page类的Render函数:
我们先不需要去管InitializeWriter(writer);我们可以看到我们调用了base.Render(Writer),所以我们又回到了Control类的Render回来了。那么既然来到了Control的Render函数,我们就来看看把:
原来只是调用了RenderChildren(writer); 函数,那我们就来看这个函数好了。
其中_occasionalFields表示当前控件的所有子控件的集合。我们发现接着会调用函数RenderChildrenInternal(writer, children);
那么我们就来看看这个函数
这里的主要代码其实就是
到这里我们可以看出来了,又是一个循环调用,先调用RenderControl(writer),然后加载当前控件的render函数实现呈现,所以我们现在就可以知道了他们的加载顺序,为什么先呈现父控件了。 那么总结一下呈现的大致过程:开始调用Control的RenderControl函数,接着调用它的重载函数RenderControl(HtmlTextWriter writer, ControlAdapter adapter)以便可以在不同的浏览器实现解析。接着调用RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)触发当前控件的Render方法。然后在Render方法中实现base.Render(writer); 最终调用Control类的Render(HtmlTextWriter writer),在此函数中实现调用当前控件的所有子控件的呈现,即函数RenderChildren(writer);在此函数中,遍历所有的子控件,并且调用RenderChildrenInternal(writer, children);来实现呈现子控件。这就到了一个循环,直到所有的控件都呈现完毕为止。
以上就是关于整个生命周期的主要阶段了。从初始化到呈现阶段,那么最后还有一个卸载阶段了。 那卸载阶段发生在哪里呢? 它并不在
此函数中,而是在下面这个函数中:
看到了把,就是这里是卸载阶段,就是调用上面那个函数的函数。所以卸载阶段是肯定会被执行的,就算在初始化到呈现阶段出现异常了。我们来看看这个函数:
对于这里我们关注一下_request和_response,所以我们在我们的Page类中重写OnUnload方法的时候,不能用Response和Request的原因了。因为这里已经给它销毁了。所以我们要用的时候,只能用HttpContext.Current.Response了。 现在我们对于生命周期的过程就讲解到这里了,这里非常粗糙。希望大家能够讨论讨论。后续计划想写一篇从源代码上看运行时,ViewState等的存储机制等。希望得到大家的支持,^_^。