HTTP协议是无状态的,每当客户端对Web服务器进行一次页面请求时,服务器都会将本次请求看成是一次全新的请求;即使它在0.0001秒之前还响应过对相同页面地址、来自相同客户端的请求,0.0001秒之后它就已经把上次自己做过的事情给忘光了。以ASP.NET为例,每次有一个页面请求到服务器,服务器在响应过程中都会创建该页面的System.Web.UI.Page的实例,即使有连续两次来自相同客户端、对相同页面的请求,服务器为这两次请求创建的Page实例并不会共享。因此,开发者如果需要保存上一次页面已存在的某些状态,以便在下一次请求中使用(如一次Postback过程),那么就必须使用ASP.NET的状态管理来实现。
ASP.NET中常用的状态管理方法有(QueryString、form表单传值等这里不列举):
一,应用程序状态(Application):为所有用户保存的变量,对所有用户有效;存储在服务器端;站点级;
二,会话状态(Session):为单个用户保存的变量,对单个用户有效;存储在服务器端;用户级;
三,Cookie:存储在浏览器端;站点级;
四,视图状态(ViewState):存储在单个页面中;页面级;
五,控件状态(ControlState):存储在单个页面中;页面级;
其中应用程序状态、会话状态、Cookie等状态并不是ASP.NET特有的状态,在JSP、PHP中都有类似的实现方式;而视图状态与控件状态(控件状态可以看成是对视图状态的补充)则是ASP.NET特有的状态管理方式,虽然二者本质上都是利用form表单提交实现的,但是由于微软其进行了很好的封装,因此使用起来非常方便。由于理解视图状态的实现原理对于理解ASP.NET的页面周期过程非常关键,因此本文将对视图状态与控件状态进行较详细的介绍,页面生命周期将在下篇文章中详细介绍。
一,视图状态的使用
无论是在服务器控件、用户控件还是在页面后台中,使用视图状态都是统一用ViewState属性,如下例在页面后台Page_Load事件中使用:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) { ViewState["test"] = "test"; }
else { string test = ViewState["text"] == null ? "" : ViewState["text"].ToString(); }
}
上面的示例中,页面首次请求页面时,将ViewState["test"]中存入”test”字符串;而在页面进行一次Postback后,则将之前存入ViewState中值赋值到变量test中,通过使用VIewState保存了变量在上一次请求后的值。接下来的示例用于对比使用视图状态的属性与普通属性之间的区别,代码如下(两个属性都位于页面后台中,也就是页面Page实例的属性):
普通属性:
private string _test = ”默认文本”;
public string Test //属性1
{
get { return _test; }
set { _test = value; }
}
使用视图状态的属性:
public string Test //属性2
{
get { return ViewState["test"] == null ? "默认文本" : ViewState["test"].ToString(); }
set { ViewState["test"] = value; }
}
可以看出上面两个属性试图起到的效果是一致的,即如果未给该属性赋值,则其值为”默认文本”,否则属性值为被设定的值。然而,运行页面后可以发现,倘若使用属性1,则无论页面的任何事件和方法中给Test赋值后,Test的新值只能在本次请求中才有效,而在下一次Postback后Test属性的值又被还原为”默认文本”,其原因很明显:因为服务器端在每次页面请求中都重新生成Page的新实例,上次请求的Page实例已经在经历页面周期的Unload后被回收了,新的Page实例的Test就是”默认文本”。改用属性2后,下次Postback后发现,上次请求过程中给Test赋的新值被保留,ViewState["test"]的值已经不是null,而是上一次Test的值。
二,ViewState在服务端
无论是在服务器控件、用户控件还是在页面后台使用的ViewState实质上都是继承于System.Web.UI.Control的ViewState属性,用Reflector反编译System.Web程序集,查看System.Web.UI.Control的ViewState属性,截图如下:
可以发现,ViewState属性的类型是System.Web.UI.StateBag,查看该类型的定义,如下图所示对它的说明:
可以确定该类就是为了实现对视图状态(或类似功能)的管理而设计的。为了深入研究ViewState就必须先对StateBag类有足够的了解,下面对StateBag类的重要属性和方法进行介绍:
索引器this[string Key]:用Reflector反编译得到的代码如下:
public object this[string key]
{
get
{
if (string.IsNullOrEmpty(key))
{
throw ExceptionUtil.ParameterNullOrEmpty("key");
}
StateItem item = this.bag[key] as StateItem;
if (item != null)
{
return item.Value;
}
return null;
}
set
{
this.Add(key, value);
}
}
它使用字符串作为key获取内部维护的变量列表中对用的object值,而给索引赋值则调用Add(key, value)方法来添加新的键/值对。
Add(key, value)方法:添加一个新的键/值对。
程序集内方法internal voidTrackViewState():这是一个非常关键的方法,该方法在程序集外不能被调用。该方法的作用在于开始追踪(Track)并记录 ViewState的值的变化情况。
IsItemDirty(string key)方法:这个方法非常重要,指示对应Key键的值是否已经变的“Dirty”了,你可能会猜想应该只有当ViewState[key]的值发生变化后才会将该方法返回True,实际上无论ViewState[key]在赋值前后的是否发现变化,只要是在TrackViewState方法后的赋值行为,IsItemDirty(key)都会返回True。因为对它这种设定的理解非常重要,因此下面举例进行说明,实例代码如下:
System.Web.UI.StateBag _StateBag = new System.Web.UI.StateBag(); //这里实例化一个StateBag类
_StateBag["Test0"] = 0; //添加一个新的键/值
Console.WriteLine("Test0:" + _StateBag["Test0"].ToString()); //查看值
Console.WriteLine(_StateBag.IsItemDirty("Test0")); //IsItemDirty("Test0")返回False,表示未将该值标志为“Dirty”
_StateBag.TrackViewState(); //开始对内部键值进行追踪,注意:这里是不能直接调用的,因为该方法使用internal修饰的,只有本程序集内才能被调用,只是模拟该方法在System.Web.UI.Control内被调用的情形而已。
_StateBag["Test0"] = 0; //再次对其进行复制,但是与旧值相同,都是0
Console.WriteLine(_StateBag.IsItemDirty("Test0")); //会发现这里返回True,因为已经调用过TrackViewState方法,即使前后的赋值没有变化。
上述就是System.Web.UI.StateBag类的最重要的几个成员,当然它还有若干其他的方法和属性这里就不一一列举了,因为这几个成员对于理解ASP.NET中ViewState的工作原理非常重要,因此这里将其较详细地进行介绍。
关于ViewState如果在ASP.NET中实现原理与ASP.NET页面生命周期息息相关,页面周期将在下一篇文章中进行详细介绍,所以本文先进行概括性的描述,详尽的理解读者可以在理解下一篇关于页面周期的文章后回过头来翻看本篇文章,相信一定能够有更加深刻的理解。这里将其概括为:
1, 浏览器对服务器发出对某页面的请求。
2, 服务器创建该页面的实例,调用其内部方法,进入页面生命周期(这里先不考虑IIS、Application、Module、Handler的作用)
3, 经过一系列方法到达PreInit之后,各个服务器控件根据ASPX页面上已有的设置生成自身的实例,如<asp:Button ID="aspbtn_Test" runat="server"Text="测试按钮" />就会生成System.Web.UI.WebControls.Button类的实例,并将“aspbtn_Test”赋值到属性ID中,将“测试按钮”赋值到Text属性中,当然,这些属性都是类似于前面的“属性2”的方式实现的,因此这些值都被记录到相应的StateBag实例中。
4, 在事件Init之后调用StateBag的TrackViewState方法,开始对ViewState的赋值进行追踪,并记录哪些值会变得“Dirty”。
5, 如果本次请求非首次请求,在页面生命周期中的InitComplete事件与PreLoad事件之间,会执行LoadViewState操作,即加载本次Postback传递的ViewState的旧值,以及执行ProcessPostData方法加载PostBack传递的其他变量,从而根据这些数据生成各个服务器控件。
6, 经历事件Load、LoadComplete、PreRender、PreRenderComplete
7, 执行SaveViewState方法,将所有已经被标志为“Dirty”的键值序列化后以字符串编码的方式列出来,并且存储在一个类型为Hidden的Input标签中并在Render过程中保存(因为读者会看到,常规ASPX页面的源代码都有一个name和id为“__VIEWSTATE”的Hidden类型的Input标签,并且它的值像是一套乱码)
8, 执行Render过程,各个服务器控件都将解析为Html标签以及Javascript代码,最后返回给浏览器。
9, 下次Postback时,本次已经保存的ViewState值就能够提交给服务器,以供服务器在新的生命周期中根据这些ViewState的值在LoadViewState方法进行加载,并将各个服务器控件还原为“最新”状态。
下面用图形直观地展现视图状态在页面生命周期中的实现过程:
读者可能已经发现,正式因为有TrackViewState和IsItemDirty(key)的配合,才实现了只将需要存储的ViewState的值通过上述方式进行存储,服务器控件的大多数属性是无需保存ViewState的,有以下两类:
1, 因为很多值如果没有对其进行赋值的话,就会采用默认值(如null,空字符串等等),根本不需要将这些默认值也通过ViewState的方式存储及还原。
2, ASPX页面本身就已经有对一些属性的赋值,如Button的Text,如果不对其进行重新赋值的话,每次进行生命周期都只需读取服务器ASPX文件上的文本即可。
三,ViewState在客户端
对Session与ViewState都有一定的使用经验的人会发现,它们的使用方式非常相似,一个是Session[Key],一个是ViewState[Key],的,如果不去思考(包括应用程序状态)这些状态之前的机理而随便的滥用,就会导致非常严重的性能问题。比如,某些状态只可能在本页面中使用,在其他页面根本用不到,此时如果不假思索地使用Session保存是非常浪费效率的,因为Session的维护需要额外的系统开销(通过Web.Config可以配置Session的维护模式,但无论是哪种模式都是需要占用额外的系统资源);量级比较大的数据则不宜使用ViewState保存,原因在于ViewState是绑定在单个页面上的,每次Postback都会将ViewState的值传输回服务器,但数据量巨大时,这个传输过程占用的服务器宽带和时间是非常可观的。
ViewState在客户端的形态是怎样的?如果对上小节“ViewState在服务端”已经基本理解的读者,那么对这个问题是没有任何迟疑的。一个页面内的所有服务器控件中需要的所有ViewState值序列化一系列已转化的字符串(默认使用Base64编码,可以进行额外加密,ASP.NET默认通过设置EnableViewStateMac为True防篡改,但未加密)保存在一个类型为Hidden的Input标签中。ASP.NET用这种方式对其保存的目的在于,当Postback时,Hidden类型Input的Value就可以通过form表单提交给服务器,服务器对传回的字符串进行解析还原为服务器控件的各个成员(属性,索引器等等),也就实现了保存页面状态的目的。下面的截图就是一个典型的ViewState在页面的示例:
下面是一次PostBack中客户端向服务器传输的数据列表,其中包含ViewState数据:
可以发现,向服务器传输的数据中,__VIEWSTATE占了大头,有90%以上。这些数据都是需要提交给服务器的,而服务器再经过页面生命周期后返回的__VIEWSTATE的值只可能多不会少。可以想象,如果开发者将大型的数据存入ViewState会是什么后果?将导致客户端与服务器之间传输的数据量大大增加,将严重影响服务器的响应速度。
四,使用VIewState的注意事项
根据上述ViewState的实现原理,在使用视图状态时就应该注意一下事项:
a) 禁止某些服务器控件中的ViewState功能,以减少保存在__VIEWSTATE中的字符串大小:向LinkButton和Button这样的控件,仅仅需要使用它们的点击事件时(Text属性可以在页面上设置,无需保存在ViewState中),就完全可以将它们的ViewState功能禁止掉。禁止ViewState功能有以下几种方式:
i, 在Web.Config中配置<pagesenableViewState="false"/>,这种方式将导致整个Web项目的ViewState功能都被禁止,而一般情况下还是需要保留ViewState功能的,它方便了开发者的编程,需要禁止的只是部分页面或服务器控件而已。
ii,在Page配置选项中配置<%@ Page EnableViewState="false" %>,这种方式将禁止该页面内的ViewState功能。
iii,在特定服务器控件中配置,如<asp:TextBox ID="TextBox1" runat="server" EnableViewState="false">,这种方式只对该服务器控件有效,不会影响到其他服务器控件。
b) 无论在页面还是服务器控件后台编程中,都不要为了图方便不假思索地将变量存放在视图状态中,要对比其带来的便利与性能损耗,以达到二者之间的平衡点。
c) 对于服务器控件中无需保留状态的属性(请注意此处描述!)的赋值应该在页面周期的Init事件之前执行,因为在该事件之后才通过调用TrackViewState方法来进行追踪,而在此之前的赋值行为不会被标志为“Dirty”,也就不会记录到最终的__VIEWSTATE中。
d) 切勿在Init事件后往ViewState中添加大数据,比如不要再视图状态中保存一个大量数据的DataTable,宁可实时需要实时从数据库中获取。
e) 切勿将机密数据存放在ViewState中,因为这些值都是暴露在浏览器中的,即使经过加密也不能保证不被其他人破解。
五,ViewState的安全性问题
如果ASP.NET的设计者们是傻瓜的话,那么他们可能会将视图状态的值直接以“a=0&b=1&c=2”的方式存储在__VIEWSTATE隐藏字段中,这样在页面周期中写入与读取视图状态会非常简单。但是,既然设计者们都能够设计出这么巧妙的视图状态的整个实现过程,那么他们就肯定早已考虑好了其中的安全性问题。既然__VIEWSTATE隐藏字段的值对任何人来说都是公开的,那么就必须保证其他人不能还原ViewState的真实值,只能看到一堆乱码。
在MSDN中关于视图状态安全性的说明文档中,把视图状态的安全性隐患归结为以下几点:
l 假冒
l 篡改
l 否认
l 信息泄露
l 拒绝服务
l 提升权限
本文就最为常见的信息泄露和篡改的安全性问题进行较详细的说明。
首先来做一个简单的测试来展示视图状态在安全性方面的隐患,新建一个空的ASPX页面,后台的代码为:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ViewState["Test0"] = "Value0";
ViewState["Test1"] = "Value1";
ViewState["Test2"] = "Value2";
ViewState["Test3"] = "Value3";
}
}
上述代码的作用在于:在页面首次请求时给在视图状态中赋了几个简单的值,而在PostBack中则不重复赋值。运行该页面后查看该页面的源代码,得到__VIEWSTATE字段的值为“/wEPDwUKMjA0OTM4MTAwNA8WCB4FVGVzdDAFBlZhbHVlMB4FVGVzdDEFBlZhbHVlMR4FVGVzdDIFBlZhbHVlMh4FVGVzdDMFBlZhbHVlM2Rk7V/Jp5oRqLagu/U6eWfFXNIbV0A=”。这些乱码一样的字符串开始起似乎安全性已经有了保障,但是实际上它只经过了序列化为二进制格式和转化base64格式等公开的转化方式,可以通过对应的反向操作来还原为视图状态的各个键值对,实际上.NET FrameWork中的System.Web.UI.LosFormatter类就能轻松将其“破解”,如下示代码:
LosFormatter formatter = new LosFormatter();
object viewstateObj = formatter.Deserialize("/wEPDwUKMjA0OTM4MTAwNA8WCB4FVGVzdDAFBlZhbHVlMB4FVGVzdDEFBlZhbHVlMR4FVGVzdDIFBlZhbHVlMh4FVGVzdDMFBlZhbHVlM2Rk7V/Jp5oRqLagu/U6eWfFXNIbV0A=");
在第二行代码处添加断点,监视viewstateObj对象,如下面截图所示:
可以看到,用LosFormatter类还原一系列System.Web.UI.Pair 对象,类似于字典类型,结果与在后台Page_Load事件中的赋值一致。
通过上述简单的测试,发现如果仅采用ASP.NET默认措施(默认只打开EnableViewStateMac=”True”,实现防篡改功能),那么其他人就能够非常轻松将原开发者保存在视图状态中的值还原出来,导致所谓的视图状态的“信息泄露”。防止信息泄露的方法就是对输出视图状态的值进行加密,当然,ASP.NET的设计者已经为开发者们早就准备的实现方法,单个服务器环境中,只需配置一个属性ViewStateEncryptionMode即可。配置该属性有两种方式:
1, 在Page首行设置中配置单个页面,如:<%@ Page ViewStateEncryptionMode="Always" %>
2, 在Web.Config中配置所有页面,如:
<configuration>
<system.web>
<pages viewStateEncryptionMode="Always">
</system.web>
</system.web>
在单服务器环境中,只启用ViewStateEncryptionMode就足够了,但是在多服务器环境中就没有那么简单了。实际上配置viewStateEncryptionMode="Always"后,ASP.NET会在输出__VIEWSTATE时使用对称加密,对称加密密钥是由ASP.NET自动产生的,各个服务器之间产生的对称密钥是不相同的,因此在单服务器环境中,这种方式没有问题;而在多服务器环境因为服务器与服务器之间的负载平衡请求将会出错,因为它们之间的加密密钥不匹配,不能正确解析彼此的视图状态。因此,在多服务器环境中必须在各个服务器中配置相同的加密密钥,配置方法如下:
<configuration>
<system.web>
<machineKey decryption="AES" decryptionKey="abcdefg.......(这里是自定义对称加密密钥)">
</system.web>
</configuration>
值得一提的是,读者可能会想:既然视图状态信息这么容易泄露,那为什么ASP.NET不默认在页面上使用ViewStateEncryptionMode="Always"进行加密呢?这个问题就是安全性与效率之间的平衡问题,加密解密算法是要耗费一定响应时间的,加密方法越复杂,耗费时间越多,ASP.NET设计者们可能会假设开发者不会将机密数据存储在视图状态中,所有默认不进行加密。
除信息泄露威胁外,篡改是视图状态的另一项严重威胁。篡改指的是,其他人修改页面的已有视图状态值,然后提交给服务器,从而让服务器生成带有危险JS代码的Html。
ASP.NET 为防止篡改也提供了相应功能,即通过配置EnableViewStateMac属性实现,配置该属性方法同ViewStateEncryptionMode类似:
1, 在Page首行设置中配置单个页面,如:<%@ Page EnableViewStateMac="True" %>
2, 在Web.Config中配置所有页面,如:
<configuration>
<system.web>
<pages enableViewStateMac="true">
</system.web>
</configuration>
实际上ASP.NET默认就在Page中配置了该属性为True。为了更直观的说明该属性的作用,可以先做一个对比试验,沿用上面已有的例子(默认enableViewStateMac="true"),然后设置页面的enableViewStateMac="false",两次最终生成的__VIEWSTATE的结果分别如下:
输出1:/wEPDwUKMjA0OTM4MTAwNA8WCB4FVGVzdDAFBlZhbHVlMB4FVGVzdDEFBlZhbHVlMR4FVGVzdDIFBlZhbHVlMh4FVGVzdDMFBlZhbHVlM2Rk7V/Jp5oRqLagu/U6eWfFXNIbV0A=
输出2:/wEPDwUKMjA0OTM4MTAwNA8WCB4FVGVzdDAFBlZhbHVlMB4FVGVzdDEFBlZhbHVlMR4FVGVzdDIFBlZhbHVlMh4FVGVzdDMFBlZhbHVlM2Rk
可以看出:输出1比输出2要多出"7V/Jp5oRqLagu/U6eWfFXNIbV0A="这样一段,实际上这段字符串就是为实现防篡改而添加的消息身份验证代码(MAC)。
ASP.NET验证__VIEWSTATE是否已经在外部被篡改的的原理如下:在生成视图状态时会使用MAC密钥对视图状态数据序列化后的二进制格式序列创建对应的哈希值,并添加到其尾部,之后被编码为 base64 字符串存入__VIEWSTATE隐藏字段;当该页面的请求提交的服务器时,ASP.NET会根据返回的视图状态数据再通过相同的方式重新计算生成另一个哈希值,如果这个哈希值与提交到服务器中视图状态字符串中包含的哈希值一致,就认为视图状态值未被篡改;否则认为已经被篡改,继而抛出异常。
同ViewStateEncryptionMode一样,ASP.NET默认提供自动生成的加密密钥,同时可以对MAC密钥进行配置,在多服务器环境中配置MAC密钥也需要注意使用同一个MAC密钥,配置示例如下(实际上视图状态加密密钥与视图状态验证MAC密钥在相同的地方配置):
<configuration>
<system.web>
<machineKey validation="AES" validationKey="abcdefg.......(这里是自定义对称加密密钥)">
</system.web>
</configuration>
下面用图形来直观的展现视图状态防篡改加密过程:
六,控件状态的简单介绍和使用
控件状态是ASP.NET2.0新加的,它的出场就跟足球队的替补球员似的,它跟视图状态的关系就像:视图状态是主力,控件状态是替补(虽然这么说可能有失偏颇,但这样的比喻能够增强对二者的理解)。前面说到视图状态是能够通过属性设置被关闭的,比如<asp:Label ID="Label1" runat="server"EnableViewState="false" >,而服务器控件的大多数属性内部都是通过视图状态实现的,那么就很可能出现这样一种情况:某个服务器控件有个关键属性内部是使用视图状态保存的,倘若视图状态被关闭(无论是页面级还是控件级),那么这个关键属性就不能正常地使用,最终导致整个服务器控件都将失效。因此,为了防止出现这种“服务器控件开发者与ASP.NET开发之间沟通不协调”的情况,于是控件状态就应运而生了。控件状态与视图状态的基本实现机制是相似的,都是将输出放置在页面的隐藏字段中,但是它有一个最大异同点,那就是不能被关闭。这样设计的目的在于使服务器控件开发者能够更大限度的控制服务器控件的运行结果,即便该控件的实际使用者对此控件的实现机制完全不了解。服务器控件开发者通过将关键属性通过控件状态存储而不是视图状态,从而保证了该服务器控件在任何情况下都能够正常运行,即使使用者关闭了视图状态。因此可以说,控件状态是ASP.NET设计者为服务器控件开发者特别新增的功能。
基于上述原因,控件状态的使用一般情况下只用于开发服务器控件中,页面后台基本不会使用(虽然Page的本质也是一种控件)。应用控件状态比起视图状态要复杂一些,需要做的代码工作如下(其余的实现过程ASP.NET都已经为开发者考虑好了):
1, 在初始化过程中(OnInit事件处理方法)调用RegisterRequiresControlState方法(告诉ASP.NET:我要使用控件状态了!);
2, 重写SaveControlState和LoadControlState方法。前者用于在控件状态中保存数据,后者用于从控件状态加载数据。
下面为使用控件状态的简单示例,并与视图状态进行对比,新建一个Web应用程序,并添加一个cs类文件,创建一个自定义服务器控件,该服务器控件代码如下:
using System;
using System.Web.UI;
namespace ControlStateTestControl
{
public class TestControl : Control
{
private int currentIndex_ControlState = 0;
/// <summary>
/// 使用控件状态的属性
/// </summary>
public int CurrentIndex_ControlState
{
get { return currentIndex_ControlState; }
set { currentIndex_ControlState = value; }
}
/// <summary>
/// 使用视图状态的属性
/// </summary>
public int CurrentIndex_ViewState
{
get { return ViewState["CurrentIndex"] == null ? 0 : Convert.ToInt32(ViewState["CurrentIndex"]); }
set { ViewState["CurrentIndex"] = value; }
}
#region 实现控件状态
// 重写OnInit事件处理程序
protected override void OnInit(EventArgs e)
{
Page.RegisterRequiresControlState(this);
base.OnInit(e);
}
// 重写SaveControlState方法
protected override object SaveControlState()
{
return currentIndex_ControlState != 0 ? (object)currentIndex_ControlState : null;
}
// 重写LoadControlState方法
protected override void LoadControlState(object state)
{
if (state != null) { currentIndex_ControlState = (int)state; }
}
#endregion
protected override void Render(HtmlTextWriter writer)
{
writer.Write("使用视图状态的属性值为:" + CurrentIndex_ViewState);
writer.Write("<br/>使用控件状态的属性值为:" + CurrentIndex_ControlState);
base.Render(writer);
}
}
}
服务器控件TestControl说明:
1, CurrentIndex_ViewState属性使用视图状态存储,而CurrentIndex_ControlState属性则使用控件状态保存数据。
2, 重写Render方法,最终呈现结果分别显示两个属性的值。
在空页面中使用该服务器控件,首先需要对该控件进行注册:
<%@ Register Assembly="ControlStateTestControl" Namespace="ControlStateTestControl" TagPrefix="TestControl" %>
页面代码:
<form id="form1" runat="server">
<TestControl:TestControl ID="TestControl0" runat="server" EnableViewState="false">
</TestControl:TestControl>
<asp:Button ID="Button1" runat="server" Text="Button" οnclick="Button1_Click" />
</form>
页面后台代码:
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
TestControl0.CurrentIndex_ControlState++;
TestControl0.CurrentIndex_ViewState++;
}
运行该页面,多次点击按钮,可以发现使用控件状态的属性值每点击一次都会加1,而使用视图状态的值在点击第二次之后就一直显示为1,无论点击多少次都不会再增长。
原因分析:控件TestControl0通过设置EnableViewState=false来禁止视图状态,因此每次PostBack时ViewState["CurrentIndex"]都为null,CurrentIndex_ViewState属性返回0,最后在Button1_Click事件中+1变成1,所以每次点击按钮CurrentIndex_ViewState属性都为1。而控件状态是不能被关闭的,所有每次PostBack,上一次CurrentIndex_ControlState属性的值都会被记录,因此没点击按钮一次,该属性值就+1。