ASP.NET ViewState详解
2011-10-21 15:33
495 查看
[b]概述[/b]ViewState是一个被误解很深的动物了。我希望通过此文章来澄清人们对ViewState的一些错误认识。为了达到这个目的,我决定从头到尾详细的描述一下整个ViewState的工作机制,其中我会同时用一些例子说明我文章中的观点,结论。比如我会用静态控件(declaredcontrols)和动态控件(dynamiccontrols)两个方面来说明同一个问题。现在有关ViewState的文章可谓多如牛毛,你可能会说再写有关ViewState的文章无异于炒剩饭(我这篇文章便是:D)。但是我却不这么认为,如果把ViewState看成一匹野马的话,那么这匹野马并没有死去,它还活跃的很,说不定这个时候它正在你的客厅里撒野呢。所以我们有必要再次去把它击倒。不过你也不需要担心,从这篇文章你可以发现其实这匹马也没有那么坏。我的意思并不是否然目前还没有好好说明ViewState的文章,只是我总觉得好像这些文章都缺少一些东西,而这些缺少的东西往往就会导致人们对ViewState的困惑。比如:理解ViewState是怎样跟踪那些已经出现变化的数据(dirtydata)就非常重要,但是很多文章却没有过多的涉及,或者即便涉及了可能其中却包含了错误的信息。比如这篇文章(W3Schools)中就说页面回传的值也是保存在ViewState中的,但是这个观点是错误的。不信是吗?那么你在一个页面上放置一个TextBox控件和一个Button控件,然后你在“属性”中将TextBox的EnableViewState设置为False,然后通过点击Button回传页面,你会发现TextBox还是仍旧会保留你输入的值,而不会如你想象的由于TextBox的ViewState被禁用了而导致TextBox的值在页面回传的过程中消失了。还有一些文章(#1GoogleSearchResult,ASP.NETDocumentationonMSDN)描述了服务器控件如何在页面的回传中保持自身状态。这些文档虽然没有全错,但是有些描述还是存在一些不准确的地方,如:"IfacontrolusesViewStateforpropertydatainsteadofaprivatefield,thatpropertyautomaticallywillbepersistedacrossroundtripstotheclient."(如果一个控件用ViewState而不是用类的私有字段(privatefield)来存储数据,那么这些控件属性的值将会自动在页面回传之间保持状态??[这句话的意思有待确定])
上面这句话似乎在暗示任何东西只要是保存在ViewState状态包(StateBag)中,那么就会在服务器和客户端页面回传的过程中被传递。(ThatseemstoimplythatanythingyoushoveintotheViewStateStateBagwillberound-trippedintheclient'sbrowser.NOTTRUE!)不对!所以说对于ViewState控件的困惑还是存在的。而且在Internet上我目前还没有找到一篇100%准确完整的描述ViewState工作的文章。我目前找到的最好文章是这一篇(ViewState["Key1"]=123.45M;//storeadecimalvalueViewState["Key2"]="abc";//storeastringViewState["Key3"]=DateTime.Now;//storeaDateTime实际上ViewState仅仅就是一个定义在System.Web.UI.Control类中的一个保护类型(Protected) 的属性名称。由于所有服务器端的控件,用户自定义控件还有页面(Page)类都是继承自System.Web.UI.Control类, 所以这些控件都具有这些属性。ViewState的真正类型实际应该是System.Web.UI.StateBag类。 严格的说,虽然StateBag类虽然定义在System.Web的命名空间下,实际上StateBag类 和ASP.NET并没有严格上的依存关系,它也完全可以放在System.Collections命名空间下。 事实上许多服务器端控件大多数属性值都是利用ViewState来进行数据存储。你可能认为 TextBox.Text属性是按如下形式存储的:publicstringTextpublicstringTextpublicclassMyClasspublicstringTextstateBag.IsItemDirty("key");//returnsfalsestateBag["key"]="abc";stateBag.IsItemDirty("key");//stillreturnsfalsestateBag["key"]="def";stateBag.IsItemDirty("key");//STILLreturnsfalsestateBag.TrackViewState();stateBag.IsItemDirty("key");//yupstillreturnsfalsestateBag["key"]="ghi";stateBag.IsItemDirty("key");//TRUE!stateBag.SetItemDirty("key",false);stateBag.IsItemDirty("key");//FALSE! 看到上面的例子应该很清楚了,在调用了TrackViewState()方法后,StateBag开始跟踪 其所包含项值的变化。再次无论你如何修改StateBag中项的值,都无法把数据弄“脏” 的。而且这里还需要注意一点,在TrackViewState()方法调用后,只要是出现了赋值操作 那么就会使其被标记为脏数据,StateBag并不会判断赋值前后对应项的值是否出现了变化。 如下例子所示:stateBag["key"]="abc";stateBag.IsItemDirty("key");//returnsfalsestateBag.TrackViewState();stateBag["key"]="abc";stateBag.IsItemDirty("key");//returnstrue可能你会认为根据赋值前后ViewState是否存在变化然后再标记是否是脏数据这样更加符合常理。但是必须注意的是ViewState的项是可以存储任何类型的值的(实际上任何赋值给ViewState的变量都会被装箱为Object类型的变量),所以比较赋值前后的值是否一致实际上并没有变面上看的那么容易。而且不是每种类型都是先了IComparable的接口,所以通过调用CompareTo方法来进行比较也是不可行的。另外还有一个原因,我们知道ViewState还需要将其内部的数据进行序列和反序列化,当这些操作发生后,你得到的对象已经不是原来那个对象了,所以比较对象之间的引用也是无法完成的。基于以上这些原因,ViewState采取了一种简单的做法,也就意味着ViewState的数据变化跟踪也是一个简要的跟踪。到这里你可能会想在设计StateBag类的时候为什么要使其具备跟踪数据变化的能力呢?我们为什么要跟踪那些出现变化的项呢?(WhyonearthwouldanyoneneedtoknowonlychangessinceTrackViewState()iscalled?Whywouldn'ttheyjustutilizetheentirecollectionofitems?),这个疑问往往是造成对ViewState困惑的根源。基于这个问题我曾经和很多人交谈过,其中不乏有着多年ASP.NET开发经验的专家,但是很遗憾,我没有从任何一个人那里得到我满意的答案。没有一个人能够解释清楚为什么StateBag对数据变化的跟踪是必要的。为了解释清楚这个问题,我们首先需要先了解一下ASP.NET是怎样建立静态控件的。所谓静态控件(declarativecontrol)就是那些从页面或者用户自定义控件的源码中可以看到声明代码的控件。如:<asp:Labelid="lbl1"runat="server"Text="HelloWorld"/>这里在页面上声明了一个Label控件。然后ASP.NET会解析这段代码,它首先会查找那些标签中带有“runat=server”的代码,然后根据类型创建对应类型的控件对象,接着将标签中设置的控件属性值一个一个的赋值到控件实例对象中。比如例子中我们设置了Label对象的Text属性,那么在解析的时候就会存在一个类似于:lbl1.Text="HelloWorld"的赋值过程。通过反射机制,ASP.NET可以知道对应的类型是否具有对应的属性,对应的属性是什么数据类型。这里Text属性的数据类型是String型,对于数据类型不是String的属性,那么在设置属性前ASP.NET必须实现将String到对应数据类型的转换。如:TextBox控件可以设置Width属性,但是Width是Unit类型的,所以这里就设计到一个从String到Unit类型的转化过程。好,到了这,我们再以前面所说的内容将当前发生的事情再描述一遍。我们已经知道大多数服务器控件的属性值最终是存储在ViewState中。而且如果ViewState已经开始了跟踪数据,那么此次属性的赋值就会导致“脏数据”的产生,但是如果ViewState还没有开始跟踪数据,那么脏数据的标记值就一直为False。现在问题就是在当前ASP.NET解析静态控件的时候是否开始跟踪和是否产生了“脏数据”呢?答案是,没有。原因是此时的ViewState赋值之前ASP.NET并没有去调用TrackViewState()方法,所以ViewState是不会对数据的更改进行跟踪的。事实上ASP.NET是在页面生命周期的OnInit阶段才调用TrackViewState()方法的。这样做的目的就是让ASP.NET可以很方便的区分控件的哪些属性值在初次声明后仍未改变,那些属性值已经被改变了(可能是程序的方式也可能是人工输入的方式)。如果到目前为止你还没有意识到这个观点很重要的话,那么请继续往下读吧。3.序列化和反序列化(SERIALIZATIONANDDESERIALIZATION)我们先把ASP.NET怎样解析生成静态控件放一边,我们前面提到的ViewState的两个重要功能(1.ViewState可以像HashTable那样通过名值对来存储值;2.ViewState可以对那些修改的数据进行跟踪。)现在我们将来讨论另外一个话题,那就是ASP.NET是怎样通过StateBag类的特性来实现那些看似诡异的功能的。如果你在ASP.NET中使用过ViewState,事实上我相信只要是ASP.NET的开发者都会使用过ViewState了。而且可能你也知道了序列化(serialization)的问题。如果是默认的方式,那么VIewState中的值会被序列化成一个基于Base64编码的字符串,然后存储在页面中一个叫做_ViewState的隐藏变量中。这里在继续之前,我需要稍稍叉开一下话题先说一些页面的控件树。我发现有不少有多年工作ASP.NET开放经验的程序员还不知道控件树的存在。由于他们仅仅是对.aspx页面进行操作,所以他们仅仅只关心那些页面上声明的控件。但是我们必须认识到页面的控件实际是以一颗控件树存在的,并且控件中还可以包含子控件。这颗控件树的根节点就是页面本身(Page),然后树的第二层通常是包含3个控件,它们分别是用于保存表单(<form>)标签前所有信息的文本控件(Literal),然后是表单控件(Form),然后是表单(</form>)标签后面的所有信息的文本控件(Literal)。接着是树的第三层包含的控件就是在表单标签内声明的那些控件,如果这些控件中还包含子控件,那么这颗控件树的深度将会不断的加深,一直到所有页面的控件都被包含在这颗控件树中。每个控件都会有自己的ViewState对象,并且由于这些控件共同的基类(System.Web.UI.Control)中包含一个受保护(protected)的方法SaveViewState,方法的返回值是一个Object变量。在Control.SaveViewState方法中如果发现ViewState不为空,那么就直接调用其私有变量_viewState(StateBag类型)的SaveViewState方法。通过阅读这个方法,可以发现其作用就是将ViewState中被标记为脏数据(dirty)的项的键和值都存储在一个ArrayList中,然后再将这个ArrayList进行返回。通过递归的方法遍历整个控件树的各个节点,并递归的调用各个控件的SaveViewState方法,这样当整个控件树被遍历完成以后,那么和控件树一一对应的会形成一个由ViewState的值组成的数据树。在这个阶段,ViewState中存储的数据还没有被转化为我们在_ViewState隐藏变量中存储的Base64编码的字符串。这里仅仅是形成了一颗需要被持久化存储的数据树。这里再强调一下,存储的数据是ViewState中那些被标记为Dirty的项。StateBag类具有跟踪功能就是为了在存储的时候判断哪些数据需要被存储,哪些数据不需要被存储(实际上这是StateBag具有跟踪数据功能的唯一原因)。很聪明是吧,但是如果使用不当的话,在ViewState中依然可能保存一些不必要的数据。我会在后面的例子中来说明这些可能犯的错误。(.Thatistheonlyreasonwhyithasit.Andohwhatagoodreasonitis--StateBagcouldjustprocesseverysingleitemstoredwithinit,butwhyshoulddatathathasnotbeenchangedfromit'snatural,declarativestatebepersisted?There'snoreasonforittobe--itwillberestoredonthenextrequestwhenASP.NETreparsesthepageanyway(actuallyitonlyparsesitonce,buildingacompiledclassthatdoestheworkfromthenon).DespitethissmartoptimizationemployedbyASP.NET,unnecessarydataisstillpersistedintoViewStateallthetimeduetomisuse.Iwillgetintoexamplesthatdemonstratethesetypesofmistakeslateron.)突击测试(POPQUIZ)如果你已经读到了这里,那么祝贺你,我要奖励一下这么有毅力的你。咱们来个突击测试如何?我是不是人很好呢?哈哈。题目是这样的,我们有两个几乎一模一样的.aspx页面,我们分别称之为Page1.aspx和Page2.aspx,每个页面都存在一个form,其中包含一个Label控件,如下所示:<formid="form1"runat="server"><asp:Labelid="label1"runat="server"Text=""/></form> 这两个页面唯一的区别是Label中包含的值不同(Label.Text的值)。Page1.aspx中的 label1.Text="abc",如下代码所示。<asp:Labelid="label1"runat="server"Text="abc"/>那么对于Page2.aspx中的Label,我们对其多赋一点值(就来个美国宪法的序言吧)。如下代码所示。<asp:Labelid="label1"runat="server"Text="WethepeopleoftheUnitedStates,inordertoformamoreperfectunion,establishjustice,insuredomestictranquility,provideforthecommondefense,promotethegeneralwelfare,andsecuretheblessingsoflibertytoourselvesandourposterity,doordainandestablishthisConstitutionfortheUnitedStatesofAmerica."/>现在我们在浏览器中运行Page1.aspx,那么我们将看到一个abc。然后你通过浏览器查看页面的HTML源码,你可以找到那个“臭名昭著”的隐藏字段(_ViewState)。然后把Page1.aspx的_ViewState值保留下来。接着运行Page2.aspx,同样保留其_ViewState的值。然后比较这两个ViewState的大小(注意:这里比较的是大小,或者说比较字符串的长度,而不是内容)。问题来了,请问这两个ViewState的大小是否一样呢?好了,在公布答案之前我们再去看看另外一个问题,我们在两个页面上都增加一个Button控件,这样通过点击Button按钮我们就可以回传页面了。一下就是页面中声明Button控件的代码:<asp:Buttonid="button1"runat="server"Text="Postback"/>这个Button并没有任何的Click事件处理函数,仅仅用于将页面提交服务器。我们再重复上面的实验,唯一不同的是,我们这回是在点击了Button后再去查看各自得_ViewState的值,我们的问题还是一样的,请问这两个ViewState的大小是否一样呢?好,现在揭晓正确答案。第一个问题的答案是:是的,两个页面的ViewState的大小是一样的。原因是这当前这两个ViewState中并不包含任何和Label有关的数据。前面我们知道所有需要存储在_ViewState中的数据都必须是被标记为Dirty的脏数据。而需要启动对ViewState中各项数据的跟踪,必须先要调用TrackViewState()方法,什么时候调用TrackViewState方法呢?是在页面生命周期中的OnInit阶段,而由于Label控件中的Text是在页面中静态声明的,所以在AddParsedSubObject阶段(早于OnInit阶段)Text值就已经被赋值到对应Label控件中了。所以这些Text中的值将不会被标记为Dirty,同时也不会被保存在_ViewState中。所以无论Label.Text有什么不同,那么其页面的_ViewState始终是相同的。那页面中那一小段的_ViewState到底包含了什么信息呢?你可以用ViewStateDecoder工具查看一下,可以发现在这样一个简单的界面,_ViewState仅仅包含了页面的哈希代码(HashCode)。好,让我们到第二个问题(页面加了Button那个情况),答案同样是:是的,它们的大小也是一样的。原因和上面解释的一样。简单的说就是在TrackViewState()方法后面并没有对Label.Text属性进行赋值操作,所以ViewState中的项并没有被标记为Dirty,自然就不会被序列化并记录到_ViewState隐藏变量中了。到此为止我们已经基本了解了ASP.NET平台是怎样决定一个数据是否需要被序列化并永久保留在_ViewState中了(那些被标记为Dirty的数据)。至于ASP.NET是怎样序列化这些数据的已经不是本文的范围了,如果你有兴趣进一步了解的话,那么请参看如下两篇文章: 3.持久化廉价的数据(Persistingcheapdata) 这个问题实际上包含了第一个问题。静态数据往往是很容易就可以得到的(取得的开销 /成本比较小),但是并不是所有容易取得的数据都是静态数据。可能这些数据会不停 的被更改,但是总体来说得到这些数据的成本很低。一个典型的例子是美国各个州的列表。 除非你要回到1787年12月7日( 当然我们现在的程序员都很痛恨硬编码。“让我把美国各个州的列表都静态的写在页面 上?傻子才这样做呢。”我们更加倾向于将州名都保留在一个数据库(或者其他易于 修改的配置文件中。),这样如果州名或者州的列表出现了任何变化,就不用修改源 代码了。恩,我完全同意这一点,我们的著名程序员Joe也是这样认为的,而且这张表 在他们公司已经存在了,表名叫做USSTATES,这回Joe的任务就是和操作这张表有关系的。 下面是用于显示美国各个州列表的下拉菜单(DropDownList):<asp:DropdownListid="lstStates"runat="server"DataTextField="StateName"DataValueField="StateCode"/> 这里显示的是绑定从数据库中取得的美国州列表的数据代码:protectedoverridevoidOnLoad(EventArgsargs)<asp:DropdownListid="lstStates"runat="server"DataTextField="StateName"DataValueField="StateCode"EnableViewState="false"/>protectedoverridevoidOnInit(EventArgsargs)<asp:Labelid="lblDate"runat="server"/>protectedoverridevoidOnInit(EventArgsargs)<asp:Labelid="Label1"runat="server"Text="<%=DateTime.Now.ToString()%>"/>你可能也有过这样尝试,但是ASP.NET会给你当头一棒,它会明确的告诉你<%=%>语法不能对服务器端控件的属性进行赋值操作。当然Joe也可以使用<%#%>的方法,但是这个方法和我们前面提到的禁用Label的ViewState同时在每次请求页面的时候绑定数据的方法实际上是一样的。问题是我们希望通过编码的方式为Label的Text属性的初值进行赋值操作(我们不希望这些赋值操作导致ViewState大小的增加),同样在以后的操作中我们希望这个Label控件依然可以像一个普通的Label控件被使用。简单的说就是这样,我们需要一个Label,它的默认值是当前的日期和时间,但是如果我们人工的对其Label.Text进行了赋值操作,那么我们还是希望这个值在页面的回传之间可以保留(即通过ViewState进行持久化)。举个简单的例子,Joe的页面上有一个按钮,当用户点击这个按钮那么显示当前日期和时间的Label将显示一个“空时间”(即:“--/--/------:--:--”),此按钮的响应代码为:privatevoidcmdRemoveDate_Click(objectsender,EventArgsargs)<asp:Labelid="Label2"runat="server"OnInit="lblDate_Init"/>同样在后台编写Label.OnInit事件对应的响应函数并对Label.Text赋初值也是可以的。2.创建用户自定义组件(Createacustomcontrol):publicclassDateTimeLabel:LabelpublicclassJoesCustomControl:ControlpublicclassJoesCustomControl:ControlpublicclassJoesCustomControl:Control 相关文章推荐
|