做者:Infinities Loopphp
概述html
ViewState是一个被误解很深的动物了。我但愿经过此文章来澄清人们对ViewState的一些错误认识。为了达到这个目的,我决定从头至尾详细的描述一下整个ViewState的工做机制,其中我会同时用一些例子说明我文章中的观点,结论。好比我会用静态控件(declared controls)和动态控件(dynamic controls)两个方面来讲明同一个问题。程序员
如今有关ViewState的文章可谓多如牛毛,你可能会说再写有关ViewState的文章无异于炒剩饭(我这篇文章即是:D)。可是我却不这么认为,若是把ViewState当作一匹野马的话,那么这匹野马并无死去,它还活跃的很,说不定这个时候它正在你的客厅里撒野呢。因此咱们有必要再次去把它击倒。不过你也不须要担忧,从这篇文章你能够发现其实这匹马也没有那么坏。web
个人意思并非否然目前尚未好好说明ViewState的文章,只是我总以为好像这些文章都缺乏一些东西,而这些缺乏的东西每每就会致使人们对ViewState的困惑。好比:理解ViewState是怎样跟踪那些已经出现变化的数据(dirty data)就很是重要,可是不少文章却没有过多的涉及,或者即使涉及了可能其中却包含了错误的信息。好比这篇文章(W3Schools)中就说页面回传的值也是保存在ViewState中的,可是这个观点是错误的。不信是吗?那么你在一个页面上放置一个TextBox控件和一个Button控件,而后你在“属性”中将TextBox的EnableViewState设置为False,而后经过点击Button回传页面,你会发现TextBox仍是仍旧会保留你输入的值,而不会如你想象的因为TextBox的ViewState被禁用了而致使TextBox的值在页面回传的过程当中消失了。还有一些文章(#1 Google Search Result , ASP.NET Documentation on MSDN )描述了服务器控件如何在页面的回传中保持自身状态。这些文档虽然没有全错,可是有些描述仍是存在一些不许确的地方,如:数据库
"If a control uses ViewState for property data instead of a private field, that property automatically will be persisted across round trips to the client."编程
(若是一个控件用ViewState而不是用类的私有字段(private field)来存储数据,那么这些控件属性的值将会自动在页面回传之间保持状态??[这句话的意思有待肯定])浏览器
上面这句话彷佛在暗示任何东西只要是保存在ViewState 状态包(StateBag)中,那么就会在服务器和客户端页面回传的过程当中被传递。(That seems to imply that anything you shove into the ViewState StateBag will be round-tripped in the client's browser. NOT TRUE!)不对!因此说对于ViewState控件的困惑仍是存在的。并且在Internet上我目前尚未找到一篇100%准确完整的描述ViewState工做的文章。我目前找到的最好文章是这一篇(this one by Scott Mitchell)。这篇文章是值得一读的。然而这篇文章仍是有些美中不足,它没有描述在控件初始化和ViewState跟踪的时候父控件和子控件之间的关系。而偏偏就是这一点就会致使对ViewState大量的误解,至少我是有过这种经历的。缓存
根据上面的状况,这篇文章从最开始先会对ViewState实现的功能进行一个彻底描述。而后对ViewState的实现进行一个详细的阐述,在这个过程当中我会同时举一些相对应的例子,一般我会先举一个开发人员常常会犯的错误的例子,而后再举一个例子来表示如何修正错误。在这里我须要实现声明一下的是,虽然在ASP.NET 2.0中ViewState的实现机制有稍许变更,可是在写这篇文章的时候我依然是以ASP.NET 1.x的版本为前提进行的。好比说在ASP.NET 2.0的新增了一个新类型的ViewState -- ControlState, 可是实际上ControlState和ViewState并不大变,因此咱们这里能够忽略它。服务器
首先让咱们看看为何深刻了解ViewState是如此重要。网络
对ViewState的误解可能致使...
若是你正在开发基于ASP.NET平台的网络应用程序,而且你无视ViewState存在的话,那么如下的状况可能会发生在你的身上。
![]() |
![]() |
The ViewState form field.看看你页面的HTML源代码吧,密密麻麻很恐怖吧。 | ViewState will add your web app's distinctiveness to it's own. Performance is futile. 沉重的星际垃圾 |
像如上的例子我还能够举出不少,可是我想这两个例子已经具备表明性了。好了,如今让咱们从最开始认识ViewState吧。
ViewState能够用来作什么?这里列举的每一项都是ViewState须要完成的主要工做,咱们将根据这些工做来学习ViewState是如何实现这些功能。
下面列举的是ViewState不能用来作什么的列表,这个其实比了解ViewState是用来作什么的还重要。
什么是ViewState不能作的?
虽然ViewState做为一个总体出如今.NET Framework框架中有它的惟一目的,那就是在页面回传的过程当中保存状态值,使本来没有“记忆”的Http协议变得有“记忆”起来。可是上面列举的ViewState的四个主要功能之间却没有太多的关联。因此从逻辑上咱们能够将其划分开来,各个击破。
1. ViewState就是用来存储数据的
若是你曾经使用过HashTable的话,那么你应该明白个人意思了。这里并无什么高深的理论。ViewState经过String类型的数据做为索引(注意在ViewState中不容许经过整形下表的方式对其中的项进行访问,如:ViewState.Item(0) 的形式是不容许的。)ViewState对应项中的值能够存储任何类型的值,实施上任何类型的值存储到ViewState中都会被装箱为Object类型。如下是几个对ViewState进行赋值的几个例子。
实际上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属性是按以下形式存储的:
可是你必须注意,上面的形式(经过类的私有字段)并非大多数ASP.NET 服务器控件存储其属性值得方式。这些控件的属性值大可能是经过ViewState来进行存储的。经过Reflector查看TextBox.Text属性的源代码你能够看到相似以下的代码:
为了表示这个观点的重要性,我这里再重申一遍“大多数ASP.NET 服务器控件存储其属性值得方式是经过ViewState的方式存储的,而不是咱们一般想象的那样经过类的私有字段来存储。”即使是用于设定服务器控件样式的Style类中的大多数属性值也是经过ViewState来进行存储的。因此在设计自定义的组件时,对于那些须要存储的组件属性值也最好遵循这个方式。这里我还须要着重讲一个问题,在以ViewState为存储方式的状况下,若是实现属性的默认值(default value),咱们可能会认为属性值是这样实现的:
这样若是在对Text的属性没有设置的时候,直接取Text属性,那么咱们能够获得默认值"Default Value!"。那么若是咱们使用ViewState来存储的时候如何实现默认值呢?以下所示:
就像操做HashTable同样,若是StateBag(ViewState)中没有包含某个键值的项,那么它会返回一个null(在VB.NET中是返回Nothing)。因此咱们能够经过判断对应键值的项是不是null来判断某个ViewState项是否被赋值。而后咱们经过三目运算符来根据实际状况来返回默认值或者设置的值。而且使用三目运算符实际上这里还出于一个考虑,那么就是在服务器控件中,若是将某个属性值设置为空(null),那么每每表明的意思是使用此属性的默认值。因此第一种实现方法还存在一个问题,那就是若是把某个属性值设置为null,当咱们再取这个属性的时候咱们将获得null,而不是咱们指望的"Default Value!"了,因此对于第一种实现方法还须要对null这个特殊值进行判断才能够彻底知足需求。ViewState还能够被用做其余的做用,好比在页面回传过程当中保留某些值,好比咱们在页面后台代码中经常使用ViewState("Key") = "SomeValue"的方式来存储值,实际上就是使用了Page类的ViewState属性来进行值得存储。一样的咱们也能够在控件级别进行ViewState的字定义存储。可是因为这是另一个话题,和咱们如今所要描述的东西没有太多关系,因此这里就再也不详细说明下去了。 2. ViewState能够跟踪值的变化
你知道吗?若是你设置一个控件的属性值,那么你会把ViewState中这个属性值对应的数据弄脏(dirty)的。固然数据这个和数据库中的脏数据不一样,这里的脏能够理解为“发生变化”的意思。你知道为何StateBag会存在,而不会被HashTable取代吗?前面咱们但是大肆宣扬了一下StateBag和HashTable有多么的像。虽然他们都是经过名值对的方式来存储值,可是StateBag还具备对其中数据更改的跟踪过程(Tracking ability)。是否进行跟踪的开关能够被设置成开或者关,当调用StateBag.TrackViewState()方法后跟踪开关将被开启。只有在跟踪的开关设置为“开”的状况下StateBag中的数据更改才会被跟踪,只要数据出现修改,那么对应StateBag项的数据将会被标记为“脏的”(dirty)。StateBag还提供了检查一个数据项是不是脏数据的方法 -- IsItemDirty(string key)。你也能够在不更改项数据数值的状况下将对应项设置为脏数据,这里须要使用SetItemDirty(string key)方法。为了说明这些,咱们看一下如下的例子。这里咱们假设当前的StateBag跟踪的开关是处于关闭状态的。
看到上面的例子应该很清楚了,在调用了TrackViewState()方法后,StateBag开始跟踪
其所包含项值的变化。再次不管你如何修改StateBag中项的值,都没法把数据弄“脏”
的。并且这里还须要注意一点,在TrackViewState()方法调用后,只要是出现了赋值操做
那么就会使其被标记为脏数据,StateBag并不会判断赋值先后对应项的值是否出现了变化。
以下例子所示:
可能你会认为根据赋值先后ViewState是否存在变化而后再标记是不是脏数据这样更加符合常理。可是必须注意的是ViewState的项是能够存储任何类型的值的(实际上任何赋值给ViewState的变量都会被装箱为Object类型的变量),因此比较赋值先后的值是否一致实际上并无变面上看的那么容易。并且不是每种类型都是先了IComparable的接口,因此经过调用CompareTo方法来进行比较也是不可行的。另外还有一个缘由,咱们知道ViewState还须要将其内部的数据进行序列和反序列化,当这些操做发生后,你获得的对象已经不是原来那个对象了,因此比较对象之间的引用也是没法完成的。基于以上这些缘由,ViewState采起了一种简单的作法,也就意味着ViewState的数据变化跟踪也是一个简要的跟踪。 到这里你可能会想在设计StateBag类的时候为何要使其具有跟踪数据变化的能力呢?咱们为何要跟踪那些出现变化的项呢?(Why on earth would anyone need to know only changes since TrackViewState() is called? Why wouldn't they just utilize the entire collection of items? ),这个疑问每每是形成对ViewState困惑的根源。基于这个问题我曾经和不少人交谈过,其中不乏有着多年ASP.NET开发经验的专家,可是很遗憾,我没有从任何一我的那里获得我满意的答案。没有一我的可以解释清楚为何StateBag对数据变化的跟踪是必要的。为了解释清楚这个问题,咱们首先须要先了解一下ASP.NET是怎样创建静态控件的。所谓静态控件(declarative control)就是那些从页面或者用户自定义控件的源码中能够看到声明代码的控件。如:
这里在页面上声明了一个Label控件。而后ASP.NET会解析这段代码,它首先会查找那些标签中带有“runat=server”的代码,而后根据类型建立对应类型的控件对象,接着将标签中设置的控件属性值一个一个的赋值到控件实例对象中。好比例子中咱们设置了Label对象的Text属性,那么在解析的时候就会存在一个相似于:lbl1.Text = "Hello World"的赋值过程。经过反射机制,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. 序列化和反序列化(SERIALIZATION AND DESERIALIZATION ) 咱们先把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中依然可能保存一些没必要要的数据。我会在后面的例子中来讲明这些可能犯的错误。(. That is the only reason why it has it. And oh what a good reason it is -- StateBag could just process every single item stored within it, but why should data that has not been changed from it's natural, declarative state be persisted? There's no reason for it to be -- it will be restored on the next request when ASP.NET reparses the page anyway (actually it only parses it once, building a compiled class that does the work from then on). Despite this smart optimization employed by ASP.NET, unnecessary data is still persisted into ViewState all the time due to misuse. I will get into examples that demonstrate these types of mistakes later on.)
突击测试(POP QUIZ) 若是你已经读到了这里,那么祝贺你,我要奖励一下这么有毅力的你。我们来个突击测试如何?我是否是人很好呢?哈哈。题目是这样的,咱们有两个几乎如出一辙的.aspx页面,咱们分别称之为Page1.aspx和Page2.aspx, 每一个页面都存在一个form,其中包含一个Label控件,以下所示:
这两个页面惟一的区别是Label中包含的值不一样(Label.Text的值)。Page1.aspx中的
label1.Text = "abc",以下代码所示。
那么对于Page2.aspx中的Label,咱们对其多赋一点值(就来个美国宪法的序言吧)。以下代码所示。
如今咱们在浏览器中运行Page1.aspx,那么咱们将看到一个abc。而后你经过浏览器查看页面的HTML源码,你能够找到那个“臭名昭著”的隐藏字段(_ViewState)。而后把Page1.aspx的_ViewState值保留下来。接着运行Page2.aspx,一样保留其_ViewState的值。而后比较这两个ViewState的大小(注意:这里比较的是大小,或者说比较字符串的长度,而不是内容)。问题来了,请问这两个ViewState的大小是否同样呢?好了,在公布答案以前咱们再去看看另一个问题,咱们在两个页面上都增长一个Button控件,这样经过点击Button按钮咱们就能够回传页面了。一下就是页面中声明Button控件的代码:
这个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到底包含了什么信息呢?你能够用ViewState Decoder工具查看一下,能够发如今这样一个简单的界面,_ViewState仅仅包含了页面的哈希代码(HashCode)。
好,让咱们到第二个问题(页面加了Button那个状况),答案一样是:是的,它们的大小也是同样的。缘由和上面解释的同样。简单的说就是在TrackViewState()方法后面并无对Label.Text属性进行赋值操做,因此ViewState中的项并无被标记为Dirty,天然就不会被序列化并记录到_ViewState隐藏变量中了。
到此为止咱们已经基本了解了ASP.NET平台是怎样决定一个数据是否须要被序列化并永久保留在_ViewState中了(那些被标记为Dirty的数据)。至于ASP.NET是怎样序列化这些数据的已经不是本文的范围了,若是你有兴趣进一步了解的话,那么请参看以下两篇文章: LosFormatter for ASP.NET 1.x 和 ObjectStateFormatter for ASP.NET 2.0。
在这一个小节的最后,咱们要简单的说说反序列化。反序列化和序列化是相对应的,若是不能经过反序列化来说序列化的对象进行还原,进行进行操做的话,那么序列化操做将没有任何意义。可是这是另一个话题,因此这里就再也不进行赘述。
4. 自动恢复数据(AUTOMATICALLY RESTORES DATA) 到此为止咱们已经说到了ViewState最后一个功能,那就是自动恢复数据。有些文章将这个过程和上面提到的反序列化过程混淆在一块儿,这样的理解是不正确的,实际上自动恢复数据的过程并非反序列化过程的一部分。ASP.NET首先反序列化_ViewState中的值,将其还原为对象,而后再将这些还原的值从新赋值给其对应的控件。
做为全部控件包括Page类基类的System.Web.UI.Control类型中包含一个LoadViewState(object savedState)方法。其中须要被载入的数据就是经过参数savedState进行传递的。LoadViewState和前面所说的SaveViewState是相对应的方法。并且和SaveViewState方法相似的是,Control.LoadViewState也是简单的调用了StateBag中的LoadViewState方法。经过查看LoadViewState的源代码能够发现,这个函数实际就是将savedState中存储的名值对从新Add到StateBag列表中(StateBag.Add(key, value))。同时咱们从LoadViewState也能够发现在.NET Framework 1.1中传入的object变量是一个pair类型的变量。pair类型包含两个属性First, Second都是object类型的变量,在ViewState中其中一个属性存储的是包含ViewState.Item.Key的ArrayList而另一个属性包含的是ViewState.Item.Value的ArrayList,相对应的Key和Value在ArrayList中的下标相同。而后StateBag类就经过遍历两个ArrayList将值添加到状态项中(注意在.NET Framework 2.0中这个方法的实现有些小小的改动,放弃使用Pair类型而仅仅使用一个ArrayList, ArrayList中每一个名值对占两个Item, 前一个为key后一个为value, 循环的时候以步进2进行循环)。这里须要注意的是从LoadViewState()从新载入到ViewState的数据仅仅包含前一次请求被标记为Dirty的那些数据(注意不是当次请求(current request),而是前一次请求(previous request)就是当前请求的前一次请求。)在载入_ViewState中包含的数据以前,对应控件的ViewState中可能已经包含了一些值了,好比那些静态控件中预先声明好的值(如:<asp:Label Text="abc"/>中的Text属性在LoadViewState()以前就已是"abc"了)。若是LoadViewState()中须要载入的数据中已经存在值了,那么对应的值将被新值所覆盖。
为了让你们有一个完整的认识,这里将页面回传之后发生的事情再简单的描述一下。首先页面回传之后,整个Page将从新生成而且那些页面上声明的静态控件也都已经被解析添加到以Page为根节点的控件树中,那些静态控件对应的静态声明的属性值也都被初始化。而后是OnInit阶段,在这个阶段ASP.NET会调用TrackViewState方法,今后之后全部对控件属性的赋值操做都将致使被跟踪。接着就是LoadViewState()方法被调用,这里那些从_ViewState中反序列化出来的值将被从新赋给对应的控件,因为在此以前TrackViewState()已经被调用了,_ViewState中包含的数据对应的属性值都会被标记为Dirty。这样当调用SaveViewState的时候,这些属性值仍是会被持久的保留到_ViewState中,这样在页面的一次次回传和页面一次次的从新创建的过程当中,这些控件的值就被保留下来了。如今是否是有种豁然开朗的感受?恭喜你,你如今已是一个ViewState管理的小小专家了:)。
一些常见的ViewState使用错误(IMPROPER USE OF VIEWSTATE) 到目前为止咱们已经大体了解了ViewState运行机制了,咱们能够再次回顾一下咱们在使用ViewState中的一些错误,而后分析其缘由。有些错误在你了解了ViewState之后是显而易见,可是有些错误却比较隐蔽,可是经过对这些错误的深刻分析将会让你对ViewState有进一步的了解。
错误使用ViewState的状况(CASES OF MISUSE)
1. 为服务器端控件(webcontrol)设置默认值(Forcing a Default)
注:这里我我的认为原文的例子存在问题,因此我这里按照本身的理解来谢。你们若是看了原文有不一样的理解的话,欢迎和我进行交流。
这个错误是开发服务器端控件(WebControl)中最多见的错误,不过这个错误修改起来很是的简单,并且修改后的代码会更加的简洁明了(事情每每就是这样,约正确的方式,越优的方式每每也是最简明的方式。be simple is good)。形成这种错误的缘由每每是开发人员没有了解ViewState的跟踪机制或者根本就不知道有跟踪机制这种说法。咱们来看一个例子,咱们如今须要一个空间,这个控件有一个Text属性,若是没有对Text进行赋值,那么就从一个Session变量中获得其默认值。咱们的程序员Joe写下了以下代码:
(注:这里我将if (!this.IsPostBack) 的条件设置为if (this.Text == null)就是指当Text属性没有赋值时,那么就赋初值。)
以上代码存在一个问题,第一个问题是Joe花了大力气为控件设置一个Text,他但愿使用者能够对这个控件赋值。Jane是其中一个使用者,她写下了以下的代码:
当Jane查看其页面HTML源代码的时候,她发现她的页面ViewState的体积也变大了。天哪,要知道Jane的页面上仅仅只有Joe的那个控件了。还了,你知道世界上的男女关系啦,Jane确定是去让Joe去修改他这个蹩足的控件了,不过让人高兴的是这回Joe修改后的控件彷佛工做的很好了。这就是Joe的第二次实现方式:
看看这段代码,多么简洁!并且Joe也没必要再去重写控件的OnLoad方法了。这个时候Jane再次使用了这个控件,当Jane设置了控件的Text属性时,她将获得她先前设置的值。若是Jane没有设置值,那么她将获得Session["SomeSessionKey"]中存储的默认值。而且Jane也发现她的页面HTML源码的ViewState大小并无由于添加了Joe的控件而增长。你们都很开心!那么前面的代码为何会存在问题呢:
1. 为何第一种实现方式会使页面的ViewState大小变大?
这里先要说明的是,若是在使用JoesControl的时候赋了初值,以下:
这样和后面的实现方式在现实上也是没有区别的。由于这里并无执行this.Text = Session["SomeSessionKey"]这个语句,天然this.Text并不认为出现了变化,那么ViewState["Text"]并不会被标记为Dirty,因此也不会被序列化到_ViewState中。如今咱们讨论一下若是没有设置Text属性初值的状况,那么这个时候就会在JoesControl的OnLoad方法中执行this.Text = Session["SomeSessionKey"]这个语句,可是这个时候各个控件已经执行完成了OnInit阶段,因此TrackViewState()已经调用,这个时候this.Text已经被标记为Dirty了,因此会被持久化到_ViewState隐藏变量中,这样就增长了ViewState的大小。那么若是使用了第二种方法,判断是否设置了初值,若是没有那么就经过Session["SomeSessionValue"]中的默认值替代,这个阶段是在生成JoesControl(New JoesControl)的时候进行赋值的,这个时候因为还未到达OnInit阶段,因此TrackViewState()方法尚未被调用,因此ViewState["Text"]并不会被标记为Dirty,固然也就不会记录到_ViewState中进行持久化。因此第二种实现方式是优于第一种实现方式的。
2. 持久化静态数据(Persisting static data)
咱们这里所说的静态数据是那些不会被改变的数据(never change)或者在页面的生命周期中、一个用户会话中不会被改变的数据。仍是咱们可爱的程序员Joe,最近他又接到了一个改造网站的任务,在他们公司的eCommerce网站上显示那些已经登陆的用户,好比“嗨,XXXX,欢迎回来!”Joe的前提条件是这个网站已经有了一个业务层的API,能够经过CurrentUser.Name的方法方便的获得当前已经验证的用户姓名。剩下的把这我的名显示到页面上的工做就看Joe的了。如下是Joe的代码:
好了,F5,运行,一切正常,Joe又开始得意洋洋了。可是咱们知道其实这里Joe仍是犯了个错误。用户的名称不只仅会显示在Label中,一样还会被序列化到_ViewState中,并根据页面/服务器之间的来来回回而不停的被序列化、反序列化...。这个开销是值得的吗?Joe耸耸肩说,这有什么关系,就那么几个字节而已。可是能够节约一点为何不节约呢,并且解决的方法仍是如此的简单。第一种方法,不用修改源代码,直接禁用Label控件的ViewState,如:
好了,问题解决了。可是是否有更加好的解决方法呢?有!Label控件多是ASP.NET中最最被高估的控件了。这个多是因为那些WinForm的VB编程者,在WinForm中若是要显示一些文本信息,你可能须要一个Label。而ASP.NET中的这个Label可能被认为和WinForm中的Label是等价的了。可是真的就是这样的吗?经过HTML源码咱们能够看到Label控件实际被解析成了HTML中的<span>标签。你必须问问你本身是否真的须要这个<span>标签呢?若是不须要涉及到特定的格式,仅仅是显示信息那么我以为答案是否认的。请看:
恩,这样你就能够避免生成一个<span>标签了,而且能够很好的解决问题。可是从编程习惯上来讲,这种将前台和后台代码混合的形式是不提倡的,这样会使代码的可读性降低,而且使开发的职责没法明确区分。因此这里还能够使用一种ASP.NET中存在可是确被Label控件的光环笼罩的控件 -- Literal。这个控件仅仅将其Text中的内容输出到客户端,而且不会生成<span>标签。是否是以为对这个控件有些印象,对了,前面在说道将页面解析成一个控件树的时候,第二层通常由三个控件组成,一个是Literal,用于存储到<form>标签之前的全部html代码。就是这个控件。如下就是使用Literal控件来替代Label控件的方法。固然这里也须要将EnableViewState设置为false。问题解决了的同时,咱们节省了网络传输的资源。不错!
3. 持久化廉价的数据(Persisting cheap data)
这个问题实际上包含了第一个问题。静态数据每每是很容易就能够获得的(取得的开销
/成本比较小),可是并非全部容易取得的数据都是静态数据。可能这些数据会不停
的被更改,可是整体来讲获得这些数据的成本很低。一个典型的例子是美国各个州的列表。
除非你要回到1787年12月7日(here),那么当前美国的全部州列表在短时间内是不会有改变的。
固然咱们如今的程序员都很痛恨硬编码。“让我把美国各个州的列表都静态的写在页面
上?傻子才这样作呢。”咱们更加倾向于将州名都保留在一个数据库(或者其余易于
修改的配置文件中。),这样若是州名或者州的列表出现了任何变化,就不用修改源
代码了。恩,我彻底赞成这一点,咱们的著名程序员Joe也是这样认为的,并且这张表
在他们公司已经存在了,表名叫作USSTATES,这回Joe的任务就是和操做这张表有关系的。
下面是用于显示美国各个州列表的下拉菜单(DropDownList):
这里显示的是绑定从数据库中取得的美国州列表的数据代码:
因为美国50个州是在OnLoad阶段中被绑定到下拉菜单(DropDownList)中的,因此这些信息在绑定到下拉菜单的同时,还被序列化并被记录到了ViewState中了。天哪,那可能一个庞大的数据,特别是对于那些低速接入网络的用户。你知道吗,我好几回都想给个人奶奶讲解为何网络这么慢(那是由于你的电脑正在请求全部美国的州呢,能不慢吗?),可是我想个人奶奶是不会懂了。我想她可能会开始跟我说,在她年轻的时候美国只有46个州。那4个新增的州,真是可恶,它们拉慢了咱们的网络。可是咱们又有什么办法呢?(咱们可都是平民百姓。:D)
这个问题和上面提到的静态数据有些相似,一种比较通用的解决方法就是将控件的EnableViewState属性设置为False。可是这种解决方法并非万能药,好比咱们如今的例子,若是Joe仅仅是将用于显示美国各州的DropDownList的EnableViewState控件设置为false,而且将OnLoad函数中的!Page.IsPostBack的限制条件去掉(这样就保证每次载入页面后DropDownList都会被从新绑定,而不会再页面回传之后致使DropDownList中的数据丢失。),那么在使用的时候,Joe就会发现他有麻烦了。什么麻烦呢?“当页面回传之后,Joe发现他先前选择的州并非下拉菜单(DropDownList)中的默认值。”(注意这里的DropDownList是静态控件才会出现上面说的这种状况,若是是在OnLoad中动态生成的DropDownList控件而后再绑定数据那么不会出现此问题)怎么会这个样子!!这是对ViewState的另一个误解。下拉菜单之因此没有保留页面回传前的选择值并非由于咱们禁用了下拉菜单的ViewState。在页面回传的时候还有一些用于获取页面信息的控件值并非经过ViewState来进行保存的,他们是经过名值对的方式经过Http请求(HttpRequest)的方式进行回传的,这些值被称为回传值(PostData)(能够经过将回传方式修改成GET来从URL中查看存在哪些回传值)。因此即使是咱们禁用了DropDownList的ViewState,DropDownList依然能够将那个选择的值回传服务器。这里之因此下拉菜单(DropDownList)会在页面回传后“忘记”上次选择的值是由于在OnLoad阶段以前的ProcessPostData已经对DropDownList设置了默认值,可是这个时候DropDownList尚未ListItem,天然没法设置到最后一次回传选择的值。而后是OnLoad事件中对DropDownList进行数据绑定,可是因为没有执行ProcessPostData方法因此不会再次设置默认值。前面的括号中有说明,若是这个DropDownList控件也是在OnLoad中动态生成的,那么因为进度追赶,在OnLoad阶段后还会从新执行一次ProcessPostData,在这里又会把下拉菜单中的值设置为默认值,因此说以上描述的问题仅仅只有在DropDownList为静态控件的时候才会存在。幸运的是咱们解决这个问题的方法也很简单,咱们将绑定数据的代码移动到OnInit阶段,这个阶段将先于ProcessPostData执行,因此下拉菜单将被设置为最后一次回传的默认值。
上面这种方法适用于几乎全部的廉价数据(cheap data,容易得到的,得到的代价很低的数据)。你可能会反驳我说若是每次都从新去取数据,如:每次都链接数据库去取得对应的数据可能会比将数据存储在ViewState中代价更高,可是我不这样认为。当前的数据库管理系统(DBMS, 如SQL Server)已经至关的成熟,它们每每具备良好的缓存机制,若是配置得当的话执行的效率也很是高。(译者:我也有这样的经验,我曾经比较两种处理数据的方式,其中一种是先取得一个大范围的数据,而后在代码中经过循环的方式将其中不符合条件的数据过滤掉;另一种方式是直接经过SQL语句在数据库中进行数据筛选,而后将符合条件的数据进行返回。根据页面显示的速度来判断,后者的执行效率远远高于前者。)其实想一想究竟是将一堆无用的数据经过56kbps的速度和千里以外的客户传来传去仍是将少量的数据在可能只相距几百英尺的应用服务器和数据库服务器之间传递(它们之间的链接速度通常都高于10M)的代价高,这个结果应该已经很明显了。固然若是你必定想精益求精的话,那么你能够选择把一些经常使用不易变的数据缓存起来这样能够进一步的提升性能。
若是须要实现这样一个需求那么咱们前面的作法(简单的将Label的EnableViewState属性设置为false)将不能解决这个问题,由于若是用户经过按钮取消了时间的显示,因为Label的ViewState被禁用,那么就意味着Label的值在回传之间不会被保存,因此在下次页面回传之后Label依然会显示当前的日期和时间。那么Joe须要怎么作呢?可怜的Joe老是被无穷无尽的需求折磨着。
实际上上面的例子描述的就是一个逻辑,Label控件必须按照逻辑来决定应该显示什么内容。上面的逻辑咱们简化的说就是,对于Label的初值咱们不但愿它保留在ViewState中而之后若是出现了改变那么咱们但愿都保留在ViewState中,以便在页面回传的过程当中进行状态的保留。从这个表述咱们能够看出,若是咱们能在控件的TrackViewState()被调用前为其赋初值,那么什么问题都解决了。可是前面我提到过,ASP.NET并无提供一种简单的方法来实现这个过程(在TrackViewState()被调用前进行操做)。在ASP.NET 2.0的版本中已经为咱们提供了一些先于OnInit阶段的阶段(如:OnPreInit阶段),这里针对ASP.NET 1.1版本,咱们确实没有一个先于OnInit阶段进行控件的初值设置(其实这个表述是不正确的,在ASP.NET 1.1中你能够经过重写DeterminePostBackMode方法来实现对控件进行赋初值,因为这个方法先于OnInit方法,因此此时赋的初值是不会被记录到ViewState中)。一下做者提供了另外两种实现方法:
这里在构造函数中就对Label.Text的属性进行初值赋值,必定是在TrackViewState()方法以前,因此这样也能够达到咱们前面提到的目的。
5. 以编码的方式建立动态控件(Initializing dynamically created controls programmatically)
好了,咱们如今考虑的是那些被动态建立的控件(例子中是Label控件)何时开始跟踪它的ViewState呢?咱们知道咱们能够在页面生命周期的任何阶段动态生成控件并添加到页面的控件树中,可是ASP.NET中是在OnInit阶段调用TrackViewState()以开始跟踪控件ViewState的变化。那么咱们这里动态建立的控件是否会因为错过了OnInit事件从而致使不能对动态生成的控件的状态进行跟踪和持久化呢?答案是否认的,这个奥秘就是Controls.Add()方法,这个方法并不像咱们原来使用ArrayList.Add方法仅仅是将一个Object添加到一个列表中,Controls.Add()方法在将子控件添加到当前控件下后还须要调用一个叫作AddedControl()的方法,就是这个方法对于那些新加入的控件状态进行检查,若是发现当前控件的状态落后于页面的生命周期,那么将会调用对应的方法使当前控件的状态和页面声明周期保持一致,这个过程叫作“追赶(catch up)”。好比咱们举一个稍稍极端的例子,咱们在页面生命周期的OnPreRender阶段动态生成了一个控件并将其添加到当前页面的控件树中,那么系统发现新添加的控件并非出于OnPreRender状态便会调用方法使这个控件经历LoadViewState,LoadPostBackData,OnLoad等方法(页面声明周期中的一些私有方法将被忽略),直到这个控件也到了OnPreRender状态。其实经过查看Temporary ASP.NET Files中编译过的ASP.NET aspx页面的类代码你就能够发如今建立页面控件树的时候,调用的是一个叫作__BuildControlTree()的方法,里面对于添加子控件使用的是AddParsedSubObject()方法,而这个方法实际就是调用了Controls.Add()方法,一样的过程。
咱们再回到Joe编写的用户自定义组件,因为CreateChildControls没法肯定在什么时候被调用,若是页面已经执行到了OnInit阶段,那么只要调用了Controls.Add()方法那么这个控件立刻就会被调用TrackViewState()方法,并当即开始对ViewState进行跟踪。而Joe的代码是在this.Contorls.Add(l)以后再对Label进行初值赋值(l.Text = “Joe’s Label!”),这样”Joe’s Label!”将被添加到ViewState进行保存。那么知道了一切缘由都源于Controls.Add()方法后,解决方法也就出来了,咱们只要颠倒一些最后两个语句的顺序就能够解决问题,代码以下所示:
很玄妙是吧?理解了这个咱们再回头看看咱们前面提到的经过下拉菜单(DropDownList)列举美国全部州的名称的例子。在前面提供的解决方法中,咱们是先禁用DropDownList的ViewState,而后在OnInit阶段对DropDownList进行数据绑定。那么咱们这里又提供了一个新的解决方法。首先在页面中去掉静态声明的DropDownList,而后在页面生命周期OnLoad阶段前的任何位置动态生成DropDownList,而且对其进行值的绑定,而后经过Controls.Add()方法将其添加到页面控件树中,一样能够达到同样的效果。
这样作的好处还有,因为DropDownList的EnableViewState = true, 因此DropDownList依然能够触发诸如OnSelectedIndexChanged事件。你也能够对一样的方法操做DataGrid控件,可是可能对于使用DataGrid的排序(sorting),分页(paging)还有SelectedIndex属性仍是存在问题??(这几个问题尚未考究过)
到目前为止,若是你理解了这篇文章中所说的东西那么恭喜你,你已经知道ViewState是怎样实现其功能的了。知道了ViewState的工做原理咱们就能够写出更加优化的代码,而每每这些更优的代码比那些蹩足的代码更加简洁明了。Enjoy it!