您的位置:首页 > 编程语言 > ASP

ASP.NET ViewState详解

2016-02-19 19:02 579 查看
ViewState可以用来做什么?  

[b]  [/b]这里列举的每一项都是ViewState需要完成的主要工作,我们将根据这些工作来学习ViewState是如何实现这些功能。

  1,以键值对的方式来存控件的值,和Hashtable的结构类似;

  2,跟踪那些ViewState中出现改变的值,以便对这些脏数据(dirty)进行进一步的处理;

  3,通过序列化将ViewState中的值保存在页面的隐藏域(HiddenField)中(这是默认的持久化方式),并通过反序列化得到对应的ViewState对象以便进行相应的操作;

  4,在页面回传的过程中自动的存储ViewState中的跟踪的值。

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

  1,自动保存一个类中变量的状态,无论是private,protected还是public的变量;

  2,可以在页面回传的过程中记住所有状态值;

  3,只要有了ViewState那么每次页面请求时重新构造的数据的操作是不必要的了;

  4,ViewStateisnotresponsibleforthepopulationofvaluesthatarepostedsuchasbyTextBoxcontrols(althoughitdoesplayanimportantrole)ViewState并不存储那些通过Post键值对回传的数据值(如TextBox的TextBox.Text);

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

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

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

ViewState["Key1"]=123.45M;//storeadecimalvalue
ViewState["Key2"]="abc";//storeastring
ViewState["Key3"]=DateTime.Now;//storeaDateTime


ViewState["Key1"]=123.45M;//storeadecimalvalue
ViewState["Key2"]="abc";//storeastring
ViewState["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属性是按如下形式存储的:


publicstringText
{
get{return_text;}
set{_text=value;}
}


publicstringText
{
get{return_text;}
set{_text=value;}
}


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


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


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


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

publicclassMyClass
{
privatestring_text="DefauleValue";
publicstringText
{
get{return_text;}
set{_text=value;}
}
}


<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的数据)。


4.自动恢复数据(AUTOMATICALLYRESTORESDATA)
到此为止我们已经说到了ViewState最后一个功能,那就是自动恢复数据。有些文章将这个过程和上面提到的反序列化过程混淆在一起,这样的理解是不正确的,实际上自动恢复数据的过程并不是反序列化过程的一部分。ASP.NET首先反序列化_ViewState中的值,将其还原为对象,然后再将这些还原的值重新赋值给其对应的控件。

作为所有控件包括Page类基类的System.Web.UI.Control类型中包含一个LoadViewState(objectsavedState)方法。其中需要被载入的数据就是通过参数savedState进行传递的。LoadViewState和前面所说的SaveViewState是相对应的方法。而且和SaveViewState方法类似的是,Control.LoadViewState也是简单的调用了StateBag中的LoadViewState方法。通过查看LoadViewState的源代码可以发现,这个函数实际就是将savedState中存储的名值对重新Add到StateBag列表中(StateBag.Add(key,value))。同时我们从LoadViewState也可以发现在.NETFramework1.1中传入的object变量是一个pair类型的变量。pair类型包含两个属性First,Second都是object类型的变量,在ViewState中其中一个属性存储的是包含ViewState.Item.Key的ArrayList而另外一个属性包含的是ViewState.Item.Value的ArrayList,相对应的Key和Value在ArrayList中的下标相同。然后StateBag类就通过遍历两个ArrayList将值添加到状态项中(注意在.NETFramework2.0中这个方法的实现有些小小的改动,放弃使用Pair类型而仅仅使用一个ArrayList,ArrayList中每个名值对占两个Item,前一个为key后一个为value,循环的时候以步进2进行循环)。这里需要注意的是从LoadViewState()重新载入到ViewState的数据仅仅包含前一次请求被标记为Dirty的那些数据(注意不是当次请求(currentrequest),而是前一次请求(previousrequest)就是当前请求的前一次请求。)在载入_ViewState中包含的数据之前,对应控件的ViewState中可能已经包含了一些值了,比如那些静态控件中预先声明好的值(如:<asp:LabelText="abc"/>中的Text属性在LoadViewState()之前就已经是"abc"了)。如果LoadViewState()中需要载入的数据中已经存在值了,那么对应的值将被新值所覆盖。

为了让大家有一个完整的认识,这里将页面回传以后发生的事情再简单的描述一下。首先页面回传以后,整个Page将重新生成并且那些页面上声明的静态控件也都已经被解析添加到以Page为根节点的控件树中,那些静态控件对应的静态声明的属性值也都被初始化。然后是OnInit阶段,在这个阶段ASP.NET会调用TrackViewState方法,从此以后所有对控件属性的赋值操作都将导致被跟踪。接着就是LoadViewState()方法被调用,这里那些从_ViewState中反序列化出来的值将被重新赋给对应的控件,由于在此之前TrackViewState()已经被调用了,_ViewState中包含的数据对应的属性值都会被标记为Dirty。这样当调用SaveViewState的时候,这些属性值还是会被持久的保留到_ViewState中,这样在页面的一次次回传和页面一次次的重新建立的过程中,这些控件的值就被保留下来了。


 错误使用ViewState的情况(CASESOFMISUSE)

  1为服务器端控件赋默认值(ForcingaDefault);

  2,持久化静态数据(Persistingstaticdata);

  3,持久化廉价数据(Persistingcheapdata);

  4,以编码的方式初始化子控件(Initializingchildcontrolsprogrammatically);

  5,以编码的方式创建控件(Initializingdynamicallycreatedcontrolsprogrammatically)。

1.为服务器端控件(webcontrol)设置默认值(ForcingaDefault)

这个错误是开发服务器端控件(WebControl)中最常见的错误,不过这个错误修改起来非常的简单,而且修改后的代码会更加的简洁明了(事情往往就是这样,约正确的方式,越优的方式往往也是最简明的方式。besimpleisgood)。造成这种错误的原因往往是开发人员没有了解ViewState的跟踪机制或者根本就不知道有跟踪机制这种说法。我们来看一个例子,我们现在需要一个控件,这个控件有一个Text属性,如果没有对Text进行赋值,那么就从一个Session变量中得到其默认值。我们的程序员Joe写下了如下代码

<abc:JoesControlid="joe1"runat="server"Text="ViewStaterocks!"/>

这样和后面的实现方式在现实上也是没有区别的。因为这里并没有执行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(NewJoesControl)的时候进行赋值的,这个时候由于还未到达OnInit阶段,所以TrackViewState()方法还没有被调用,所以ViewState["Text"]并不会被标记为Dirty,当然也就不会记录到_ViewState中进行持久化。所以第二种实现方式是优于第一种实现方式的。

2.持久化静态数据(Persistingstaticdata)

我们这里所说的静态数据是那些不会被改变的数据(neverchange)或者在页面的生命周期中、一个用户会话中不会被改变的数据。还是我们可爱的程序员Joe,最近他又接到了一个改造网站的任务,在他们公司的eCommerce网站上显示那些已经登录的用户,比如“嗨,XXXX,欢迎回来!”Joe的前提条件是这个网站已经有了一个业务层的API,可以通过CurrentUser.Name的方法方便的得到当前已经验证的用户姓名。剩下的把这个人名显示到页面上的工作就看Joe的了。以下是Joe的代码:

(ShoppingCart.aspx)
<!--用于显示登录用户姓名的Label控件-->
<asp:Labelid="lblUserName"runat="server"/>

(ShoppingCart.aspx.cs)
//用于在Label中动态显示登录用户姓名的代码;

<asp:Labelid="lblUserName"runat="server"EnableViewState="false"/>

好了,问题解决了。但是是否有更加好的解决方法呢?有!Label控件可能是ASP.NET中最最被高估的控件了。这个可能是由于那些WinForm的VB编程者,在WinForm中如果要显示一些文本信息,你可能需要一个Label。而ASP.NET中的这个Label可能被认为和WinForm中的Label是等价的了。但是真的就是这样的吗?通过HTML源码我们可以看到Label控件实际被解析成了HTML中的<span>标签。你必须问问你自己是否真的需要这个<span>标签呢?如果不需要涉及到特定的格式,仅仅是显示信息那么我觉得答案是否定的。请看:

<%=CurrentUser.Name%>

恩,这样你就可以避免生成一个<span>标签了,并且可以很好的解决问题。但是从编程习惯上来说,这种将前台和后台代码混合的形式是不提倡的,这样会使代码的可读性下降,并且使开发的职责无法明确区分。所以这里还可以使用一种ASP.NET中存在但是确被Label控件的光环笼罩的控件--Literal。这个控件仅仅将其Text中的内容输出到客户端,并且不会生成<span>标签。是不是觉得对这个控件有些印象,对了,前面在说道将页面解析成一个控件树的时候,第二层一般由三个控件组成,一个是Literal,用于存储到<form>标签以前的所有html代码。就是这个控件。以下就是使用Literal控件来替代Label控件的方法。当然这里也需要将EnableViewState设置为false。问题解决了的同时,我们节省了网络传输的资源。不错!

<asp:Literalid="litUserName"runat="server"EnableViewState="false"/>

3.持久化廉价的数据(Persistingcheapdata)

这个问题实际上包含了第一个问题。静态数据往往是很容易就可以得到的(取得的开销成本比较小),但是并不是所有容易取得的数据都是静态数据。可能这些数据会不停的被更改,但是总体来说得到这些数据的成本很低。一个典型的例子是美国各个州的列表。除非你要回到1787年12月7日(here),那么当前美国的所有州列表在短期内是不会有改变的。当然我们现在的程序员都很痛恨硬编码。“让我把美国各个州的列表都静态的写在页面上?傻子才这样做呢。”我们更加倾向于将州名都保留在一个数据库(或者其他易于修改的配置文件中。),这样如果州名或者州的列表出现了任何变化,就不用修改源代码了。恩,我完全同意这一点,我们的著名程序员Joe也是这样认为的,而且这张表在他们公司已经存在了,表名叫做USSTATES,这回Joe的任务就是和操作这张表有关系的。

下面是用于显示美国各个州列表的下拉菜单(DropDownList):


  <asp:DropdownListid="lstStates"runat="server"DataTextField="StateName"DataValueField="StateCode"/>

这里显示的是绑定从数据库中取得的美国州列表的数据代码:

<asp:DropdownListid="lstStates"runat="server"DataTextField="StateName"DataValueField="StateCode"EnableViewState="false"/>

<asp:Labelid="lblDate"runat="server"/>

<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将显示一个“空时间”(即:“--/--/------:--:--”),此按钮的响应代码为:

  <asp:Labelid="Label2"runat="server"OnInit="lblDate_Init"/>

  同样在后台编写Label.OnInit事件对应的响应函数并对Label.Text赋初值也是可以的。

  2.创建用户自定义组件(Createacustomcontrol):

ViewCode
这里在构造函数中就对Label.Text的属性进行初值赋值,一定是在TrackViewState()方法之前,所以这样也可以达到我们前面提到的目的。

5.以编码的方式创建动态控件(Initializingdynamicallycreatedcontrolsprogrammatically)

这个实际上和上面的就是一个问题,由于到目前为止我们对ViewState已经有了一定深度的了解,所以我们解决起问题来就更加的得心应手。让我们来看看我们的老朋友Joe编写的一个用户自定义组件,这个组件重写了Control类的一个CreateChildControls方法,动态生成了一个Label控件。代码如下:


ViewCode
好了,我们现在考虑的是那些被动态创建的控件(例子中是Label控件)什么时候开始跟踪它的ViewState呢?我们知道我们可以在页面生命周期的任何阶段动态生成控件并添加到页面的控件树中,但是ASP.NET中是在OnInit阶段调用TrackViewState()以开始跟踪控件ViewState的变化。那么我们这里动态创建的控件是否会由于错过了OnInit事件从而导致不能对动态生成的控件的状态进行跟踪和持久化呢?答案是否定的,这个奥秘就是Controls.Add()方法,这个方法并不像我们原来使用ArrayList.Add方法仅仅是将一个Object添加到一个列表中,Controls.Add()方法在将子控件添加到当前控件下后还需要调用一个叫做AddedControl()的方法,就是这个方法对于那些新加入的控件状态进行检查,如果发现当前控件的状态落后于页面的生命周期,那么将会调用对应的方法使当前控件的状态和页面声明周期保持一致,这个过程叫做“追赶(catchup)”。比如我们举一个稍稍极端的例子,我们在页面生命周期的OnPreRender阶段动态生成了一个控件并将其添加到当前页面的控件树中,那么系统发现新添加的控件并不是出于OnPreRender状态便会调用方法使这个控件经历LoadViewState,LoadPostBackData,OnLoad等方法(页面声明周期中的一些私有方法将被忽略),直到这个控件也到了OnPreRender状态。其实通过查看TemporaryASP.NETFiles中编译过的ASP.NETaspx页面的类代码你就可以发现在创建页面控件树的时候,调用的是一个叫做__BuildControlTree()的方法,里面对于添加子控件使用的是AddParsedSubObject()方法,而这个方法实际就是调用了Controls.Add()方法,同样的过程。

我们再回到Joe编写的用户自定义组件,由于CreateChildControls无法确定在何时被调用,如果页面已经执行到了OnInit阶段,那么只要调用了Controls.Add()方法那么这个控件马上就会被调用TrackViewState()方法,并立即开始对ViewState进行跟踪。而Joe的代码是在this.Contorls.Add(l)之后再对Label进行初值赋值(l.Text=“Joe’sLabel!”),这样”Joe’sLabel!”将被添加到ViewState进行保存。那么知道了一切原因都源于Controls.Add()方法后,解决方法也就出来了,我们只要颠倒一些最后两个语句的顺序就可以解决问题,代码如下所示:

ViewCode
很玄妙是吧?理解了这个我们再回头看看我们前面提到的通过下拉菜单(DropDownList)列举美国所有州的名称的例子。在前面提供的解决方法中,我们是先禁用DropDownList的ViewState,然后在OnInit阶段对DropDownList进行数据绑定。那么我们这里又提供了一个新的解决方法。首先在页面中去掉静态声明的DropDownList,然后在页面生命周期OnLoad阶段前的任何位置动态生成DropDownList,并且对其进行值的绑定,然后通过Controls.Add()方法将其添加到页面控件树中,同样可以达到一样的效果。

ViewCode
这样做的好处还有,由于DropDownList的EnableViewState=true,所以DropDownList依然可以触发诸如OnSelectedIndexChanged事件。你也可以对同样的方法操作DataGrid控件,但是可能对于使用DataGrid的排序(sorting),分页(paging)还有SelectedIndex属性还是存在问题??(这几个问题还没有考究过).


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: