ViewState

ForTechnology發表於2011-08-17

ViewState是一個被誤解很深的動物了。我希望通過此 文章來澄清人們對ViewState的一些錯誤認識。為了達到這個目的,我決定從頭到尾詳細的描述一下整個ViewState的工作機制,其中我會同時用 一些例子說明我文章中的觀點,結論。比如我會用靜態控制元件(declared controls)和動態控制元件(dynamic controls)兩個方面來說明同一個問題。

現在有關ViewState的文章可謂多如牛毛,你可能會說 再寫有關ViewState的文章無異於炒剩飯(我這篇文章便是:D)。但是我卻不這麼認為,如果把ViewState看成一匹野馬的話,那麼這匹野馬並 沒有死去,它還活躍的很,說不定這個時候它正在你的客廳裡撒野呢。所以我們有必要再次去把它擊倒。不過你也不需要擔心,從這篇文章你可以發現其實這匹馬也沒有那麼壞。

我的意思並不是否然目前還沒有好好說明ViewState的 文章,只是我總覺得好像這些文章都缺少一些東西,而這些缺少的東西往往就會導致人們對ViewState的困惑。比如:理解ViewState是怎樣跟蹤 那些已經出現變化的資料(dirty data)就非常重要,但是很多文章卻沒有過多的涉及,或者即便涉及了可能其中卻包含了錯誤的資訊。比如這篇文章(W3Schools) 中就說頁面回傳的值也是儲存在ViewState中的,但是這個觀點是錯誤的。不信是嗎?那麼你在一個頁面上放置一個TextBox控制元件和一個 Button控制元件,然後你在屬性中將TextBoxEnableViewState設定為False,然後通過點選Button回傳頁面,你會發現 TextBox還是仍舊會保留你輸入的值,而不會如你想象的由於TextBoxViewState被禁用了而導致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.0ViewState的實現機制有稍許變動,但是在寫這篇文章的時候我依然是以ASP.NET 1.x的版本為前提進行的。比如說在ASP.NET 2.0的新增了一個新型別的ViewState -- ControlState, 但是實際上ControlStateViewState並不大變,所以我們這裡可以忽略它。

首先讓我們看看為什麼深入瞭解ViewState是如此重要。

ViewState的誤解可能導致...

  1. 導致一些敏感資訊被洩漏;
  2. 針對ViewState的攻擊(aka the Jedi Mind Trickaka 是又稱作,又叫做的意思。Jedi Mind Trick, 看過星球大戰的人對於Jedi一定不陌生,Jedi就是絕地武士。Jedi Mind Trick 是絕地武士的一個招式,可以用於控制對方的思維。有關這個的具體知識可以參見:http://gollum.easycp.de/gollum/gollum.php?a=core&l=en&wl=en&q= 這裡作者估計是表達了通過ViewState的攻擊來達到控制對方的目的。比如一個等離子電視的價格被修改為了1美元一臺)
  3. 很差的效能,在某些極端的情況下可能根本就沒有效能。
  4. 併發性差 -- 想象一下如果每次回傳的資料都有50kB,那麼你的伺服器能承受多少併發的訪問量呢?
  5. 糟糕的全域性設計;
  6. 以上結果一定會讓你抓狂的(頭痛,反胃,頭昏眼花,皺眉頭...)

如果你正在開發基於ASP.NET平臺的網路應用程式,並且你無視ViewState存在的話,那麼以下的情況可能會發生在你的身上。

The ViewState form. field.看看你頁面的HTML原始碼吧,密密麻麻很恐怖吧。

ViewState will add your web app's distinctiveness to it's own. Performance is futile. 沉重的星際垃圾

像如上的例子我還可以舉出很多,但是我想這兩個例子已經具有代表性了。好了,現在讓我們從最開始認識ViewState吧。

ViewState可以用來做什麼?
這裡列舉的每一項都是ViewState需要完成的主要工作,我們將根據這些工作來學習ViewState是如何實現這些功能。

  1. 以名值對的方式來存控制元件的值,和Hashtable的結構類似;
  2. 跟蹤那些ViewState中出現改變的值,以便對這些髒資料(dirty)進行進一步的處理;
  3. 通過序列化將ViewState中的值儲存在頁面的隱藏域(Hidden Field)(這是預設的持久化方式),並通過反序列化得到對應的ViewState物件以便進行相應的操作;
  4. 在頁面回傳的過程中自動的儲存ViewState中的跟蹤的值。

下面列舉的是ViewState不能用來做什麼的列表,這個其實比了解ViewState是用來做什麼的還重要。

什麼是ViewState不能做的?

  1. 自動儲存一個類中變數的狀態,無論是private, protected還是public的變數;
  2. 可以在頁面回傳的過程中記住所有狀態值;
  3. 只要有了ViewState那麼每次頁面請求時重新構造的資料的操作是不必要的了;
  4. 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名值對回傳的資料值(TextBoxTextBox.Text)
  5. 想讓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取代嗎?前面我們可是大肆宣揚了一下StateBagHashTable有多麼的像。雖然他們都是通過名值對的方式來儲存值,但是 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屬性,但是WidthUnit型別的,所以這裡就設計到一個從StringUnit型別的轉化過程。

好,到了這,我們再以前面所說的內容將當前發生的事情再描述一遍。我們已經知道大多數伺服器控制元件的屬性值最終是儲存在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個控 件,它們分別是用於儲存表單(

)
標籤前所有資訊的文字控制元件(Literal),然後是表單控制元件(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.)

突擊測試(POP QUIZ)
如 果你已經讀到了這裡,那麼祝賀你,我要獎勵一下這麼有毅力的你。我們們來個突擊測試如何?我是不是人很好呢?哈哈。題目是這樣的,我們有兩個幾乎一模一樣的.aspx頁面,我們分別稱之為Page1.aspxPage2.aspx, 每個頁面都存在一個form,其中包含一個Label控制元件,如下所示:

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

這兩個頁面唯一的區別是Label中包含的值不同(Label.Text的值)Page1.aspx中的

label1.Text = "abc",如下程式碼所示。

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


那麼對於Page2.aspx中的Label,我們對其多賦一點值(就來個美國憲法的序言吧)。如下程式碼所示。

<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,那麼我們將看到一個abc。然後你通過瀏覽器檢視頁 面的HTML原始碼,你可以找到那個臭名昭著的隱藏欄位(_ViewState)。然後把Page1.aspx_ViewState值保留下來。接著 執行Page2.aspx,同樣保留其_ViewState的值。然後比較這兩個ViewState大小(注意:這裡比較的是大小,或者說比較字串的長度,而不是內容)問題來了,請問這兩個ViewState的大小是否一樣呢?好了,在公佈答案之前我們再去看看另外一個問題,我們在兩個頁面上都增加一個Button控制元件,這樣通過點選Button按鈕我們就可以回傳頁面了。一下就是頁面中宣告Button控制元件的程式碼:

<asp:Button id="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到底包含了什麼資訊呢?你可以用ViewState Decoder工具檢視一下,可以發現在這樣一個簡單的介面,_ViewState僅僅包含了頁面的雜湊程式碼(HashCode)

好,讓我們到第二個問題(頁面加了Button那個情況),答案同樣是:是的,它們的大小也是一樣的。原因和上面解釋的一樣。簡單的說就是在TrackViewState()方法後面並沒有對Label.Text屬性進行賦值操作,所以 ViewState中的項並沒有被標記為Dirty,自然就不會被序列化並記錄到_ViewState隱藏變數中了。

到此為止我們已經基本瞭解了ASP.NET平臺是怎樣決定一個資料是否需要被序列化並永久保留在 _ViewState中了(那些被標記為Dirty的資料)。至於ASP.NET是怎樣序列化這些資料的已經不是本文的範圍了,如果你有興趣進一步瞭解的 話,那麼請參看如下兩篇文章: LosFormatter for ASP.NET 1.x    ObjectStateFormatter for ASP.NET 2.0

在這一個小節的最後,我們要簡單的說說反序列化。反序列化和序列化是相對應的,如果不能通過反序列化來講序列化的物件進行還原,進行進行操作的話,那麼序列化操作將沒有任何意義。但是這是另外一個話題,所以這裡就不再進行贅述。

4.
自動恢復資料(AUTOMATICALLY RESTORES DATA)
到 此為止我們已經說到了ViewState最後一個功能,那就是自動恢復資料。有些文章將這個過程和上面提到的反序列化過程混淆在一起,這樣的理解是不正確的,實際上自動恢復資料的過程並不是反序列化過程的一部分。ASP.NET首先反序列化_ViewState中的值,將其還原為物件,然後再將這些還原的 值重新賦值給其對應的控制元件。

作為所有控制元件包括Page類基類的System.Web.UI.Control型別中包含一個 LoadViewState(object savedState)方法。其中需要被載入的資料就是通過引數savedState進行傳遞的。LoadViewState和前面所說的 SaveViewState是相對應的方法。而且和SaveViewState方法類似的是,Control.LoadViewState也是簡單的呼叫 了StateBag中的LoadViewState方法。通過檢視LoadViewState的原始碼可以發現,這個函式實際就是將savedState 中儲存的名值對重新AddStateBag列表中(StateBag.Add(key, value))。同時我們從LoadViewState也可以發現.NET Framework 1.1中 傳入的object變數是一個pair型別的變數。pair型別包含兩個屬性First, Second都是object型別的變數,在ViewState中其中一個屬性儲存的是包含ViewState.Item.KeyArrayList而 另外一個屬性包含的是ViewState.Item.ValueArrayList,相對應的KeyValueArrayList中的下標相同。然 後StateBag類就通過遍歷兩個ArrayList將值新增到狀態項中(注意在.NET Framework 2.0中這個方法的實現有些小小的改動,放棄使用Pair型別而僅僅使用一個ArrayList, ArrayList中每個名值對佔兩個Item, 前一個為key後一個為value, 迴圈的時候以步進2進行迴圈)。這裡需要注意的是從LoadViewState()重新載入到ViewState的資料僅僅包含前一次請求被標記為Dirty的那些資料(注意不是當次請求(current request),而是前一次請求(previous request)就是當前請求的前一次請求。) 在載入_ViewState中包含的資料之前,對應控制元件的ViewState中可能已經包含了一些值了,比如那些靜態控制元件中預先宣告好的值 (如:中的Text屬性在LoadViewState()之前就已經是"abc")。如果LoadViewState()中需 要載入的資料中已經存在值了,那麼對應的值將被新值所覆蓋。

為了讓大家有一個完整的認識,這裡將頁面回傳以後發生的事情再簡單的描述一下。首先頁面回傳以後,整個Page將重新生成並且那些頁面上宣告的靜態控制元件也都已經被解析新增到以Page為根節點的控制元件樹中,那些靜態控制元件對應的靜態宣告的屬性值也都被初始化。然後是OnInit階段,在這個階段ASP.NET會呼叫TrackViewState方法,從此以後所有對控制元件屬性的賦值操作都將導致被跟蹤。接著 就是LoadViewState()方法被呼叫,這裡那些從_ViewState中反序列化出來的值將被重新賦給對應的控制元件,由於在此之前 TrackViewState()已經被呼叫了,_ViewState中包含的資料對應的屬性值都會被標記為Dirty。這樣當呼叫 SaveViewState的時候,這些屬性值還是會被持久的保留到_ViewState中,這樣在頁面的一次次回傳和頁面一次次的重新建立的過程中,這 些控制元件的值就被保留下來了。現在是不是有種豁然開朗的感覺?恭喜你,你現在已經是一個ViewState管理的小小專家了:)

一些常見的ViewState使用錯誤(IMPROPER USE OF VIEWSTATE)
到 目前為止我們已經大致瞭解了ViewState執行機制了,我們可以再次回顧一下我們在使用ViewState中的一些錯誤,然後分析其原因。有些錯誤在 你瞭解了ViewState以後是顯而易見,但是有些錯誤卻比較隱蔽,但是通過對這些錯誤的深入分析將會讓你對ViewState有進一步的瞭解。
 

            錯誤使用ViewState的情況(CASES OF MISUSE)

    1. 為伺服器端控制元件賦預設值(Forcing a Default);
    2. 持久化靜態資料(Persisting static data);
    3. 持久化廉價資料(Persisting cheap data);
    4. 以編碼的方式初始化子控制元件(Initializing child controls programmatically);
    5. 以編碼的方式建立控制元件(Initializing dynamically created controls programmatically)

1. 為伺服器端控制元件(webcontrol)設定預設值(Forcing a Default)

注:這裡我個人認為原文的例子存在問題,所以我這裡按照自己的理解來謝。大家如果看了原文有不同的理解的話,歡迎和我進行交流。

這個錯誤是開發伺服器端控制元件(WebControl)中最常見的錯誤,不過這個錯誤修改起來非常的 簡單,而且修改後的程式碼會更加的簡潔明瞭(事情往往就是這樣,約正確的方式,越優的方式往往也是最簡明的方式。be simple is good)。造成這種錯誤的原因往往是開發人員沒有了解ViewState的跟蹤機制或者根本就不知道有跟蹤機制這種說法。我們來看一個例子,我們現在需要一個空間,這個控制元件有一個Text屬性,如果沒有對Text進行賦值,那麼就從一個Session變數中得到其預設值。我們的程式設計師Joe寫下了如下代 碼:

public class JoesControl : WebControl 
...{    
      
public string Text 
      
...{        
             
get ...return this.ViewState["Text"] as string; }        
             
set ...this.ViewState["Text"] = value; 
      }    
     
      
protected override void OnLoad(EventArgs args) 
      
...{        
            
if(this.Text == null
            
...{            
    
this.Text = Session["SomeSessionKey"] as string;        
            }         
            
            
base.OnLoad(args);    
      }
}

(注:這裡我將if (!this.IsPostBack) 的條件設定為if (this.Text == null)就是指當Text屬性沒有賦值時,那麼就賦初值。)

以上程式碼存在一個問題,第一個問題是Joe花了大力氣為控制元件設定一個Text,他希望使用者可以對這個控制元件賦值。Jane是其中一個使用者,她寫下了如下的程式碼:

<abc:JoesControl id="joe1" runat="server" />

Jane檢視其頁面HTML原始碼的時候,她發現她的頁面ViewState的體積也變大了。天 哪,要知道Jane的頁面上僅僅只有Joe的那個控制元件了。還了,你知道世界上的男女關係啦,Jane肯定是去讓Joe去修改他這個蹩足的控制元件了,不過讓人 高興的是這回Joe修改後的控制元件似乎工作的很好了。這就是Joe的第二次實現方式:

public class JoesControl : WebControl 
...{    
    
public string Text 
    
...{        
        
get 
        
...{            
            
return this.ViewState["Text"] == null?Session["SomeSessionKey"]:this.ViewState["Text"] as string;        
        }        
        
set 
        
...
            
this.ViewState["Text"] = value; 
        }    
    }
}

看看這段程式碼,多麼簡潔!而且Joe也不必再去重寫控制元件的OnLoad方法了。這個時候Jane再 次使用了這個控制元件,當Jane設定了控制元件的Text屬性時,她將得到她先前設定的值。如果Jane沒有設定值,那麼她將得到 Session["SomeSessionKey"]中儲存的預設值。並且Jane也發現她的頁面HTML原始碼的ViewState大小並沒有因為新增了 Joe的控制元件而增加。大家都很開心!那麼前面的程式碼為什麼會存在問題呢:

1. 為什麼第一種實現方式會使頁面的ViewState大小變大?

這裡先要說明的是,如果在使用JoesControl的時候賦了初值,如下:

<abc:JoesControl id="joe1" runat="server" Text="ViewState rocks!" />

這樣和後面的實現方式在現實上也是沒有區別的。因為這裡並沒有執行this.Text = Session["SomeSessionKey"]這個語句,自然this.Text並不認為出現了變化,那麼ViewState["Text"]並不會被標記為Dirty,所以也不會被序列化到_ViewState中。現在我們討論一下如果沒有設定Text屬性初值的情況,那麼這個時候就會在 JoesControlOnLoad方法中執行this.Text = Session["SomeSessionKey"]這個語句,但是這個時候各個控制元件已經執行完成了OnInit階段,所以 TrackViewState()已經呼叫,這個時候this.Text已經被標記為Dirty了,所以會被持久化到_ViewState隱藏變數中,這 樣就增加了ViewState的大小。那麼如果使用了第二種方法,判斷是否設定了初值,如果沒有那麼就通過 Session["SomeSessionValue"]中的預設值替代,這個階段是在生成JoesControl(New JoesControl)的時候進行賦值的,這個時候由於還未到達OnInit階段,所以TrackViewState()方法還沒有被呼叫,所以 ViewState["Text"]並不會被標記為Dirty,當然也就不會記錄到_ViewState中進行持久化。所以第二種實現方式是優於第一種實 現方式的。

2.
持久化靜態資料(Persisting static data)

我們這裡所說的靜態資料是那些不會被改變的資料(never change)或者在頁面的生命週期中、一個使用者會話中不會被改變的資料。 還是我們可愛的程式設計師Joe,最近他又接到了一個改造網站的任務,在他們公司的eCommerce網站上顯示那些已經登入的使用者,比如嗨,XXXX,歡 迎回來!”Joe的前提條件是這個網站已經有了一個業務層的API,可以通過CurrentUser.Name的方法方便的得到當前已經驗證的使用者姓名。剩下的把這個人名顯示到頁面上的工作就看Joe的了。以下是Joe的程式碼:

(ShoppingCart.aspx)
<!--
用於顯示登入使用者姓名的Label控制元件--&gt
<asp:Label id="lblUserName" runat="server" />

 

(ShoppingCart.aspx.cs)
//
用於在Label中動態顯示登入使用者姓名的程式碼;
protected override void OnLoad(EventArgs args) 
...{    
     
this.lblUserName.Text = CurrentUser.Name;    
     
base.OnLoad(args);
}

 

好了,F5,執行,一切正常,Joe又開始得意洋洋了。但是我們知道其實這裡Joe還是犯了個錯 誤。使用者的名稱不僅僅會顯示在Label中,同樣還會被序列化到_ViewState中,並根據頁面/伺服器之間的來來回回而不停的被序列化、反序列化...。這個開銷是值得的嗎?Joe聳聳肩說,這有什麼關係,就那麼幾個位元組而已。但是可以節約一點為什麼不節約呢,而且解決的方法還是如此的簡單。第一種方法,不用修改原始碼,直接禁用Label控制元件的ViewState,如:

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


好了,問題解決了。但是是否有更加好的解決方法呢?有!Label控制元件可能是ASP.NET中最 最被高估的控制元件了。這個可能是由於那些WinFormVB程式設計者,在WinForm中如果要顯示一些文字資訊,你可能需要一個Label。而 ASP.NET中的這個Label可能被認為和WinForm中的Label是等價的了。但是真的就是這樣的嗎?通過HTML原始碼我們可以看到Label 控制元件實際被解析成了HTML中的標籤。你必須問問你自己是否真的需要這個標籤呢?如果不需要涉及到特定 的格式,僅僅是顯示資訊那麼我覺得答案是否定的。請看:

= CurrentUser.Name %>

恩,這樣你就可以避免生成一個標籤了,並且可以很好的解決問題。但是從編 程習慣上來說,這種將前臺和後臺程式碼混合的形式是不提倡的,這樣會使程式碼的可讀性下降,並且使開發的職責無法明確區分。所以這裡還可以使用一種 ASP.NET中存在但是確被Label控制元件的光環籠罩的控制元件 -- Literal。這個控制元件僅僅將其Text中的內容輸出到客戶端,並且不會生成標籤。是不是覺得對這個控制元件有些印象,對了,前面 在說道將頁面解析成一個控制元件樹的時候,第二層一般由三個控制元件組成,一個是Literal,用於儲存到

標籤以前的所有html代 碼。就是這個控制元件。以下就是使用Literal控制元件來替代Label控制元件的方法。當然這裡也需要將EnableViewState設定為false。問題 解決了的同時,我們節省了網路傳輸的資源。不錯!

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



3.
持久化廉價的資料(Persisting cheap data) 

這個問題實際上包含了第一個問題。靜態資料往往是很容易就可以得到的(取得的開銷

/成本比較小),但是並不是所有容易取得的資料都是靜態資料。可能這些資料會不停

的被更改,但是總體來說得到這些資料的成本很低。一個典型的例子是美國各個州的列表。

除非你要回到1787127(here),那麼當前美國的所有州列表在短期內是不會有改變的。

當然我們現在的程式設計師都很痛恨硬編碼。讓我把美國各個州的列表都靜態的寫在頁面

上?傻子才這樣做呢。我們更加傾向於將州名都保留在一個資料庫(或者其他易於

修改的配置檔案中。),這樣如果州名或者州的列表出現了任何變化,就不用修改源

程式碼了。恩,我完全同意這一點,我們的著名程式設計師Joe也是這樣認為的,而且這張表

在他們公司已經存在了,表名叫做USSTATES,這回Joe的任務就是和操作這張表有關係的。

下面是用於顯示美國各個州列表的下拉選單(DropDownList)

 

       

 

       

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

 

       

 

       

這裡顯示的是繫結從資料庫中取得的美國州列表的資料程式碼:

protected override void OnLoad(EventArgs args) 
...{    
    
if(!this.IsPostback) 
    
...{       
        
this.lstStates.DataSource = QueryDatabase();        
        
this.lstStates.DataBind();    
    }    
    
base.OnLoad(e);
}

由於美國50個州是在OnLoad階段中被繫結到下拉選單(DropDownList)中的,所以 這些資訊在繫結到下拉選單的同時,還被序列化並被記錄到了ViewState中了。天哪,那可能一個龐大的資料,特別是對於那些低速接入網路的使用者。你知 道嗎,我好幾次都想給我的奶奶講解為什麼網路這麼慢(那是因為你的電腦正在請求所有美國的州呢,能不慢嗎?),但是我想我的奶奶是不會懂了。我想她可能會開始跟我說,在她年輕的時候美國只有46個州。那4個新增的州,真是可惡,它們拉慢了我們的網路。但是我們又有什麼辦法呢?(我們可都是平民百姓。:D)

這 個問題和上面提到的靜態資料有些類似,一種比較通用的解決方法就是將控制元件的EnableViewState屬性設定為False。但是這種解決方法並不是 萬能藥,比如我們現在的例子,如果Joe僅僅是將用於顯示美國各州的DropDownListEnableViewState控制元件設定為false,並 且將OnLoad函式中的!Page.IsPostBack的限制條件去掉(這樣就保證每次載入頁面後DropDownList都會被重新繫結,而不會再頁面回傳以後導致DropDownList中的資料丟失。),那麼在使用的時候,Joe就會發現他有麻煩了。什麼麻煩呢?當頁面回傳以後,Joe發現他 先前選擇的州並不是下拉選單(DropDownList)中的預設值。”(注意這裡的DropDownList是靜態控制元件才會出現上面說的這種情況,如果是在OnLoad中動態生成的DropDownList控制元件然後再繫結資料那麼不會出現此問題)怎麼會這個樣子!!這是對ViewState的另外一個誤 解。下拉選單之所以沒有保留頁面回傳前的選擇值並不是因為我們禁用了下拉選單的ViewState。在頁面回傳的時候還有一些用於獲取頁面資訊的控制元件值並 不是通過ViewState來進行儲存的,他們是通過名值對的方式通過Http請求(HttpRequest)的方式進行回傳的,這些值被稱為回傳值 (PostData)(可以通過將回傳方式修改為GET來從URL中檢視存在哪些回傳值)。所以即便是我們禁用了DropDownList ViewStateDropDownList依然可以將那個選擇的值回傳伺服器。這裡之所以下拉選單(DropDownList)會在頁面回傳後忘 記上次選擇的值是因為在OnLoad階段之前的ProcessPostData已經對DropDownList設定了預設值,但是這個時候 DropDownList還沒有ListItem,自然無法設定到最後一次回傳選擇的值。然後是OnLoad事件中對DropDownList進行資料綁 定,但是由於沒有執行ProcessPostData方法所以不會再次設定預設值。前面的括號中有說明,如果這個DropDownList控制元件也是在 OnLoad中動態生成的,那麼由於進度追趕,在OnLoad階段後還會重新執行一次ProcessPostData,在這裡又會把下拉選單中的值設定為 預設值,所以說以上描述的問題僅僅只有在DropDownList為靜態控制元件的時候才會存在。幸運的是我們解決這個問題的方法也很簡單,我們將繫結資料的 程式碼移動到OnInit階段,這個階段將先於ProcessPostData執行,所以下拉選單將被設定為最後一次回傳的預設值。
       

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

 

 

protected override void OnInit(EventArgs args) 
...{    
       
this.lstStates.DataSource = QueryDatabase();    
       
this.lstStates.DataBind();    
       
base.OnInit(e);
}

上面這種方法適用於幾乎所有的廉價資料(cheap data,容易獲得的,獲得的代價很低的資料)。你可能會反駁我說如果每次都重新去取資料,如:每次都連線資料庫去取得對應的資料可能會比將資料儲存在 ViewState中代價更高,但是我不這樣認為。當前的資料庫管理系統(DBMS, SQL Server)已經相當的成熟,它們往往具有良好的快取機制,如果配置得當的話執行的效率也非常高。(譯者:我也有這樣的經驗,我曾經比較兩種處理資料的 方式,其中一種是先取得一個大範圍的資料,然後在程式碼中通過迴圈的方式將其中不符合條件的資料過濾掉;另外一種方式是直接通過SQL語句在資料庫中進行數 據篩選,然後將符合條件的資料進行返回。根據頁面顯示的速度來判斷,後者的執行效率遠遠高於前者。)其實想想到底是將一堆無用的資料通過56kbps的速 度和千里之外的客戶傳來傳去還是將少許的資料在可能只相距幾百英尺的應用伺服器和資料庫伺服器之間傳遞(它們之間的連線速度一般都高於10M)的代價高, 這個結果應該已經很明顯了。當然如果你一定想精益求精的話,那麼你可以選擇把一些常用不易變的資料快取起來這樣可以進一步的提高效能。

 

4. 通過編碼的方式初始化子控制元件(Initializing child controls programmatically)
讓我們再一起面對這樣一個事實,我們不可能什麼都事先把所需的控制元件宣告好,有時候頁面的顯示,控制元件的現實和外觀都和一定的業務邏輯有關係(這不正是我們程式設計師存在的原因嗎?)。但是麻煩的是ASP.NET並沒有提供一種簡單的方式讓我們正確的建立動態控制元件。當然我們可以通過重寫OnLoad方法並在這裡宣告動態的控制元件,事實上我們也常常這樣做,但是這樣做的結果有時候會讓我們在ViewState持久化了一些本不應該持久化的資料。我們同樣可以重寫OnInit方法,但是同樣的問題依然存在。還記得我們前面提到過ASP.NETOnInit階段是怎樣呼叫TrackViewState()方法的嗎?它是從控制元件樹的底部遞迴呼叫每個子控制元件的TrackViewState()方法,最後一個呼叫的就是控制元件樹的根節點(Page),所以當你在Page.OnInit階段的時候對動態控制元件進行操作的話,那麼頁面的子控制元件的TrackViewState已經被呼叫了,所以這個時候你賦值的資料也會被標記為髒資料(dirty data)並最終被ViewState進行持久化儲存。讓我們再看看Joe的例子,Joe在頁面中定義了用於顯示當前日期和時間的標籤控制元件(Label),宣告程式碼如下所示:

<asp:Label id="lblDate" runat="server" />

 

protected override void OnInit(EventArgs args) 
...{    
    
this.lblDate.Text = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss");    
    
base.OnInit(e);
}


 

雖然Joe已經在最早的事件中將LabelText屬性設定為了當前的日期時間資訊。但是還是晚了,原因我們前面已經分析過了,這個時候Label.TrackViewState()已經被呼叫,所以Label.Text的賦值操作將導致Label.Text被標記為髒資料(dirty data),從而被記錄到ViewState中。但是這個是不必要的,應為這個日期很容易得到,可以歸結於我們前面總結的持久化廉價的資料這個問題。要解決這個問題我們可以簡單的將Label控制元件的EnableViewState屬性設定為false。但是我們這裡將用另外一種方法來解決,因為這種解決方法揭示了一個重要的概念。首先我們看看Joe的做法,直接將Label中的Text屬性設定為當前時間資訊,如下所示:

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


 

你可能也有過這樣嘗試,但是ASP.NET會給你當頭一棒,它會明確的告訴你語法不能對伺服器端控制元件的屬性進行賦值操作。當然Joe也可以使用的方法,但是這個方法和我們前面提到的禁用LabelViewState同時在每次請求頁面的時候繫結資料的方法實際上是一樣的。問題是我們希望通過編碼的方式為LabelText屬性的初值進行賦值操作(我們不希望這些賦值操作導致ViewState大小的增加),同樣在以後的操作中我們希望這個Label控制元件依然可以像一個普通的Label控制元件被使用。簡單的說就是這樣,我們需要一個Label,它的預設值是當前的日期和時間,但是如果我們人工的對其Label.Text進行了賦值操作,那麼我們還是希望這個值在頁面的回傳之間可以保留(即通過ViewState進行持久化)。舉個簡單的例子,Joe的頁面上有一個按鈕,當使用者點選這個按鈕那麼顯示當前日期和時間的Label將顯示一個空時間”(即:“--/--/---- --:--:--”),此按鈕的響應程式碼為:

private void cmdRemoveDate_Click(object sender, EventArgs args) 
...{    
    
this.lblDate.Text = "--/--/---- --:--:--";
}


如果需要實現這樣一個需求那麼我們前面的做法(簡單的將LabelEnableViewState屬性設定為false)將不能解決這個問題,因為如果使用者通過按鈕取消了時間的顯示,由於LabelViewState被禁用,那麼就意味著Label的值在回傳之間不會被儲存,所以在下次頁面回傳以後Label依然會顯示當前的日期和時間。那麼Joe需要怎麼做呢?可憐的Joe總是被無窮無盡的需求折磨著。


實際上上面的例子描述的就是一個邏輯,Label控制元件必須按照邏輯來決定應該顯示什麼內容。上面的邏輯我們簡化的說就是,對於Label的初值我們不希望它保留在ViewState中而以後如果出現了改變那麼我們希望都保留在ViewState中,以便在頁面回傳的過程中進行狀態的保留。從這個表述我們可以看出,如果我們能在控制元件的TrackViewState()被呼叫前為其賦初值,那麼什麼問題都解決了。但是前面我提到過,ASP.NET並沒有提供一種簡單的方法來實現這個過程(TrackViewState()被呼叫前進行操作)。在ASP.NET 2.0的版本中已經為我們提供了一些先於OnInit階段的階段(如:OnPreInit階段),這裡針對ASP.NET 1.1版本,我們確實沒有一個先於OnInit階段進行控制元件的初值設定(其實這個表述是不正確的,在ASP.NET 1.1中你可以通過重寫DeterminePostBackMode方法來實現對控制元件進行賦初值,由於這個方法先於OnInit方法,所以此時賦的初值是不會被記錄到ViewState)。一下作者提供了另外兩種實現方法:
 

1. 在控制元件的OnInit事件對其進行賦值操作(Declaratively hook into the Init event) :

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


同樣在後臺編寫Label.OnInit事件對應的響應函式並對Label.Text賦初值也是可以的。

 

2. 建立使用者自定義元件(Create a custom control):

public class DateTimeLabel : Label 
...{    
    
public DateTimeLabel() 
    
...{        
        
this.Text = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss");    
    }
}


這裡在建構函式中就對Label.Text的屬性進行初值賦值,一定是在TrackViewState()方法之前,所以這樣也可以達到我們前面提到的目的。

5. 以編碼的方式建立動態控制元件(Initializing dynamically created controls programmatically)

這個實際上和上面的就是一個問題,由於到目前為止我們對ViewState已經有了一定深度的瞭解,所以我們解決起問題來就更加的得心應手。讓我們來看看我們的老朋友Joe編寫的一個使用者自定義元件,這個元件重寫了Control類的一個CreateChildControls方法,動態生成了一個Label控制元件。程式碼如下:

public class JoesCustomControl : Control 
...{    
    
protected override void CreateChildControls() 
    
...{        
        Label l = 
new Label();         
        
this.Controls.Add(l);        
        l.Text = "Joe's label!";    
    }
}


好了,我們現在考慮的是那些被動態建立的控制元件(例子中是Label控制元件)什麼時候開始跟蹤它的ViewState呢?我們知道我們可以在頁面生命週期的任何階段動態生成控制元件並新增到頁面的控制元件樹中,但是ASP.NET中是在OnInit階段呼叫TrackViewState()以開始跟蹤控制元件ViewState的變化。那麼我們這裡動態建立的控制元件是否會由於錯過了OnInit事件從而導致不能對動態生成的控制元件的狀態進行跟蹤和持久化呢?答案是否定的,這個奧祕就是Controls.Add()方法,這個方法並不像我們原來使用ArrayList.Add方法僅僅是將一個Object新增到一個列表中,Controls.Add()方法在將子控制元件新增到當前控制元件下後還需要呼叫一個叫做AddedControl()的方法,就是這個方法對於那些新加入的控制元件狀態進行檢查,如果發現當前控制元件的狀態落後於頁面的生命週期,那麼將會呼叫對應的方法使當前控制元件的狀態和頁面宣告週期保持一致,這個過程叫做“追趕(catch up)”。比如我們舉一個稍稍極端的例子,我們在頁面生命週期的OnPreRender階段動態生成了一個控制元件並將其新增到當前頁面的控制元件樹中,那麼系統發現新新增的控制元件並不是出於OnPreRender狀態便會呼叫方法使這個控制元件經歷LoadViewState,LoadPostBackData,OnLoad等方法(頁面宣告週期中的一些私有方法將被忽略),直到這個控制元件也到了OnPreRender狀態。其實通過檢視Temporary ASP.NET Files中編譯過的ASP.NET aspx頁面的類程式碼你就可以發現在建立頁面控制元件樹的時候,呼叫的是一個叫做__BuildControlTree()的方法,裡面對於新增子控制元件使用的是AddParsedSubObject()方法,而這個方法實際就是呼叫了Controls.Add()方法,同樣的過程。

我們再回到
Joe編寫的使用者自定義元件,由於CreateChildControls無法確定在何時被呼叫,如果頁面已經執行到了OnInit階段,那麼只要呼叫了Controls.Add()方法那麼這個控制元件馬上就會被呼叫TrackViewState()方法,並立即開始對ViewState進行跟蹤。而Joe的程式碼是在this.Contorls.Add(l)之後再對Label進行初值賦值(l.Text = “Joe’s Label!”),這樣”Joe’s Label!”將被新增到ViewState進行儲存。那麼知道了一切原因都源於Controls.Add()方法後,解決方法也就出來了,我們只要顛倒一些最後兩個語句的順序就可以解決問題,程式碼如下所示:

public class JoesCustomControl : Control 
...{    
    
protected override void CreateChildControls() 
    
...{        
        Label l = 
new Label();        
        l.Text = "Joe's label!";         
        
this.Controls.Add(l);    
    }
}


很玄妙是吧?理解了這個我們再回頭看看我們前面提到的通過下拉選單(DropDownList)列舉美國所有州的名稱的例子。在前面提供的解決方法中,我們是先禁用DropDownListViewState,然後在OnInit階段對DropDownList進行資料繫結。那麼我們這裡又提供了一個新的解決方法。首先在頁面中去掉靜態宣告的DropDownList,然後在頁面生命週期OnLoad階段前的任何位置動態生成DropDownList,並且對其進行值的繫結,然後通過Controls.Add()方法將其新增到頁面控制元件樹中,同樣可以達到一樣的效果。

public class JoesCustomControl : Control 
...{    
    
protected override void OnInit(EventArgs args) 
    
...{        
        DropDownList states = 
new DropDownList();        
        states.DataSource = 
this.GetUSStatesFromDatabase();        
        states.DataBind();         
        
this.Controls.Add(states);    
    }
}


這樣做的好處還有,由於DropDownListEnableViewState = true, 所以DropDownList依然可以觸發諸如OnSelectedIndexChanged事件。你也可以對同樣的方法操作DataGrid控制元件,但是可能對於使用DataGrid的排序(sorting),分頁(paging)還有SelectedIndex屬性還是存在問題??(這幾個問題還沒有考究過)

 

ViewState和平共處(BE VIEWSTATE FRIENDLY)

到目前為止,如果你理解了這篇文章中所說的東西那麼恭喜你,你已經知道ViewState是怎樣實現其功能的了。知道了ViewState的工作原理我們就可以寫出更加優化的程式碼,而往往這些更優的程式碼比那些蹩足的程式碼更加簡潔明瞭。Enjoy it!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/25897606/viewspace-705192/,如需轉載,請註明出處,否則將追究法律責任。