【转】ASP.NET ViewState详解

 

作者:Infinities Loop

概述

ViewState是一个被误解很深的动物了。我希望通过此文章来澄清人们对ViewState的一些错误认识。为了达到这个目的,我决定从头到尾详细的描述一下整个ViewState的工作机制,其中我会同时用一些例子说明我文章中的观点,结论。比如我会用静态控件(declared controls)和动态控件(dynamic controls)两个方面来说明同一个问题。

现在有关ViewState的文章可谓多如牛毛,你可能会说再写有关ViewState的文章无异于炒剩饭(我这篇文章便是:D)。但是我却不这么认为,如果把ViewState看成一匹野马的话,那么这匹野马并没有死去,它还活跃的很,说不定这个时候它正在你的客厅里撒野呢。所以我们有必要再次去把它击倒。不过你也不需要担心,从这篇文章你可以发现其实这匹马也没有那么坏。

我的意思并不是否然目前还没有好好说明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是如此重要。

  1.  
  1. 对ViewState的误解可能导致...      
  2. 导致一些敏感信息被泄漏;
  3. 针对ViewState的攻击(aka the Jedi Mind Trick,aka 是又称作,又叫做的意思。Jedi Mind Trick, 看过星球大战的人对于Jedi一定不陌生,Jedi就是绝地武士。Jedi Mind Trick 是绝地武士的一个招式,可以用于控制对方的思维。有关这个的具体知识可以参见:http://gollum.easycp.de/gollum/gollum.php?a=core&l=en&wl=en&q= 这里作者估计是表达了通过ViewState的攻击来达到控制对方的目的。比如一个等离子电视的价格被修改为了1美元一台)
  4. 很差的性能,在某些极端的情况下可能根本就没有性能。
  5. 并发性差 -- 想象一下如果每次回传的数据都有50kB,那么你的服务器能承受多少并发的访问量呢?
  6. 糟糕的全局设计;
  7. 以上结果一定会让你抓狂的(头痛,反胃,头昏眼花,皱眉头...)。

如果你正在开发基于ASP.NET平台的网络应用程序,并且你无视ViewState存在的话,那么以下的情况可能会发生在你的身上。

The ViewState form field.看看你页面的HTML源代码吧,密密麻麻很恐怖吧。ViewState will add your web app's distinctiveness to it's own. Performance is futile. 沉重的星际垃圾

像如上的例子我还可以举出很多,但是我想这两个例子已经具有代表性了。好了,现在让我们从最开始认识ViewState吧。

  1.   ViewState可以用来做什么?
    这里列举的每一项都是ViewState需要完成的主要工作,我们将根据这些工作 来学习ViewState是如何实现这些功能。
  2. 以名值对的方式来存控件的值,和Hashtable的结构类似;
  3. 跟踪那些ViewState中出现改变的值,以便对这些脏数据(dirty)进行进一步的处理;
  4. 通过序列化将ViewState中的值保存在页面的隐藏域(Hidden Field)中(这是默认的持久化方式),并通过反序列化得到对应的ViewState对象以便进行相应的操作;
  5. 在页面回传的过程中自动的存储ViewState中的跟踪的值。

下面列举的是ViewState不能用来做什么的列表,这个其实比了解ViewState是用来做什么的还重要。

  1. 什么是ViewState不能做的?
  2. 自动保存一个类中变量的状态,无论是private, protected还是public的变量;
  3. 可以在页面回传的过程中记住所有状态值;
  4. 只要有了ViewState那么每次页面请求时重新构造的数据的操作是不必要的了;
  5. ViewState is not responsible for the population of values that are posted such as by TextBox controls (although it does play an important role) ViewState并不存储那些通过Post名值对回传的数据值(如TextBox的TextBox.Text);
  6. 想让ViewState替你泡一杯咖啡,做梦吧:P。

虽然ViewState作为一个整体出现在.NET Framework框架中有它的唯一目的,那就是在页面回传的过程中保存状态值,使原本没有“记忆”的Http协议变得有“记忆”起来。但是上面列举的ViewState的四个主要功能之间却没有太多的关联。所以从逻辑上我们可以将其划分开来,各个击破。

哈哈,这样大小的ViewState是不是更加好下口了?


1. ViewState就是用来存储数据的

如果你曾经使用过HashTable的话,那么你应该明白我的意思了。这里并没有什么高深的理论。ViewState通过String类型的数据作为索引(注意在ViewState中不允许通过整形下表的方式对其中的项进行访问,如:ViewState.Item(0) 的形式是不允许的。)ViewState对应项中的值可以存储任何类型的值,实施上任何类型的值存储到ViewState中都会被装箱为Object类型。以下是几个对ViewState进行赋值的几个例子。

 
  
   
   
ViewState[ " Key1 " ]  =   123.45M ;  //  store a decimal value ViewState[ " Key2 " ]  =   " abc " ;  //  store a string ViewState[ " Key3 " ]  =  DateTime.Now;  //  store a DateTime
 
 
实际上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属性是按如下形式存储的:

   
   
public   string  Text  {         get { return _text; }         set { _text = value; } }

但是你必须注意,上面的形式(通过类的私有字段)并不是大多数ASP.NET 服务器控件存储其属性值得方式。这些控件的属性值大多是通过ViewState来进行存储的。通过Reflector查看TextBox.Text属性的源代码你可以看到类似如下的代码:

   
   
public   string  Text  {         get { return (string)ViewState["Text"]; }         set { ViewState["Text"] = value; } }

为了表示这个观点的重要性,我这里再重申一遍“大多数ASP.NET 服务器控件存储其属性值得方式是通过ViewState的方式存储的,而不是我们通常想象的那样通过类的私有字段来存储。”即便是用于设定服务器控件样式的Style类中的大多数属性值也是通过ViewState来进行存储的。所以在设计自定义的组件时,对于那些需要存储的组件属性值也最好遵循这个方式。这里我还需要着重讲一个问题,在以ViewState为存储方式的情况下,如果实现属性的默认值(default value),我们可能会认为属性值是这样实现的:

public   class  MyClass 
{   
      private string _text = "Default Value!";     
      public string Text
     {       
           get { return _text; }        
           set { _text = value; }    
     }

}


这样如果在对Text的属性没有设置的时候,直接取Text属性,那么我们可以得到默认值"Default Value!"。那么如果我们使用ViewState来存储的时候如何实现默认值呢?如下所示:

   
   
public   string  Text  {          get        {                    return ViewState["Text"] == null ? "Default Value!" :  (string)ViewState["Text"];           }           set        {            ViewState["Text"] = value;        } }

就像操作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跟踪的开关是处于关闭状态的。

 
  
   
   
stateBag.IsItemDirty( " key " );  //  returns false stateBag[ " key " ]  =   " abc " ; stateBag.IsItemDirty( " key " );  //  still returns false  stateBag[ " key " ]  =   " def " ; stateBag.IsItemDirty( " key " );  //  STILL returns false  stateBag.TrackViewState(); stateBag.IsItemDirty( " key " );  //  yup still returns false  stateBag[ " 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 " );  //  returns false stateBag.TrackViewState(); stateBag[ " key " ]  =   " abc " ; stateBag.IsItemDirty( " key " );  //  returns true

         可能你会认为根据赋值前后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)就是那些从页面或者用户自定义控件的源码中可以看到声明代码的控件。如:

   
   
< asp:Label  id ="lbl1"  runat ="server"  Text ="Hello World"   />

这里在页面上声明了一个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.)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ASP.NET ViewState 是一种用于在 Web 应用程序中跨请求存储数据的机制。以下是一个使用 ViewState 的示例: 假设您有一个页面,其中包含一个文本框和一个按钮。用户在文本框中输入一些文本,然后单击按钮。在单击按钮时,将在服务器端处理程序中使用 ViewState 存储文本框中的值,并在页面上显示它。 以下是一个简单的 ASP.NET 页面代码示例,它演示了如何使用 ViewState 存储和检索文本框中的值: ```html <%@ Page Language="C#" %> <!DOCTYPE html> <html> <head runat="server"> <title>ViewState Example</title> </head> <body> <form id="form1" runat="server"> <div> <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox> <br /> <asp:Button ID="Button1" runat="server" Text="Save" OnClick="Button1_Click" /> <br /> <asp:Label ID="Label1" runat="server"></asp:Label> </div> </form> </body> </html> ``` 在按钮单击事件处理程序中,我们将文本框中的值存储ViewState 中,并将其显示在页面上: ```csharp protected void Button1_Click(object sender, EventArgs e) { string text = TextBox1.Text; ViewState["myText"] = text; Label1.Text = "Text saved: " + text; } ``` 在页面加载事件处理程序中,我们检索存储ViewState 中的值,并将其显示在页面上: ```csharp protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { if (ViewState["myText"] != null) { string text = (string)ViewState["myText"]; Label1.Text = "Text retrieved: " + text; } } } ``` 通过这种方式,我们可以在页面上保留用户在文本框中输入的值,即使用户单击其他按钮或导航到其他页面。请注意,ViewState 可能会增加页面大小,并增加网络传输时间。因此,我们应该谨慎使用 ViewState,并仅在必要时使用它。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值