以前做EasyFramework的时候,对ViewState的控制一直是个问题。后来看了一篇强文,才有点搞清楚了。由于原文很长,下面照本宣科,把要点讲一下。
在Google中搜索ASP.NET ViewState,第一条结果就是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而不是私有字段,那么这个属性值将在往返传送的过程中被持久化。
这让人感觉,你扔给控件的任何东西,控件都会扔给ViewState,然后都会在服务器和浏览器之间往返。这是不对的。
1. ViewState如何存储数据
ViewState是System.Web.UI.StateBag的实例。StateBag是一个HashTable,键为string,值为object:
ViewState["Key2"] = "abc"; // store a string
ViewState["Key3"] = DateTime.Now; // store a DateTime
StateBag实现了System.Web.UI.IStateManager接口,该接口包含三个方法:
object SaveViewState ();
void LoadViewState (object state);
这些方法后面将依次用到。
做为StateBag的实例,ViewState是System.Web.UI.Control类的属性,其声明为
所有的服务器控件、用户控件、页面都继承了Control类,所以他们都具有StateView属性。
服务端控件把几乎所有的属性值都存入它的ViewState里。比如一个Text属性,是这样实现的:
{
get
{
return ViewState["Text"] == null ? "Default Value" : (string)ViewState["Text"];
}
set
{
ViewState["Text"] = value;
}
}
而不是这样:
public string Text
{
get
{
return _text;
}
set
{
_text = value;
}
}
可以看出:
- 如果没有设置Text属性,Text属性会返回默认值;
- 如果设置Text属性为null,Text属性会恢复默认值。
2. 追踪ViewState的变化
StateBag能track其item(即键值对)的变化。一旦调用了StateBag的TrackViewState()方法,便开始了track;并且一旦开始track,就无法停止track。
调用TrackViewState()后,对StateBag中任何item的赋值都会导致该item被标记为dirty。
可以调用IsItemDirty(string key)方法来查看一个item是不是dirty的。
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()方法后,任何赋值都会把Item标记为dirty,即使新值和已有的值相同:
stateBag.IsItemDirty("key"); // returns false
stateBag.TrackViewState();
stateBag["key"] = "abc";
stateBag.IsItemDirty("key"); // returns true
3. ASP.NET页面的生命周期中发生了什么
先从《Essential ASP.NET 2.0》上抄来一个图:
ASP.NET在页面早期的Init阶段,会调用StateBag的TrackViewState()方法。
在后期的Control/View state saved阶段,会在页面的控件树上递归地调用每个控件的SaveViewState()方法。其声明与ViewState的SaveViewState方法相同:
每个控件的SaveViewState()方法都会调用该控件的ViewState的SaveViewState()方法,并返回object。这个object里,并不包含ViewState里所有的Item,而只包含那些被标记为dirty的Item。
所以MSDN上SaveViewState()的文档说得也不对:
Returns the server control's current view state. If there is no view state associated with the control, this method returns a null reference.
返回服务器控件的当前视图状态,如果没有与该控件关联的视图状态,则返回null。
显然,返回的不是服务器控件的当前视图状态,而是服务器控件的当前视图状态中,那些被标记为dirty的Item——也就是控件在页面Init之后,被赋过值的那些属性(这将在5.1中证明)。
递归地调用每个控件的SaveViewState()方法之后,就得到了一个和控件树对应的object树。
4. __VIEWSTATE的序列化与反序列化
在浏览器中打开一个ASP.NET网页,查看源代码,能看到一个叫__VIEWSTATE的隐藏字段:
<div>
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwULLTE3NTEwNDc2MzkPZBYCZg9kFgICAw9kFgICBw9kFgICAw9kFgJmD2QWAgIJDw8WBB4zTm9Cb3RfUmVzcG9uc2VUaW1lS2V5X2N0bDAwJGNwaENvbnRlbnQkbm9ib3REaXNjdXNzBhil/FydyclIHjFOb0JvdF9TZXNzaW9uS2V5S2V5X2N0bDAwJGNwaENvbnRlbnQkbm9ib3REaXNjdXNzBUFOb0JvdF9TZXNzaW9uS2V5X2N0bDAwJGNwaENvbnRlbnQkbm9ib3REaXNjdXNzXzYzMzI1ODkwMDMwOTM3NTAwMGQWAgIBDxYCHg9DaGFsbGVuZ2VTY3JpcHQFigF2YXIgbm9Cb3RQYW5lbCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdjdGwwMF9jcGhDb250ZW50X25vYm90RGlzY3Vzc19ub0JvdFBhbmVsODU1Jyk7IG5vQm90UGFuZWwub2Zmc2V0V2lkdGggKiBub0JvdFBhbmVsLm9mZnNldEhlaWdodDtkGAEFGmN0bDAwJGNwaENvbnRlbnQkZ3ZEaXNjdXNzD2dklPRHPPVfgLRRNU0+Dz9vtmyz3MY=" />
</div>
</form>
这就是那棵object树序列化之后的结果。
我以前用EasyFramework做的网页里,这个字符串要翻好几屏才会完,哈哈。
当form回传时,ASP.NET会接收到这一串字符。将它反序列化,就又得到了和控件树对应的object树。
之后调用控件的LoadViewState()这个方法。其声明与ViewState的LoadViewState方法相同:
这也是System.Web.UI.Control上的方法,所以,所有的服务器控件、用户控件、页面都有这个方法。
将object树中对应的object作为参数传给控件的LoadViewState()方法时,控件的LoadViewState()方法会把object传给该控件的ViewState的LoadViewState()方法,最终ViewState就获得了一系列在上一轮请求中被标记为dirty的Item(即键值对)。如果这些键值对已经存在于ViewState中,则用得到的新Item(即键值对)覆盖已有的。
5. ViewState优化实例
5.1 声明静态控件
考虑一个页面,Page1.aspx,有且仅有一个控件:
在另一个页面Page2.aspx中,有且仅有一个控件:
显然,Page1.aspx和Page2.aspx的__VIEWSTATE字段的大小是相当的。因为控件的属性都没有在Init之后动过,所以没有数据会被序列化到__VIEWSTATE字段中。
5.2 强制设定控件属性的默认值
考虑下面的控件:
{
public string Text
{
get
{
return ViewState["Text"] as string;
}
set
{
ViewState["Text"] = value;
}
}
protected void Page_Load(object sender, EventArgs args)
{
if (!IsPostback)
{
Text = GetDefaultText();
}
}
}
这样做的缺陷是,每次页面打开之后,TestControl的Text属性值必然被序列化到__VIEWSTATE中。应该避免在Page_Load中去设置控件属性的默认值:
{
public string Text
{
get
{
return ViewState["Text"] == null ? GetDefaultText() : ViewState["Text"] as string;
}
set
{
this.ViewState["Text"] = value;
}
}
}
5.3 声明控件并绑定数据
假设ShoppingCart.aspx中需要显示当前登陆的用户名:
ShoppingCart.aspx.cs:
{
litUserName.Text = CurrentUser.Name;
}
这样也会造成litUserName的Text值每次都必然被序列化到__VIEWSTATE中,而当前登陆的用户名显然是不需要持久化的。
可以直接关闭litUserName的EnableViewState属性:
或者在控件的构造函数中绑定数据,构造函数内的绑定将在生命周期开始之前执行:
{
public UserNameLiteral()
{
Text = CurrentUser.Name;
}
}
或者在控件生命周期的Init阶段绑定数据也可以:
或者在页面生命周期的PreInit阶段绑定数据:
{
litUserName.Text = CurrentUser.Name;
}
注意,可以在控件的Init阶段绑定数据,但不应该在页面的Init阶段绑定数据。因为整个控件树的Init阶段是递归的,页面是控件树的根节点,所以页面的Init一定最后发生。即页面进入Init时,所有控件都已经完成了Init。如果这时绑定数据,就会造成数据被序列化到__VIEWSTATE。应该在页面的PreInit阶段,向控件绑定数据。
5.4 动态创建控件并绑定数据
考虑如下创建子控件的代码:
{
protected override void CreateChildControls()
{
Literal litTest = new Literal();
Controls.Add(litTest);
litTest.Text = "Test";
}
}
当一个子控件被加入到页面中时,其生命周期会发生追赶。也就是说,如果CreateChildControls()方法是在页面生命周期的PreRender阶段执行的,那么,当Controls.Add(litTest)这一句执行之后,litTest的PreInit,Load,PreRender等会立即发生,以追赶上页面的PreRender阶段。然后,litTest.Text = "Test"这一句显然会导致litTest.Text被序列化到__VIEWSTATE中。
正确的做法是先绑定数据,再加入控件树:
{
protected override void CreateChildControls()
{
Literal litTest = new Literal();
litTest.Text = "Test";
Controls.Add(litTest);
}
}
6. 总结
一句话,就是:从页面的Init阶段开始,ASP.NET会监视哪些控件的哪些属性被动过了;一旦有属性在Init之后被动过了,就会被扔到页面上的__VIEWSTATE隐藏字段里去。
7. 参考
转自:http://www.cnblogs.com/dixin/archive/2007/09/19/understanding-asp_net-viewstate.html