viewState詳解

做者:Infinities Loopphp

概述html

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

如今有關ViewState的文章可謂多如牛毛,你可能會說再寫有關 ViewState的文章無異於炒剩飯(我這篇文章即是:D)。可是我卻不這麼認爲,若是把ViewState當作一匹野馬的話,那麼這匹野馬並無死 去,它還活躍的很,說不定這個時候它正在你的客廳裏撒野呢。因此咱們有必要再次去把它擊倒。不過你也不須要擔憂,從這篇文章你能夠發現其實這匹馬也沒有那 麼壞。web

個人意思並非否然目前尚未好好說明ViewState的文章,只是我總以爲好 像這些文章都缺乏一些東西,而這些缺乏的東西每每就會致使人們對ViewState的困惑。好比:理解ViewState是怎樣跟蹤那些已經出現變化的數 據(dirty data)就很是重要,可是不少文章卻沒有過多的涉及,或者即使涉及了可能其中卻包含了錯誤的信息。好比這篇文章(W3Schools) 中就說頁面回傳的值也是保存在ViewState中的,可是這個觀點是錯誤的。不信是嗎?那麼你在一個頁面上放置一個TextBox控件和一個 Button控件,而後你在「屬性」中將TextBox的EnableViewState設置爲False,而後經過點擊Button回傳頁面,你會發現 TextBox仍是仍舊會保留你輸入的值,而不會如你想象的因爲TextBox的ViewState被禁用了而致使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.0中ViewState的實現機制有稍許變更,可是在寫這篇文章的時候我依然是以ASP.NET 1.x的版本爲前提進行的。好比說在ASP.NET 2.0的新增了一個新類型的ViewState -- ControlState, 可是實際上ControlState和ViewState並不大變,因此咱們這裏能夠忽略它。服務器

首先讓咱們看看爲何深刻了解ViewState是如此重要。網絡

 
對ViewState的誤解可能致使...   
  • 致使一些敏感信息被泄漏;
  • 針對ViewState的攻擊(aka the Jedi Mind Trick,aka 是又稱做,又叫作的意思。Jedi Mind Trick, 看過星球大戰的人對於Jedi必定不陌生,Jedi就是絕地武士。Jedi Mind Trick 是絕地武士的一個招式,能夠用於控制對方的思惟。有關這個的具體知識能夠參見:http://gollum.easycp.de/gollum/gollum.php?a=core&l=en&wl=en&q= 這裏做者估計是表達了經過ViewState的攻擊來達到控制對方的目的。好比一個等離子電視的價格被修改成了1美圓一臺)
  • 不好的性能,在某些極端的狀況下可能根本就沒有性能。
  • 併發性差 -- 想象一下若是每次回傳的數據都有50kB,那麼你的服務器能承受多少併發的訪問量呢?
  • 糟糕的全局設計;
  • 以上結果必定會讓你抓狂的(頭痛,反胃,頭昏眼花,皺眉頭...)。

若是你正在開發基於ASP.NET平臺的網絡應用程序,而且你無視ViewState存在的話,那麼如下的狀況可能會發生在你的身上。

ViewState Madness!!! Drop your red bull and surrender your cpu cycles. You will be frustrated. Performance is futile!
The ViewState form field.看看你頁面的HTML源代碼吧,密密麻麻很恐怖吧。 ViewState will add your web app's distinctiveness to it's own. Performance is futile. 沉重的星際垃圾

像如上的例子我還能夠舉出不少,可是我想這兩個例子已經具備表明性了。好了,如今讓咱們從最開始認識ViewState吧。

ViewState能夠用來作什麼? 

這裏列舉的每一項都是ViewState須要完成的主要工做,咱們將根據這些工做來學習ViewState是如何實現這些功能。
  • 以名值對的方式來存控件的值,和Hashtable的結構相似;
  • 跟蹤那些ViewState中出現改變的值,以便對這些髒數據(dirty)進行進一步的處理;
  • 經過序列化將ViewState中的值保存在頁面的隱藏域(Hidden Field)中(這是默認的持久化方式),並經過反序列化獲得對應的ViewState對象以便進行相應的操做;
  • 在頁面回傳的過程當中自動的存儲ViewState中的跟蹤的值。

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

什麼是ViewState不能作的?

  • 自動保存一個類中變量的狀態,不管是private, protected仍是public的變量;
  • 能夠在頁面回傳的過程當中記住全部狀態值;
  • 只要有了ViewState那麼每次頁面請求時從新構造的數據的操做是沒必要要的了;
  • 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名值對回傳的數據值(如TextBox的TextBox.Text);
  • 想讓ViewState替你泡一杯咖啡,作夢吧:P。

雖然ViewState做爲一個總體出如今.NET Framework框架中有它的惟一目的,那就是在頁面回傳的過程當中保存狀態值,使本來沒有「記憶」的Http協議變得有「記憶」起來。可是上面列舉的 ViewState的四個主要功能之間卻沒有太多的關聯。因此從邏輯上咱們能夠將其劃分開來,各個擊破。

ViewState Nuggets
哈哈,這樣大小的ViewState是否是更加好下口了?


1. ViewState就是用來存儲數據的

若是你曾經使用過HashTable的話,那麼你應該明白個人意思了。這裏並無 什麼高深的理論。ViewState經過String類型的數據做爲索引(注意在ViewState中不容許經過整形下標/索引的方式對其中的項進行訪問, 如:ViewState.Item(0) 、ViewState[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取代嗎?前面咱們但是大肆宣揚了一下StateBag和HashTable有多麼的像。雖然他們都是經過名值對的方式來存儲值,可是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屬性,可是Width是Unit類型的,因此這裏就設計到一個從String到Unit類型的轉化過程。

好,到了這,咱們再之前面所說的內容將當前發生的事情再描述一遍。咱們已經知道大多數服務器控件的屬性值最終是存儲在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個控件,它們分別是用於保存表單(<form>)標籤前全部信息的文本控件(Literal),而後是表單控件(Form),而後是表單(</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.aspx和Page2.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中存儲的名值對從新Add到StateBag列表中(StateBag.Add(key, value))。同時咱們從LoadViewState也能夠發現在.NET Framework 1.1中傳入的object變量是一個pair類型的變量。pair類型包含兩個屬性First, Second都是object類型的變量,在ViewState中其中一個屬性存儲的是包含ViewState.Item.Key的ArrayList而另一個屬性包含的是ViewState.Item.Value的ArrayList,相對應的Key和Value在ArrayList中的下標相同。而後StateBag類就經過遍歷兩個ArrayList將值添加到狀態項中(注意在.NET Framework 2.0中這個方法的實現有些小小的改動,放棄使用Pair類型而僅僅使用一個ArrayList, ArrayList中每一個名值對佔兩個Item, 前一個爲key後一個爲value, 循環的時候以步進2進行循環)。這裏須要注意的是從LoadViewState()從新載入到ViewState的數據僅僅包含前一次請求被標記爲Dirty的那些數據(注意不是當次請求(current request),而是前一次請求(previous request)就是當前請求的前一次請求。)在載入_ViewState中包含的數據以前,對應控件的ViewState中可能已經包含了一些值了,好比那些靜態控件中預先聲明好的值(如:<asp:Label Text="abc"/>中的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.  
    1. 爲服務器端控件賦默認值(Forcing a Default);
    2. 持久化靜態數據(Persisting static data);
    3. 持久化廉價數據(Persisting cheap data);
    4. 以編碼的方式初始化子控件(Initializing child controls programmatically);
    5. 以編碼的方式建立控件(Initializing dynamically created controls programmatically)。

1. 錯誤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屬性初值的狀況,那麼這個時候就會在JoesControl的OnLoad方法中執行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控件-->
<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中最最被高估的控件了。這個多是因爲那些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:Literal id="litUserName" runat="server" EnableViewState="false"/>


3. 持久化廉價的數據(Persisting cheap data) 
這個問題實際上包含了第一個問題。靜態數據每每是很容易就能夠獲得的(取得的開銷
/成本比較小),可是並非全部容易取得的數據都是靜態數據。可能這些數據會不停
的被更改,可是整體來講獲得這些數據的成本很低。一個典型的例子是美國各個州的列表。
除非你要回到1787年12月7日(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僅僅是將用於顯示美國各州的DropDownList的EnableViewState控件設置爲false,而且將OnLoad函數中的!Page.IsPostBack的限制條件去掉(這樣就保證每次載入頁面後DropDownList都會被從新綁定,而不會再頁面回傳之後致使DropDownList中的數據丟失。),那麼在使用的時候,Joe就會發現他有麻煩了。什麼麻煩呢?「當頁面回傳之後,Joe發現他先前選擇的州並非下拉菜單(DropDownList)中的默認值。」(注意這裏的DropDownList是靜態控件纔會出現上面說的這種狀況,若是是在OnLoad中動態生成的DropDownList控件而後再綁定數據那麼不會出現此問題)怎麼會這個樣子!!這是對ViewState的另一個誤解。下拉菜單之因此沒有保留頁面回傳前的選擇值並非由於咱們禁用了下拉菜單的ViewState。在頁面回傳的時候還有一些用於獲取頁面信息的控件值並非經過ViewState來進行保存的,他們是經過名值對的方式經過Http請求(HttpRequest)的方式進行回傳的,這些值被稱爲回傳值(PostData)(能夠經過將回傳方式修改成GET來從URL中查看存在哪些回傳值)。因此即使是咱們禁用了DropDownList的ViewState,DropDownList依然能夠將那個選擇的值回傳服務器。這裏之因此下拉菜單(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.NET在OnInit階段是怎樣調用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已經在最先的事件中將Label的Text屬性設置爲了當前的日期時間信息。可是仍是晚了,緣由咱們前面已經分析過了,這個時候Label.TrackViewState()已經被調用,因此Label.Text的賦值操做將致使Label.Text被標記爲髒數據(dirty data),從而被記錄到ViewState中。可是這個是沒必要要的,應爲這個日期很容易獲得,能夠歸結於咱們前面總結的「持久化廉價的數據」這個問題。要解決這個問題咱們能夠簡單的將Label控件的EnableViewState屬性設置爲false。可是咱們這裏將用另一種方法來解決,由於這種解決方法揭示了一個重要的概念。首先咱們看看Joe的作法,直接將Label中的Text屬性設置爲當前時間信息,以下所示:
<asp:Label id="Label1" runat="server" Text="<%= DateTime.Now.ToString() %>" />

 

 
你可能也有過這樣嘗試,可是ASP.NET會給你當頭一棒,它會明確的告訴你<%= %>語法不能對服務器端控件的屬性進行賦值操做。固然Joe也能夠使用<%# %>的方法,可是這個方法和咱們前面提到的禁用Label的ViewState同時在每次請求頁面的時候綁定數據的方法其實是同樣的。問題是咱們但願經過編碼的方式爲Label的Text屬性的初值進行賦值操做(咱們不但願這些賦值操做致使ViewState大小的增長),一樣在之後的操做中咱們但願這個Label控件依然能夠像一個普通的Label控件被使用。簡單的說就是這樣,咱們須要一個Label,它的默認值是當前的日期和時間,可是若是咱們人工的對其Label.Text進行了賦值操做,那麼咱們仍是但願這個值在頁面的回傳之間能夠保留(即經過ViewState進行持久化)。舉個簡單的例子,Joe的頁面上有一個按鈕,當用戶點擊這個按鈕那麼顯示當前日期和時間的Label將顯示一個「空時間」(即:「--/--/---- --:--:--」),此按鈕的響應代碼爲:
private void cmdRemoveDate_Click(object sender, EventArgs args) 
{    
    this.lblDate.Text = "--/--/---- --:--:--";
}


若是須要實現這樣一個需求那麼咱們前面的作法(簡單的將Label的EnableViewState屬性設置爲false)將不能解決這個問題,由於若是用戶經過按鈕取消了時間的顯示,因爲Label的ViewState被禁用,那麼就意味着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)列舉美國全部州的名稱的例子。在前面提供的解決方法中,咱們是先禁用DropDownList的ViewState,而後在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);    
    }
}


這樣作的好處還有,因爲DropDownList的EnableViewState = true, 因此DropDownList依然能夠觸發諸如OnSelectedIndexChanged事件。你也能夠對一樣的方法操做DataGrid控件,可是可能對於使用DataGrid的排序(sorting),分頁(paging)還有SelectedIndex屬性仍是存在問題??(這幾個問題尚未考究過)

 
ViewState 和平共處 (BE VIEWSTATE FRIENDLY)

到目前爲止,若是你理解了這篇文章中所說的東西那麼恭喜你,你已經知道ViewState是怎樣實現其功能的了。知道了ViewState的工做原理咱們就能夠寫出更加優化的代碼,而每每這些更優的代碼比那些蹩足的代碼更加簡潔明瞭。Enjoy it!

相關文章
相關標籤/搜索