(转)理解ASP.NET中的ViewState

以前做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["Key1"= 123.45M// store a decimal value
ViewState["Key2"= "abc"// store a string
ViewState["Key3"= DateTime.Now; // store a DateTime

StateBag实现了System.Web.UI.IStateManager接口,该接口包含三个方法:

void TrackViewState ();
object SaveViewState ();
void LoadViewState (object state);

这些方法后面将依次用到。

做为StateBag的实例,ViewState是System.Web.UI.Control类的属性,其声明

protected virtual StateBag ViewState { get; }

所有的服务器控件、用户控件、页面都继承了Control类,所以他们都具有StateView属性。

服务端控件把几乎所有的属性值都存入它的ViewState里。比如一个Text属性,是这样实现的:

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

而不是这样:

private string _text = "Default 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.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()方法后,任何赋值都会把Item标记为dirty,即使新值和已有的值相同:

stateBag["key"= "abc";
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方法相同:

protected virtual object SaveViewState ()

每个控件的SaveViewState()方法都会调用该控件的ViewState的SaveViewState()方法,并返回object。这个object里,并不包含ViewState里所有的Item,而只包含那些被标记为dirty的Item。

所以MSDNSaveViewState()的文档说得也不对:

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的隐藏字段:

<form name="aspnetForm" method="post" action="discuss.aspx" id="aspnetForm">
    
<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方法相同:

void LoadViewState (object state)

这也是System.Web.UI.Control上的方法,所以,所有的服务器控件、用户控件、页面都有这个方法。

将object树中对应的object作为参数传给控件的LoadViewState()方法时,控件的LoadViewState()方法会把object传给该控件的ViewState的LoadViewState()方法,最终ViewState就获得了一系列在上一轮请求中被标记为dirty的Item(即键值对)。如果这些键值对已经存在于ViewState中,则用得到的新Item(即键值对)覆盖已有的。

5. ViewState优化实例

5.1 声明静态控件

考虑一个页面,Page1.aspx,有且仅有一个控件:

<asp:Label id="label1" runat="server" Text="" />

在另一个页面Page2.aspx中,有且仅有一个控件:

<asp:Label id="label1" runat="server" Text="We the people of the United States, in order to form a more perfect union, establish justice, insure domestic tranquility, provide for the common defense, promote the general welfare, and secure the blessings of liberty to ourselves and our posterity, do ordain and establish this Constitution for the United States of America." />

显然,Page1.aspx和Page2.aspx的__VIEWSTATE字段的大小是相当的。因为控件的属性都没有在Init之后动过,所以没有数据会被序列化到__VIEWSTATE字段中。

5.2 强制设定控件属性的默认值

考虑下面的控件:

public class TestControl : WebControl
{
    
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 class TestControl : WebControl
{
    
public string Text
    {
        
get
        {
            
return ViewState["Text"== null ? GetDefaultText() : ViewState["Text"as string;
        }
        
set
        {
            
this.ViewState["Text"= value;
        }
    }
}

5.3 声明控件并绑定数据

假设ShoppingCart.aspx中需要显示当前登陆的用户名:

<asp:Literal id="litUserName" runat="server" />

ShoppingCart.aspx.cs:

protected void Page_Load(object sender, EventArgs e)
{
    litUserName.Text 
= CurrentUser.Name;
}

这样也会造成litUserName的Text值每次都必然被序列化到__VIEWSTATE中,而当前登陆的用户名显然是不需要持久化的。

可以直接关闭litUserName的EnableViewState属性:

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

或者在控件的构造函数中绑定数据,构造函数内的绑定将在生命周期开始之前执行:

public class UserNameLiteral : Literal
{
    
public UserNameLiteral()
    {
        
Text = CurrentUser.Name;
    } 
}

或者在控件生命周期的Init阶段绑定数据也可以:

<asp:Literal id="litUserName" runat="server" OnInit="litUserName_Init" />

或者在页面生命周期的PreInit阶段绑定数据:

protected void Page_PreInit(object sender, EventArgs e)
{
    litUserName.Text 
= CurrentUser.Name;
}

注意,可以在控件的Init阶段绑定数据,但不应该在页面的Init阶段绑定数据。因为整个控件树的Init阶段是递归的,页面是控件树的根节点,所以页面的Init一定最后发生。即页面进入Init时,所有控件都已经完成了Init。如果这时绑定数据,就会造成数据被序列化到__VIEWSTATE。应该在页面的PreInit阶段,向控件绑定数据。

5.4 动态创建控件并绑定数据

考虑如下创建子控件的代码:

public class TestControl : Control
{
    
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中。

正确的做法是先绑定数据,再加入控件树:

public class TestControl : Control
{
    
protected override void CreateChildControls()
    {
        Literal litTest 
= new Literal();
        litTest.Text 
= "Test";
        
Controls.Add(litTest);
    }
}

6. 总结

一句话,就是:从页面的Init阶段开始,ASP.NET会监视哪些控件的哪些属性被动过了;一旦有属性在Init之后被动过了,就会被扔到页面上的__VIEWSTATE隐藏字段里去。

7. 参考

  1. TRULY Understanding ViewState
  2. Essential ASP.NET 2.0

转自:http://www.cnblogs.com/dixin/archive/2007/09/19/understanding-asp_net-viewstate.html

原文地址:https://www.cnblogs.com/greatandforever/p/1605104.html