asp.net ViewState详解
ViewState是一個(gè)被誤解很深的動(dòng)物了。我希望通過此文章來澄清人們對(duì)ViewState的一些錯(cuò)誤認(rèn)識(shí)。為了達(dá)到這個(gè)目的,我決定從頭到尾詳細(xì)的描述一下整個(gè)ViewState的工作機(jī)制,其中我會(huì)同時(shí)用一些例子說明我文章中的觀點(diǎn),結(jié)論。比如我會(huì)用靜態(tài)控件(declared controls)和動(dòng)態(tài)控件(dynamic controls)兩個(gè)方面來說明同一個(gè)問題。
現(xiàn)在有關(guān)ViewState的文章可謂多如牛毛,你可能會(huì)說再寫有關(guān)ViewState的文章無異于炒剩飯(我這篇文章便是:D)。但是我卻不這么認(rèn)為,如果把ViewState看成一匹野馬的話,那么這匹野馬并沒有死去,它還活躍的很,說不定這個(gè)時(shí)候它正在你的客廳里撒野呢。所以我們有必要再次去把它擊倒。不過你也不需要擔(dān)心,從這篇文章你可以發(fā)現(xiàn)其實(shí)這匹馬也沒有那么壞。
我的意思并不是否然目前還沒有好好說明ViewState的文章,只是我總覺得好像這些文章都缺少一些東西,而這些缺少的東西往往就會(huì)導(dǎo)致人們對(duì)ViewState的困惑。比如:理解ViewState是怎樣跟蹤那些已經(jīng)出現(xiàn)變化的數(shù)據(jù)(dirty data)就非常重要,但是很多文章卻沒有過多的涉及,或者即便涉及了可能其中卻包含了錯(cuò)誤的信息。比如這篇文章(W3Schools)中就說頁面回傳的值也是保存在ViewState中的,但是這個(gè)觀點(diǎn)是錯(cuò)誤的。不信是嗎?那么你在一個(gè)頁面上放置一個(gè)TextBox控件和一個(gè)Button控件,然后你在“屬性”中將TextBox的EnableViewState設(shè)置為False,然后通過點(diǎn)擊Button回傳頁面,你會(huì)發(fā)現(xiàn)TextBox還是仍舊會(huì)保留你輸入的值,而不會(huì)如你想象的由于TextBox的ViewState被禁用了而導(dǎo)致TextBox的值在頁面回傳的過程中消失了。還有一些文章(#1 Google Search Result?,?ASP.NET Documentation on MSDN?)描述了服務(wù)器控件如何在頁面的回傳中保持自身狀態(tài)。這些文檔雖然沒有全錯(cuò),但是有些描述還是存在一些不準(zhǔn)確的地方,如:
"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."
(如果一個(gè)控件用ViewState而不是用類的私有字段(private field)來存儲(chǔ)數(shù)據(jù),那么這些控件屬性的值將會(huì)自動(dòng)在頁面回傳之間保持狀態(tài)??[這句話的意思有待確定])
上面這句話似乎在暗示任何東西只要是保存在ViewState 狀態(tài)包(StateBag)中,那么就會(huì)在服務(wù)器和客戶端頁面回傳的過程中被傳遞。(That seems to imply that anything you shove into the ViewState StateBag will be round-tripped in the client's browser. NOT TRUE!)不對(duì)!所以說對(duì)于ViewState控件的困惑還是存在的。而且在Internet上我目前還沒有找到一篇100%準(zhǔn)確完整的描述ViewState工作的文章。我目前找到的最好文章是這一篇(this one by Scott Mitchell)。這篇文章是值得一讀的。然而這篇文章還是有些美中不足,它沒有描述在控件初始化和ViewState跟蹤的時(shí)候父控件和子控件之間的關(guān)系。而恰恰就是這一點(diǎn)就會(huì)導(dǎo)致對(duì)ViewState大量的誤解,至少我是有過這種經(jīng)歷的。
根據(jù)上面的情況,這篇文章從最開始先會(huì)對(duì)ViewState實(shí)現(xiàn)的功能進(jìn)行一個(gè)完全描述。然后對(duì)ViewState的實(shí)現(xiàn)進(jìn)行一個(gè)詳細(xì)的闡述,在這個(gè)過程中我會(huì)同時(shí)舉一些相對(duì)應(yīng)的例子,通常我會(huì)先舉一個(gè)開發(fā)人員經(jīng)常會(huì)犯的錯(cuò)誤的例子,然后再舉一個(gè)例子來表示如何修正錯(cuò)誤。在這里我需要實(shí)現(xiàn)聲明一下的是,雖然在ASP.NET 2.0中ViewState的實(shí)現(xiàn)機(jī)制有稍許變動(dòng),但是在寫這篇文章的時(shí)候我依然是以ASP.NET 1.x的版本為前提進(jìn)行的。比如說在ASP.NET 2.0的新增了一個(gè)新類型的ViewState -- ControlState, 但是實(shí)際上ControlState和ViewState并不大變,所以我們這里可以忽略它。
首先讓我們看看為什么深入了解ViewState是如此重要。
?對(duì)ViewState的誤解可能導(dǎo)致...???
如果你正在開發(fā)基于ASP.NET平臺(tái)的網(wǎng)絡(luò)應(yīng)用程序,并且你無視ViewState存在的話,那么以下的情況可能會(huì)發(fā)生在你的身上。
| The ViewState form field.看看你頁面的HTML源代碼吧,密密麻麻很恐怖吧。 | ViewState will add your web app's distinctiveness to it's own. Performance is futile.?沉重的星際垃圾 |
像如上的例子我還可以舉出很多,但是我想這兩個(gè)例子已經(jīng)具有代表性了。好了,現(xiàn)在讓我們從最開始認(rèn)識(shí)ViewState吧。
?ViewState可以用來做什么?
這里列舉的每一項(xiàng)都是ViewState需要完成的主要工作,我們將根據(jù)這些工作來學(xué)習(xí)ViewState是如何實(shí)現(xiàn)這些功能。
下面列舉的是ViewState不能用來做什么的列表,這個(gè)其實(shí)比了解ViewState是用來做什么的還重要。
什么是ViewState不能做的?
雖然ViewState作為一個(gè)整體出現(xiàn)在.NET Framework框架中有它的唯一目的,那就是在頁面回傳的過程中保存狀態(tài)值,使原本沒有“記憶”的Http協(xié)議變得有“記憶”起來。但是上面列舉的ViewState的四個(gè)主要功能之間卻沒有太多的關(guān)聯(lián)。所以從邏輯上我們可以將其劃分開來,各個(gè)擊破。
哈哈,這樣大小的ViewState是不是更加好下口了?
1.?ViewState就是用來存儲(chǔ)數(shù)據(jù)的
如果你曾經(jīng)使用過HashTable的話,那么你應(yīng)該明白我的意思了。這里并沒有什么高深的理論。ViewState通過String類型的數(shù)據(jù)作為索引(注意在ViewState中不允許通過整形下表的方式對(duì)其中的項(xiàng)進(jìn)行訪問,如:ViewState.Item(0) 的形式是不允許的。)ViewState對(duì)應(yīng)項(xiàng)中的值可以存儲(chǔ)任何類型的值,實(shí)施上任何類型的值存儲(chǔ)到ViewState中都會(huì)被裝箱為Object類型。以下是幾個(gè)對(duì)ViewState進(jìn)行賦值的幾個(gè)例子。
ViewState["Key1"]?=123.45M;?//store?a?decimal?valueViewState["Key2"]?="abc";?//store?a?string
ViewState["Key3"]?=DateTime.Now;?//store?a?DateTime ? 實(shí)際上ViewState僅僅就是一個(gè)定義在System.Web.UI.Control類中的一個(gè)保護(hù)類型(Protected) 的屬性名稱。由于所有服務(wù)器端的控件,用戶自定義控件還有頁面(Page)類都是繼承自System.Web.UI.Control類, 所以這些控件都具有這些屬性。ViewState的真正類型實(shí)際應(yīng)該是System.Web.UI.StateBag類。 嚴(yán)格的說,雖然StateBag類雖然定義在System.Web的命名空間下,實(shí)際上StateBag類 和ASP.NET并沒有嚴(yán)格上的依存關(guān)系,它也完全可以放在System.Collections命名空間下。 事實(shí)上許多服務(wù)器端控件大多數(shù)屬性值都是利用ViewState來進(jìn)行數(shù)據(jù)存儲(chǔ)。你可能認(rèn)為 TextBox.Text屬性是按如下形式存儲(chǔ)的:
publicstringText?
{????
????get?{?return?_text;?}????
????set?{?_text?=?value;?}
}
但是你必須注意,上面的形式(通過類的私有字段)并不是大多數(shù)ASP.NET 服務(wù)器控件存儲(chǔ)其屬性值得方式。這些控件的屬性值大多是通過ViewState來進(jìn)行存儲(chǔ)的。通過Reflector查看TextBox.Text屬性的源代碼你可以看到類似如下的代碼:
publicstringText?{???
?????get?{?return?(string)ViewState["Text"];?}???
?????set?{?ViewState["Text"]?=?value;?}
}
為了表示這個(gè)觀點(diǎn)的重要性,我這里再重申一遍“大多數(shù)ASP.NET 服務(wù)器控件存儲(chǔ)其屬性值得方式是通過ViewState的方式存儲(chǔ)的,而不是我們通常想象的那樣通過類的私有字段來存儲(chǔ)。”即便是用于設(shè)定服務(wù)器控件樣式的Style類中的大多數(shù)屬性值也是通過ViewState來進(jìn)行存儲(chǔ)的。所以在設(shè)計(jì)自定義的組件時(shí),對(duì)于那些需要存儲(chǔ)的組件屬性值也最好遵循這個(gè)方式。這里我還需要著重講一個(gè)問題,在以ViewState為存儲(chǔ)方式的情況下,如果實(shí)現(xiàn)屬性的默認(rèn)值(default value),我們可能會(huì)認(rèn)為屬性值是這樣實(shí)現(xiàn)的:
publicclassMyClass?{???
??????private?string?_text?=?"Default?Value!";?????
??????public?string?Text
?????{???????
???????????get?{?return?_text;?}????????
???????????set?{?_text?=?value;?}????
?????}
}
這樣如果在對(duì)Text的屬性沒有設(shè)置的時(shí)候,直接取Text屬性,那么我們可以得到默認(rèn)值"Default Value!"。那么如果我們使用ViewState來存儲(chǔ)的時(shí)候如何實(shí)現(xiàn)默認(rèn)值呢?如下所示:
{???
??????get?
??????{????????
???????????return?ViewState["Text"]?==?null???"Default?Value!"?:??(string)ViewState["Text"];????
??????}????
??????set?
??????{
???????????ViewState["Text"]?=?value;?
??????}
}
就像操作HashTable一樣,如果StateBag(ViewState)中沒有包含某個(gè)鍵值的項(xiàng),那么它會(huì)返回一個(gè)null(在VB.NET中是返回Nothing)。所以我們可以通過判斷對(duì)應(yīng)鍵值的項(xiàng)是否是null來判斷某個(gè)ViewState項(xiàng)是否被賦值。然后我們通過三目運(yùn)算符來根據(jù)實(shí)際情況來返回默認(rèn)值或者設(shè)置的值。并且使用三目運(yùn)算符實(shí)際上這里還出于一個(gè)考慮,那么就是在服務(wù)器控件中,如果將某個(gè)屬性值設(shè)置為空(null),那么往往代表的意思是使用此屬性的默認(rèn)值。所以第一種實(shí)現(xiàn)方法還存在一個(gè)問題,那就是如果把某個(gè)屬性值設(shè)置為null,當(dāng)我們?cè)偃∵@個(gè)屬性的時(shí)候我們將得到null,而不是我們期望的"Default Value!"了,所以對(duì)于第一種實(shí)現(xiàn)方法還需要對(duì)null這個(gè)特殊值進(jìn)行判斷才可以完全滿足需求。ViewState還可以被用作其他的作用,比如在頁面回傳過程中保留某些值,比如我們?cè)陧撁婧笈_(tái)代碼中常常使用ViewState("Key") = "SomeValue"的方式來存儲(chǔ)值,實(shí)際上就是使用了Page類的ViewState屬性來進(jìn)行值得存儲(chǔ)。同樣的我們也可以在控件級(jí)別進(jìn)行ViewState的字定義存儲(chǔ)。但是由于這是另外一個(gè)話題,和我們現(xiàn)在所要描述的東西沒有太多關(guān)系,所以這里就不再詳細(xì)說明下去了。
?
2.?ViewState可以跟蹤值的變化
你知道嗎?如果你設(shè)置一個(gè)控件的屬性值,那么你會(huì)把ViewState中這個(gè)屬性值對(duì)應(yīng)的數(shù)據(jù)弄臟(dirty)的。當(dāng)然數(shù)據(jù)這個(gè)和數(shù)據(jù)庫中的臟數(shù)據(jù)不同,這里的臟可以理解為“發(fā)生變化”的意思。你知道為什么StateBag會(huì)存在,而不會(huì)被HashTable取代嗎?前面我們可是大肆宣揚(yáng)了一下StateBag和HashTable有多么的像。雖然他們都是通過名值對(duì)的方式來存儲(chǔ)值,但是StateBag還具有對(duì)其中數(shù)據(jù)更改的跟蹤過程(Tracking ability)。是否進(jìn)行跟蹤的開關(guān)可以被設(shè)置成開或者關(guān),當(dāng)調(diào)用StateBag.TrackViewState()方法后跟蹤開關(guān)將被開啟。只有在跟蹤的開關(guān)設(shè)置為“開”的情況下StateBag中的數(shù)據(jù)更改才會(huì)被跟蹤,只要數(shù)據(jù)出現(xiàn)修改,那么對(duì)應(yīng)StateBag項(xiàng)的數(shù)據(jù)將會(huì)被標(biāo)記為“臟的”(dirty)。StateBag還提供了檢查一個(gè)數(shù)據(jù)項(xiàng)是否是臟數(shù)據(jù)的方法 -- IsItemDirty(string key)。你也可以在不更改項(xiàng)數(shù)據(jù)數(shù)值的情況下將對(duì)應(yīng)項(xiàng)設(shè)置為臟數(shù)據(jù),這里需要使用SetItemDirty(string key)方法。為了說明這些,我們看一下以下的例子。這里我們假設(shè)當(dāng)前的StateBag跟蹤的開關(guān)是處于關(guān)閉狀態(tài)的。
stateBag.IsItemDirty("key");?//returns?falsestateBag["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! 看到上面的例子應(yīng)該很清楚了,在調(diào)用了TrackViewState()方法后,StateBag開始跟蹤 其所包含項(xiàng)值的變化。再次無論你如何修改StateBag中項(xiàng)的值,都無法把數(shù)據(jù)弄“臟” 的。而且這里還需要注意一點(diǎn),在TrackViewState()方法調(diào)用后,只要是出現(xiàn)了賦值操作 那么就會(huì)使其被標(biāo)記為臟數(shù)據(jù),StateBag并不會(huì)判斷賦值前后對(duì)應(yīng)項(xiàng)的值是否出現(xiàn)了變化。 如下例子所示:
stateBag["key"]?="abc";
stateBag.IsItemDirty("key");?//returns?false
stateBag.TrackViewState();
stateBag["key"]?="abc";
stateBag.IsItemDirty("key");?//returns?true
????????
可能你會(huì)認(rèn)為根據(jù)賦值前后ViewState是否存在變化然后再標(biāo)記是否是臟數(shù)據(jù)這樣更加符合常理。但是必須注意的是ViewState的項(xiàng)是可以存儲(chǔ)任何類型的值的(實(shí)際上任何賦值給ViewState的變量都會(huì)被裝箱為Object類型的變量),所以比較賦值前后的值是否一致實(shí)際上并沒有變面上看的那么容易。而且不是每種類型都是先了IComparable的接口,所以通過調(diào)用CompareTo方法來進(jìn)行比較也是不可行的。另外還有一個(gè)原因,我們知道ViewState還需要將其內(nèi)部的數(shù)據(jù)進(jìn)行序列和反序列化,當(dāng)這些操作發(fā)生后,你得到的對(duì)象已經(jīng)不是原來那個(gè)對(duì)象了,所以比較對(duì)象之間的引用也是無法完成的。基于以上這些原因,ViewState采取了一種簡單的做法,也就意味著ViewState的數(shù)據(jù)變化跟蹤也是一個(gè)簡要的跟蹤。
到這里你可能會(huì)想在設(shè)計(jì)StateBag類的時(shí)候?yàn)槭裁匆蛊渚邆涓檾?shù)據(jù)變化的能力呢?我們?yōu)槭裁匆櫮切┏霈F(xiàn)變化的項(xiàng)呢?(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? ),這個(gè)疑問往往是造成對(duì)ViewState困惑的根源。基于這個(gè)問題我曾經(jīng)和很多人交談過,其中不乏有著多年ASP.NET開發(fā)經(jīng)驗(yàn)的專家,但是很遺憾,我沒有從任何一個(gè)人那里得到我滿意的答案。沒有一個(gè)人能夠解釋清楚為什么StateBag對(duì)數(shù)據(jù)變化的跟蹤是必要的。為了解釋清楚這個(gè)問題,我們首先需要先了解一下ASP.NET是怎樣建立靜態(tài)控件的。所謂靜態(tài)控件(declarative control)就是那些從頁面或者用戶自定義控件的源碼中可以看到聲明代碼的控件。如:
這里在頁面上聲明了一個(gè)Label控件。然后ASP.NET會(huì)解析這段代碼,它首先會(huì)查找那些標(biāo)簽中帶有“runat=server”的代碼,然后根據(jù)類型創(chuàng)建對(duì)應(yīng)類型的控件對(duì)象,接著將標(biāo)簽中設(shè)置的控件屬性值一個(gè)一個(gè)的賦值到控件實(shí)例對(duì)象中。比如例子中我們?cè)O(shè)置了Label對(duì)象的Text屬性,那么在解析的時(shí)候就會(huì)存在一個(gè)類似于:lbl1.Text = "Hello World"的賦值過程。通過反射機(jī)制,ASP.NET可以知道對(duì)應(yīng)的類型是否具有對(duì)應(yīng)的屬性,對(duì)應(yīng)的屬性是什么數(shù)據(jù)類型。這里Text屬性的數(shù)據(jù)類型是String型,對(duì)于數(shù)據(jù)類型不是String的屬性,那么在設(shè)置屬性前ASP.NET必須實(shí)現(xiàn)將String到對(duì)應(yīng)數(shù)據(jù)類型的轉(zhuǎn)換。如:TextBox控件可以設(shè)置Width屬性,但是Width是Unit類型的,所以這里就設(shè)計(jì)到一個(gè)從String到Unit類型的轉(zhuǎn)化過程。
好,到了這,我們?cè)僖郧懊嫠f的內(nèi)容將當(dāng)前發(fā)生的事情再描述一遍。我們已經(jīng)知道大多數(shù)服務(wù)器控件的屬性值最終是存儲(chǔ)在ViewState中。而且如果ViewState已經(jīng)開始了跟蹤數(shù)據(jù),那么此次屬性的賦值就會(huì)導(dǎo)致“臟數(shù)據(jù)”的產(chǎn)生,但是如果ViewState還沒有開始跟蹤數(shù)據(jù),那么臟數(shù)據(jù)的標(biāo)記值就一直為False。現(xiàn)在問題就是在當(dāng)前ASP.NET解析靜態(tài)控件的時(shí)候是否開始跟蹤和是否產(chǎn)生了“臟數(shù)據(jù)”呢?答案是,沒有。原因是此時(shí)的ViewState賦值之前ASP.NET并沒有去調(diào)用TrackViewState()方法,所以ViewState是不會(huì)對(duì)數(shù)據(jù)的更改進(jìn)行跟蹤的。事實(shí)上ASP.NET是在頁面生命周期的OnInit階段才調(diào)用TrackViewState()方法的。這樣做的目的就是讓ASP.NET可以很方便的區(qū)分控件的哪些屬性值在初次聲明后仍未改變,那些屬性值已經(jīng)被改變了(可能是程序的方式也可能是人工輸入的方式)。如果到目前為止你還沒有意識(shí)到這個(gè)觀點(diǎn)很重要的話,那么請(qǐng)繼續(xù)往下讀吧。
3.?序列化和反序列化(SERIALIZATION AND DESERIALIZATION?)
我們先把ASP.NET怎樣解析生成靜態(tài)控件放一邊,我們前面提到的ViewState的兩個(gè)重要功能(1.?ViewState可以像HashTable那樣通過名值對(duì)來存儲(chǔ)值;2. ViewState可以對(duì)那些修改的數(shù)據(jù)進(jìn)行跟蹤。?)現(xiàn)在我們將來討論另外一個(gè)話題,那就是ASP.NET是怎樣通過StateBag類的特性來實(shí)現(xiàn)那些看似詭異的功能的。
如果你在ASP.NET中使用過ViewState,事實(shí)上我相信只要是ASP.NET的開發(fā)者都會(huì)使用過ViewState了。而且可能你也知道了序列化(serialization)的問題。如果是默認(rèn)的方式,那么VIewState中的值會(huì)被序列化成一個(gè)基于Base64編碼的字符串,然后存儲(chǔ)在頁面中一個(gè)叫做_ViewState的隱藏變量中。
這里在繼續(xù)之前,我需要稍稍叉開一下話題先說一些頁面的控件樹。我發(fā)現(xiàn)有不少有多年工作ASP.NET開放經(jīng)驗(yàn)的程序員還不知道控件樹的存在。由于他們僅僅是對(duì).aspx頁面進(jìn)行操作,所以他們僅僅只關(guān)心那些頁面上聲明的控件。但是我們必須認(rèn)識(shí)到頁面的控件實(shí)際是以一顆控件樹存在的,并且控件中還可以包含子控件。這顆控件樹的根節(jié)點(diǎn)就是頁面本身(Page),然后樹的第二層通常是包含3個(gè)控件,它們分別是用于保存表單(<form>)標(biāo)簽前所有信息的文本控件(Literal),然后是表單控件(Form),然后是表單(</form>)標(biāo)簽后面的所有信息的文本控件(Literal)。接著是樹的第三層包含的控件就是在表單標(biāo)簽內(nèi)聲明的那些控件,如果這些控件中還包含子控件,那么這顆控件樹的深度將會(huì)不斷的加深,一直到所有頁面的控件都被包含在這顆控件樹中。每個(gè)控件都會(huì)有自己的ViewState對(duì)象,并且由于這些控件共同的基類(System.Web.UI.Control)中包含一個(gè)受保護(hù)(protected)的方法SaveViewState,方法的返回值是一個(gè)Object變量。在Control.SaveViewState方法中如果發(fā)現(xiàn)ViewState不為空,那么就直接調(diào)用其私有變量_viewState(StateBag類型)的SaveViewState方法。通過閱讀這個(gè)方法,可以發(fā)現(xiàn)其作用就是將ViewState中被標(biāo)記為臟數(shù)據(jù)(dirty)的項(xiàng)的鍵和值都存儲(chǔ)在一個(gè)ArrayList中,然后再將這個(gè)ArrayList進(jìn)行返回。通過遞歸的方法遍歷整個(gè)控件樹的各個(gè)節(jié)點(diǎn),并遞歸的調(diào)用各個(gè)控件的SaveViewState方法,這樣當(dāng)整個(gè)控件樹被遍歷完成以后,那么和控件樹一一對(duì)應(yīng)的會(huì)形成一個(gè)由ViewState的值組成的數(shù)據(jù)樹。
在這個(gè)階段,ViewState中存儲(chǔ)的數(shù)據(jù)還沒有被轉(zhuǎn)化為我們?cè)赺ViewState隱藏變量中存儲(chǔ)的Base64編碼的字符串。這里僅僅是形成了一顆需要被持久化存儲(chǔ)的數(shù)據(jù)樹。這里再強(qiáng)調(diào)一下,存儲(chǔ)的數(shù)據(jù)是ViewState中那些被標(biāo)記為Dirty的項(xiàng)。StateBag類具有跟蹤功能就是為了在存儲(chǔ)的時(shí)候判斷哪些數(shù)據(jù)需要被存儲(chǔ),哪些數(shù)據(jù)不需要被存儲(chǔ)(實(shí)際上這是StateBag具有跟蹤數(shù)據(jù)功能的唯一原因)。很聰明是吧,但是如果使用不當(dāng)?shù)脑?#xff0c;在ViewState中依然可能保存一些不必要的數(shù)據(jù)。我會(huì)在后面的例子中來說明這些可能犯的錯(cuò)誤。(. 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)
如果你已經(jīng)讀到了這里,那么祝賀你,我要獎(jiǎng)勵(lì)一下這么有毅力的你。咱們來個(gè)突擊測試如何?我是不是人很好呢?哈哈。題目是這樣的,我們有兩個(gè)幾乎一模一樣的.aspx頁面,我們分別稱之為Page1.aspx和Page2.aspx, 每個(gè)頁面都存在一個(gè)form,其中包含一個(gè)Label控件,如下所示:
??????<asp:Label?id="label1"runat="server"Text=""/>
</form> 這兩個(gè)頁面唯一的區(qū)別是Label中包含的值不同(Label.Text的值)。Page1.aspx中的 label1.Text = "abc",如下代碼所示。
<asp:Label?id="label1"runat="server"Text="abc"/>
那么對(duì)于Page2.aspx中的Label,我們對(duì)其多賦一點(diǎn)值(就來個(gè)美國憲法的序言吧)。如下代碼所示。
現(xiàn)在我們?cè)跒g覽器中運(yùn)行Page1.aspx,那么我們將看到一個(gè)abc。然后你通過瀏覽器查看頁面的HTML源碼,你可以找到那個(gè)“臭名昭著”的隱藏字段(_ViewState)。然后把Page1.aspx的_ViewState值保留下來。接著運(yùn)行Page2.aspx,同樣保留其_ViewState的值。然后比較這兩個(gè)ViewState的大小(注意:這里比較的是大小,或者說比較字符串的長度,而不是內(nèi)容)。問題來了,請(qǐng)問這兩個(gè)ViewState的大小是否一樣呢?好了,在公布答案之前我們?cè)偃タ纯戳硗庖粋€(gè)問題,我們?cè)趦蓚€(gè)頁面上都增加一個(gè)Button控件,這樣通過點(diǎn)擊Button按鈕我們就可以回傳頁面了。一下就是頁面中聲明Button控件的代碼:
<asp:Button?id="button1"runat="server"Text="Postback"/>這個(gè)Button并沒有任何的Click事件處理函數(shù),僅僅用于將頁面提交服務(wù)器。我們?cè)僦貜?fù)上面的實(shí)驗(yàn),唯一不同的是,我們這回是在點(diǎn)擊了Button后再去查看各自得_ViewState的值,我們的問題還是一樣的,請(qǐng)問這兩個(gè)ViewState的大小是否一樣呢?好,現(xiàn)在揭曉正確答案。第一個(gè)問題的答案是:是的,兩個(gè)頁面的ViewState的大小是一樣的。原因是這當(dāng)前這兩個(gè)ViewState中并不包含任何和Label有關(guān)的數(shù)據(jù)。前面我們知道所有需要存儲(chǔ)在_ViewState中的數(shù)據(jù)都必須是被標(biāo)記為Dirty的臟數(shù)據(jù)。而需要啟動(dòng)對(duì)ViewState中各項(xiàng)數(shù)據(jù)的跟蹤,必須先要調(diào)用TrackViewState()方法,什么時(shí)候調(diào)用TrackViewState方法呢?是在頁面生命周期中的OnInit階段,而由于Label控件中的Text是在頁面中靜態(tài)聲明的,所以在AddParsedSubObject階段(早于OnInit階段)Text值就已經(jīng)被賦值到對(duì)應(yīng)Label控件中了。所以這些Text中的值將不會(huì)被標(biāo)記為Dirty,同時(shí)也不會(huì)被保存在_ViewState中。所以無論Label.Text有什么不同,那么其頁面的_ViewState始終是相同的。那頁面中那一小段的_ViewState到底包含了什么信息呢?你可以用ViewState Decoder工具查看一下,可以發(fā)現(xiàn)在這樣一個(gè)簡單的界面,_ViewState僅僅包含了頁面的哈希代碼(HashCode)。
好,讓我們到第二個(gè)問題(頁面加了Button那個(gè)情況),答案同樣是:是的,它們的大小也是一樣的。原因和上面解釋的一樣。簡單的說就是在TrackViewState()方法后面并沒有對(duì)Label.Text屬性進(jìn)行賦值操作,所以ViewState中的項(xiàng)并沒有被標(biāo)記為Dirty,自然就不會(huì)被序列化并記錄到_ViewState隱藏變量中了。
到此為止我們已經(jīng)基本了解了ASP.NET平臺(tái)是怎樣決定一個(gè)數(shù)據(jù)是否需要被序列化并永久保留在_ViewState中了(那些被標(biāo)記為Dirty的數(shù)據(jù))。至于ASP.NET是怎樣序列化這些數(shù)據(jù)的已經(jīng)不是本文的范圍了,如果你有興趣進(jìn)一步了解的話,那么請(qǐng)參看如下兩篇文章:?LosFormatter for ASP.NET 1.x? 和??ObjectStateFormatter for ASP.NET 2.0。
在這一個(gè)小節(jié)的最后,我們要簡單的說說反序列化。反序列化和序列化是相對(duì)應(yīng)的,如果不能通過反序列化來講序列化的對(duì)象進(jìn)行還原,進(jìn)行進(jìn)行操作的話,那么序列化操作將沒有任何意義。但是這是另外一個(gè)話題,所以這里就不再進(jìn)行贅述。
4. 自動(dòng)恢復(fù)數(shù)據(jù)(AUTOMATICALLY RESTORES DATA)
到此為止我們已經(jīng)說到了ViewState最后一個(gè)功能,那就是自動(dòng)恢復(fù)數(shù)據(jù)。有些文章將這個(gè)過程和上面提到的反序列化過程混淆在一起,這樣的理解是不正確的,實(shí)際上自動(dòng)恢復(fù)數(shù)據(jù)的過程并不是反序列化過程的一部分。ASP.NET首先反序列化_ViewState中的值,將其還原為對(duì)象,然后再將這些還原的值重新賦值給其對(duì)應(yīng)的控件。
作為所有控件包括Page類基類的System.Web.UI.Control類型中包含一個(gè)LoadViewState(object savedState)方法。其中需要被載入的數(shù)據(jù)就是通過參數(shù)savedState進(jìn)行傳遞的。LoadViewState和前面所說的SaveViewState是相對(duì)應(yīng)的方法。而且和SaveViewState方法類似的是,Control.LoadViewState也是簡單的調(diào)用了StateBag中的LoadViewState方法。通過查看LoadViewState的源代碼可以發(fā)現(xiàn),這個(gè)函數(shù)實(shí)際就是將savedState中存儲(chǔ)的名值對(duì)重新Add到StateBag列表中(StateBag.Add(key, value))。同時(shí)我們從LoadViewState也可以發(fā)現(xiàn)在.NET Framework 1.1中傳入的object變量是一個(gè)pair類型的變量。pair類型包含兩個(gè)屬性First, Second都是object類型的變量,在ViewState中其中一個(gè)屬性存儲(chǔ)的是包含ViewState.Item.Key的ArrayList而另外一個(gè)屬性包含的是ViewState.Item.Value的ArrayList,相對(duì)應(yīng)的Key和Value在ArrayList中的下標(biāo)相同。然后StateBag類就通過遍歷兩個(gè)ArrayList將值添加到狀態(tài)項(xiàng)中(注意在.NET Framework 2.0中這個(gè)方法的實(shí)現(xiàn)有些小小的改動(dòng),放棄使用Pair類型而僅僅使用一個(gè)ArrayList, ArrayList中每個(gè)名值對(duì)占兩個(gè)Item, 前一個(gè)為key后一個(gè)為value, 循環(huán)的時(shí)候以步進(jìn)2進(jìn)行循環(huán))。這里需要注意的是從LoadViewState()重新載入到ViewState的數(shù)據(jù)僅僅包含前一次請(qǐng)求被標(biāo)記為Dirty的那些數(shù)據(jù)(注意不是當(dāng)次請(qǐng)求(current request),而是前一次請(qǐng)求(previous request)就是當(dāng)前請(qǐng)求的前一次請(qǐng)求。)在載入_ViewState中包含的數(shù)據(jù)之前,對(duì)應(yīng)控件的ViewState中可能已經(jīng)包含了一些值了,比如那些靜態(tài)控件中預(yù)先聲明好的值(如:<asp:Label Text="abc"/>中的Text屬性在LoadViewState()之前就已經(jīng)是"abc"了)。如果LoadViewState()中需要載入的數(shù)據(jù)中已經(jīng)存在值了,那么對(duì)應(yīng)的值將被新值所覆蓋。
為了讓大家有一個(gè)完整的認(rèn)識(shí),這里將頁面回傳以后發(fā)生的事情再簡單的描述一下。首先頁面回傳以后,整個(gè)Page將重新生成并且那些頁面上聲明的靜態(tài)控件也都已經(jīng)被解析添加到以Page為根節(jié)點(diǎn)的控件樹中,那些靜態(tài)控件對(duì)應(yīng)的靜態(tài)聲明的屬性值也都被初始化。然后是OnInit階段,在這個(gè)階段ASP.NET會(huì)調(diào)用TrackViewState方法,從此以后所有對(duì)控件屬性的賦值操作都將導(dǎo)致被跟蹤。接著就是LoadViewState()方法被調(diào)用,這里那些從_ViewState中反序列化出來的值將被重新賦給對(duì)應(yīng)的控件,由于在此之前TrackViewState()已經(jīng)被調(diào)用了,_ViewState中包含的數(shù)據(jù)對(duì)應(yīng)的屬性值都會(huì)被標(biāo)記為Dirty。這樣當(dāng)調(diào)用SaveViewState的時(shí)候,這些屬性值還是會(huì)被持久的保留到_ViewState中,這樣在頁面的一次次回傳和頁面一次次的重新建立的過程中,這些控件的值就被保留下來了。現(xiàn)在是不是有種豁然開朗的感覺?恭喜你,你現(xiàn)在已經(jīng)是一個(gè)ViewState管理的小小專家了:)。
一些常見的ViewState使用錯(cuò)誤(IMPROPER USE OF VIEWSTATE)
到目前為止我們已經(jīng)大致了解了ViewState運(yùn)行機(jī)制了,我們可以再次回顧一下我們?cè)谑褂肰iewState中的一些錯(cuò)誤,然后分析其原因。有些錯(cuò)誤在你了解了ViewState以后是顯而易見,但是有些錯(cuò)誤卻比較隱蔽,但是通過對(duì)這些錯(cuò)誤的深入分析將會(huì)讓你對(duì)ViewState有進(jìn)一步的了解。?
????????????錯(cuò)誤使用ViewState的情況(CASES OF MISUSE)
1. 為服務(wù)器端控件(webcontrol)設(shè)置默認(rèn)值(Forcing a Default)
注:這里我個(gè)人認(rèn)為原文的例子存在問題,所以我這里按照自己的理解來謝。大家如果看了原文有不同的理解的話,歡迎和我進(jìn)行交流。
這個(gè)錯(cuò)誤是開發(fā)服務(wù)器端控件(WebControl)中最常見的錯(cuò)誤,不過這個(gè)錯(cuò)誤修改起來非常的簡單,而且修改后的代碼會(huì)更加的簡潔明了(事情往往就是這樣,約正確的方式,越優(yōu)的方式往往也是最簡明的方式。be simple is good)。造成這種錯(cuò)誤的原因往往是開發(fā)人員沒有了解ViewState的跟蹤機(jī)制或者根本就不知道有跟蹤機(jī)制這種說法。我們來看一個(gè)例子,我們現(xiàn)在需要一個(gè)空間,這個(gè)控件有一個(gè)Text屬性,如果沒有對(duì)Text進(jìn)行賦值,那么就從一個(gè)Session變量中得到其默認(rèn)值。我們的程序員Joe寫下了如下代碼:
publicclassJoesControl?:?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) 的條件設(shè)置為if (this.Text == null)就是指當(dāng)Text屬性沒有賦值時(shí),那么就賦初值。)
以上代碼存在一個(gè)問題,第一個(gè)問題是Joe花了大力氣為控件設(shè)置一個(gè)Text,他希望使用者可以對(duì)這個(gè)控件賦值。Jane是其中一個(gè)使用者,她寫下了如下的代碼:
<abc:JoesControl?id="joe1"runat="server"/>當(dāng)Jane查看其頁面HTML源代碼的時(shí)候,她發(fā)現(xiàn)她的頁面ViewState的體積也變大了。天哪,要知道Jane的頁面上僅僅只有Joe的那個(gè)控件了。還了,你知道世界上的男女關(guān)系啦,Jane肯定是去讓Joe去修改他這個(gè)蹩足的控件了,不過讓人高興的是這回Joe修改后的控件似乎工作的很好了。這就是Joe的第二次實(shí)現(xiàn)方式:
publicclassJoesControl?:?WebControl?{????
????public?string?Text?
????{????????
????????get?
????????{????????????
????????????return?this.ViewState["Text"]?==?null?Session["SomeSessionKey"]:this.ViewState["Text"]?as?string;????????
????????}????????
????????set?
????????{?
????????????this.ViewState["Text"]?=?value;?
????????}????
????}
}
看看這段代碼,多么簡潔!而且Joe也不必再去重寫控件的OnLoad方法了。這個(gè)時(shí)候Jane再次使用了這個(gè)控件,當(dāng)Jane設(shè)置了控件的Text屬性時(shí),她將得到她先前設(shè)置的值。如果Jane沒有設(shè)置值,那么她將得到Session["SomeSessionKey"]中存儲(chǔ)的默認(rèn)值。并且Jane也發(fā)現(xiàn)她的頁面HTML源碼的ViewState大小并沒有因?yàn)樘砑恿薐oe的控件而增加。大家都很開心!那么前面的代碼為什么會(huì)存在問題呢:
1.?為什么第一種實(shí)現(xiàn)方式會(huì)使頁面的ViewState大小變大?
這里先要說明的是,如果在使用JoesControl的時(shí)候賦了初值,如下:
<abc:JoesControl?id="joe1"runat="server"Text="ViewState?rocks!"/>這樣和后面的實(shí)現(xiàn)方式在現(xiàn)實(shí)上也是沒有區(qū)別的。因?yàn)檫@里并沒有執(zhí)行this.Text = Session["SomeSessionKey"]這個(gè)語句,自然this.Text并不認(rèn)為出現(xiàn)了變化,那么ViewState["Text"]并不會(huì)被標(biāo)記為Dirty,所以也不會(huì)被序列化到_ViewState中。現(xiàn)在我們討論一下如果沒有設(shè)置Text屬性初值的情況,那么這個(gè)時(shí)候就會(huì)在JoesControl的OnLoad方法中執(zhí)行this.Text = Session["SomeSessionKey"]這個(gè)語句,但是這個(gè)時(shí)候各個(gè)控件已經(jīng)執(zhí)行完成了OnInit階段,所以TrackViewState()已經(jīng)調(diào)用,這個(gè)時(shí)候this.Text已經(jīng)被標(biāo)記為Dirty了,所以會(huì)被持久化到_ViewState隱藏變量中,這樣就增加了ViewState的大小。那么如果使用了第二種方法,判斷是否設(shè)置了初值,如果沒有那么就通過Session["SomeSessionValue"]中的默認(rèn)值替代,這個(gè)階段是在生成JoesControl(New JoesControl)的時(shí)候進(jìn)行賦值的,這個(gè)時(shí)候由于還未到達(dá)OnInit階段,所以TrackViewState()方法還沒有被調(diào)用,所以ViewState["Text"]并不會(huì)被標(biāo)記為Dirty,當(dāng)然也就不會(huì)記錄到_ViewState中進(jìn)行持久化。所以第二種實(shí)現(xiàn)方式是優(yōu)于第一種實(shí)現(xiàn)方式的。
2. 持久化靜態(tài)數(shù)據(jù)(Persisting static data)
我們這里所說的靜態(tài)數(shù)據(jù)是那些不會(huì)被改變的數(shù)據(jù)(never change)或者在頁面的生命周期中、一個(gè)用戶會(huì)話中不會(huì)被改變的數(shù)據(jù)。還是我們可愛的程序員Joe,最近他又接到了一個(gè)改造網(wǎng)站的任務(wù),在他們公司的eCommerce網(wǎng)站上顯示那些已經(jīng)登錄的用戶,比如“嗨,XXXX,歡迎回來!”Joe的前提條件是這個(gè)網(wǎng)站已經(jīng)有了一個(gè)業(yè)務(wù)層的API,可以通過CurrentUser.Name的方法方便的得到當(dāng)前已經(jīng)驗(yàn)證的用戶姓名。剩下的把這個(gè)人名顯示到頁面上的工作就看Joe的了。以下是Joe的代碼:
<!--用于顯示登錄用戶姓名的Label控件-->
<asp:Label?id="lblUserName"runat="server"/> ? (ShoppingCart.aspx.cs)
//用于在Label中動(dòng)態(tài)顯示登錄用戶姓名的代碼;
protectedoverridevoidOnLoad(EventArgs?args)?
{????
?????this.lblUserName.Text?=?CurrentUser.Name;????
?????base.OnLoad(args);
}
?
好了,F5,運(yùn)行,一切正常,Joe又開始得意洋洋了。但是我們知道其實(shí)這里Joe還是犯了個(gè)錯(cuò)誤。用戶的名稱不僅僅會(huì)顯示在Label中,同樣還會(huì)被序列化到_ViewState中,并根據(jù)頁面/服務(wù)器之間的來來回回而不停的被序列化、反序列化...。這個(gè)開銷是值得的嗎?Joe聳聳肩說,這有什么關(guān)系,就那么幾個(gè)字節(jié)而已。但是可以節(jié)約一點(diǎn)為什么不節(jié)約呢,而且解決的方法還是如此的簡單。第一種方法,不用修改源代碼,直接禁用Label控件的ViewState,如:
<asp:Label?id="lblUserName"runat="server"EnableViewState="false"/>
好了,問題解決了。但是是否有更加好的解決方法呢?有!Label控件可能是ASP.NET中最最被高估的控件了。這個(gè)可能是由于那些WinForm的VB編程者,在WinForm中如果要顯示一些文本信息,你可能需要一個(gè)Label。而ASP.NET中的這個(gè)Label可能被認(rèn)為和WinForm中的Label是等價(jià)的了。但是真的就是這樣的嗎?通過HTML源碼我們可以看到Label控件實(shí)際被解析成了HTML中的<span>標(biāo)簽。你必須問問你自己是否真的需要這個(gè)<span>標(biāo)簽?zāi)?#xff1f;如果不需要涉及到特定的格式,僅僅是顯示信息那么我覺得答案是否定的。請(qǐng)看:
恩,這樣你就可以避免生成一個(gè)<span>標(biāo)簽了,并且可以很好的解決問題。但是從編程習(xí)慣上來說,這種將前臺(tái)和后臺(tái)代碼混合的形式是不提倡的,這樣會(huì)使代碼的可讀性下降,并且使開發(fā)的職責(zé)無法明確區(qū)分。所以這里還可以使用一種ASP.NET中存在但是確被Label控件的光環(huán)籠罩的控件 -- Literal。這個(gè)控件僅僅將其Text中的內(nèi)容輸出到客戶端,并且不會(huì)生成<span>標(biāo)簽。是不是覺得對(duì)這個(gè)控件有些印象,對(duì)了,前面在說道將頁面解析成一個(gè)控件樹的時(shí)候,第二層一般由三個(gè)控件組成,一個(gè)是Literal,用于存儲(chǔ)到<form>標(biāo)簽以前的所有html代碼。就是這個(gè)控件。以下就是使用Literal控件來替代Label控件的方法。當(dāng)然這里也需要將EnableViewState設(shè)置為false。問題解決了的同時(shí),我們節(jié)省了網(wǎng)絡(luò)傳輸?shù)馁Y源。不錯(cuò)!
<asp:Literal?id="litUserName"runat="server"EnableViewState="false"/>3. 持久化廉價(jià)的數(shù)據(jù)(Persisting cheap data)? 這個(gè)問題實(shí)際上包含了第一個(gè)問題。靜態(tài)數(shù)據(jù)往往是很容易就可以得到的(取得的開銷 /成本比較小),但是并不是所有容易取得的數(shù)據(jù)都是靜態(tài)數(shù)據(jù)。可能這些數(shù)據(jù)會(huì)不停 的被更改,但是總體來說得到這些數(shù)據(jù)的成本很低。一個(gè)典型的例子是美國各個(gè)州的列表。 除非你要回到1787年12月7日(here),那么當(dāng)前美國的所有州列表在短期內(nèi)是不會(huì)有改變的。 當(dāng)然我們現(xiàn)在的程序員都很痛恨硬編碼。“讓我把美國各個(gè)州的列表都靜態(tài)的寫在頁面 上?傻子才這樣做呢。”我們更加傾向于將州名都保留在一個(gè)數(shù)據(jù)庫(或者其他易于 修改的配置文件中。),這樣如果州名或者州的列表出現(xiàn)了任何變化,就不用修改源 代碼了。恩,我完全同意這一點(diǎn),我們的著名程序員Joe也是這樣認(rèn)為的,而且這張表 在他們公司已經(jīng)存在了,表名叫做USSTATES,這回Joe的任務(wù)就是和操作這張表有關(guān)系的。 下面是用于顯示美國各個(gè)州列表的下拉菜單(DropDownList):
<asp:DropdownList?id="lstStates"runat="server"DataTextField="StateName"DataValueField="StateCode"/> 這里顯示的是綁定從數(shù)據(jù)庫中取得的美國州列表的數(shù)據(jù)代碼: protectedoverridevoidOnLoad(EventArgs?args)?
{????
????if(!this.IsPostback)?
????{???????
????????this.lstStates.DataSource?=?QueryDatabase();????????
????????this.lstStates.DataBind();????
????}????
????base.OnLoad(e);
}
由于美國50個(gè)州是在OnLoad階段中被綁定到下拉菜單(DropDownList)中的,所以這些信息在綁定到下拉菜單的同時(shí),還被序列化并被記錄到了ViewState中了。天哪,那可能一個(gè)龐大的數(shù)據(jù),特別是對(duì)于那些低速接入網(wǎng)絡(luò)的用戶。你知道嗎,我好幾次都想給我的奶奶講解為什么網(wǎng)絡(luò)這么慢(那是因?yàn)槟愕碾娔X正在請(qǐng)求所有美國的州呢,能不慢嗎?),但是我想我的奶奶是不會(huì)懂了。我想她可能會(huì)開始跟我說,在她年輕的時(shí)候美國只有46個(gè)州。那4個(gè)新增的州,真是可惡,它們拉慢了我們的網(wǎng)絡(luò)。但是我們又有什么辦法呢?(我們可都是平民百姓。:D)
這個(gè)問題和上面提到的靜態(tài)數(shù)據(jù)有些類似,一種比較通用的解決方法就是將控件的EnableViewState屬性設(shè)置為False。但是這種解決方法并不是萬能藥,比如我們現(xiàn)在的例子,如果Joe僅僅是將用于顯示美國各州的DropDownList的EnableViewState控件設(shè)置為false,并且將OnLoad函數(shù)中的!Page.IsPostBack的限制條件去掉(這樣就保證每次載入頁面后DropDownList都會(huì)被重新綁定,而不會(huì)再頁面回傳以后導(dǎo)致DropDownList中的數(shù)據(jù)丟失。),那么在使用的時(shí)候,Joe就會(huì)發(fā)現(xiàn)他有麻煩了。什么麻煩呢?“當(dāng)頁面回傳以后,Joe發(fā)現(xiàn)他先前選擇的州并不是下拉菜單(DropDownList)中的默認(rèn)值。”(注意這里的DropDownList是靜態(tài)控件才會(huì)出現(xiàn)上面說的這種情況,如果是在OnLoad中動(dòng)態(tài)生成的DropDownList控件然后再綁定數(shù)據(jù)那么不會(huì)出現(xiàn)此問題)怎么會(huì)這個(gè)樣子!!這是對(duì)ViewState的另外一個(gè)誤解。下拉菜單之所以沒有保留頁面回傳前的選擇值并不是因?yàn)槲覀兘昧讼吕藛蔚腣iewState。在頁面回傳的時(shí)候還有一些用于獲取頁面信息的控件值并不是通過ViewState來進(jìn)行保存的,他們是通過名值對(duì)的方式通過Http請(qǐng)求(HttpRequest)的方式進(jìn)行回傳的,這些值被稱為回傳值(PostData)(可以通過將回傳方式修改為GET來從URL中查看存在哪些回傳值)。所以即便是我們禁用了DropDownList的ViewState,DropDownList依然可以將那個(gè)選擇的值回傳服務(wù)器。這里之所以下拉菜單(DropDownList)會(huì)在頁面回傳后“忘記”上次選擇的值是因?yàn)樵贠nLoad階段之前的ProcessPostData已經(jīng)對(duì)DropDownList設(shè)置了默認(rèn)值,但是這個(gè)時(shí)候DropDownList還沒有ListItem,自然無法設(shè)置到最后一次回傳選擇的值。然后是OnLoad事件中對(duì)DropDownList進(jìn)行數(shù)據(jù)綁定,但是由于沒有執(zhí)行ProcessPostData方法所以不會(huì)再次設(shè)置默認(rèn)值。前面的括號(hào)中有說明,如果這個(gè)DropDownList控件也是在OnLoad中動(dòng)態(tài)生成的,那么由于進(jìn)度追趕,在OnLoad階段后還會(huì)重新執(zhí)行一次ProcessPostData,在這里又會(huì)把下拉菜單中的值設(shè)置為默認(rèn)值,所以說以上描述的問題僅僅只有在DropDownList為靜態(tài)控件的時(shí)候才會(huì)存在。幸運(yùn)的是我們解決這個(gè)問題的方法也很簡單,我們將綁定數(shù)據(jù)的代碼移動(dòng)到OnInit階段,這個(gè)階段將先于ProcessPostData執(zhí)行,所以下拉菜單將被設(shè)置為最后一次回傳的默認(rèn)值。
?
protectedoverridevoidOnInit(EventArgs?args)?{????
???????this.lstStates.DataSource?=?QueryDatabase();????
???????this.lstStates.DataBind();????
???????base.OnInit(e);
}
上面這種方法適用于幾乎所有的廉價(jià)數(shù)據(jù)(cheap data,容易獲得的,獲得的代價(jià)很低的數(shù)據(jù))。你可能會(huì)反駁我說如果每次都重新去取數(shù)據(jù),如:每次都連接數(shù)據(jù)庫去取得對(duì)應(yīng)的數(shù)據(jù)可能會(huì)比將數(shù)據(jù)存儲(chǔ)在ViewState中代價(jià)更高,但是我不這樣認(rèn)為。當(dāng)前的數(shù)據(jù)庫管理系統(tǒng)(DBMS, 如SQL Server)已經(jīng)相當(dāng)?shù)某墒?#xff0c;它們往往具有良好的緩存機(jī)制,如果配置得當(dāng)?shù)脑拡?zhí)行的效率也非常高。(譯者:我也有這樣的經(jīng)驗(yàn),我曾經(jīng)比較兩種處理數(shù)據(jù)的方式,其中一種是先取得一個(gè)大范圍的數(shù)據(jù),然后在代碼中通過循環(huán)的方式將其中不符合條件的數(shù)據(jù)過濾掉;另外一種方式是直接通過SQL語句在數(shù)據(jù)庫中進(jìn)行數(shù)據(jù)篩選,然后將符合條件的數(shù)據(jù)進(jìn)行返回。根據(jù)頁面顯示的速度來判斷,后者的執(zhí)行效率遠(yuǎn)遠(yuǎn)高于前者。)其實(shí)想想到底是將一堆無用的數(shù)據(jù)通過56kbps的速度和千里之外的客戶傳來傳去還是將少許的數(shù)據(jù)在可能只相距幾百英尺的應(yīng)用服務(wù)器和數(shù)據(jù)庫服務(wù)器之間傳遞(它們之間的連接速度一般都高于10M)的代價(jià)高,這個(gè)結(jié)果應(yīng)該已經(jīng)很明顯了。當(dāng)然如果你一定想精益求精的話,那么你可以選擇把一些常用不易變的數(shù)據(jù)緩存起來這樣可以進(jìn)一步的提高性能。
?
讓我們?cè)僖黄鹈鎸?duì)這樣一個(gè)事實(shí),我們不可能什么都事先把所需的控件聲明好,有時(shí)候頁面的顯示,控件的現(xiàn)實(shí)和外觀都和一定的業(yè)務(wù)邏輯有關(guān)系(這不正是我們程序員存在的原因嗎?)。但是麻煩的是ASP.NET并沒有提供一種簡單的方式讓我們“正確”的創(chuàng)建動(dòng)態(tài)控件。當(dāng)然我們可以通過重寫OnLoad方法并在這里聲明動(dòng)態(tài)的控件,事實(shí)上我們也常常這樣做,但是這樣做的結(jié)果有時(shí)候會(huì)讓我們?cè)赩iewState持久化了一些本不應(yīng)該持久化的數(shù)據(jù)。我們同樣可以重寫OnInit方法,但是同樣的問題依然存在。還記得我們前面提到過ASP.NET在OnInit階段是怎樣調(diào)用TrackViewState()方法的嗎?它是從控件樹的底部遞歸調(diào)用每個(gè)子控件的TrackViewState()方法,最后一個(gè)調(diào)用的就是控件樹的根節(jié)點(diǎn)(Page),所以當(dāng)你在Page.OnInit階段的時(shí)候?qū)?dòng)態(tài)控件進(jìn)行操作的話,那么頁面的子控件的TrackViewState已經(jīng)被調(diào)用了,所以這個(gè)時(shí)候你賦值的數(shù)據(jù)也會(huì)被標(biāo)記為臟數(shù)據(jù)(dirty data)并最終被ViewState進(jìn)行持久化保存。讓我們?cè)倏纯碕oe的例子,Joe在頁面中定義了用于顯示當(dāng)前日期和時(shí)間的標(biāo)簽控件(Label),聲明代碼如下所示: <asp:Label?id="lblDate"runat="server"/>
?
protectedoverridevoidOnInit(EventArgs?args)?{????
????this.lblDate.Text?=?DateTime.Now.ToString("MM/dd/yyyy?HH:mm:ss");????
????base.OnInit(e);
}
?
?
雖然Joe已經(jīng)在最早的事件中將Label的Text屬性設(shè)置為了當(dāng)前的日期時(shí)間信息。但是還是晚了,原因我們前面已經(jīng)分析過了,這個(gè)時(shí)候Label.TrackViewState()已經(jīng)被調(diào)用,所以Label.Text的賦值操作將導(dǎo)致Label.Text被標(biāo)記為臟數(shù)據(jù)(dirty data),從而被記錄到ViewState中。但是這個(gè)是不必要的,應(yīng)為這個(gè)日期很容易得到,可以歸結(jié)于我們前面總結(jié)的“持久化廉價(jià)的數(shù)據(jù)”這個(gè)問題。要解決這個(gè)問題我們可以簡單的將Label控件的EnableViewState屬性設(shè)置為false。但是我們這里將用另外一種方法來解決,因?yàn)檫@種解決方法揭示了一個(gè)重要的概念。首先我們看看Joe的做法,直接將Label中的Text屬性設(shè)置為當(dāng)前時(shí)間信息,如下所示: <asp:Label?id="Label1"runat="server"Text="<%=?DateTime.Now.ToString()?%>"/>?
?
你可能也有過這樣嘗試,但是ASP.NET會(huì)給你當(dāng)頭一棒,它會(huì)明確的告訴你<%= %>語法不能對(duì)服務(wù)器端控件的屬性進(jìn)行賦值操作。當(dāng)然Joe也可以使用<%# %>的方法,但是這個(gè)方法和我們前面提到的禁用Label的ViewState同時(shí)在每次請(qǐng)求頁面的時(shí)候綁定數(shù)據(jù)的方法實(shí)際上是一樣的。問題是我們希望通過編碼的方式為Label的Text屬性的初值進(jìn)行賦值操作(我們不希望這些賦值操作導(dǎo)致ViewState大小的增加),同樣在以后的操作中我們希望這個(gè)Label控件依然可以像一個(gè)普通的Label控件被使用。簡單的說就是這樣,我們需要一個(gè)Label,它的默認(rèn)值是當(dāng)前的日期和時(shí)間,但是如果我們?nèi)斯さ膶?duì)其Label.Text進(jìn)行了賦值操作,那么我們還是希望這個(gè)值在頁面的回傳之間可以保留(即通過ViewState進(jìn)行持久化)。舉個(gè)簡單的例子,Joe的頁面上有一個(gè)按鈕,當(dāng)用戶點(diǎn)擊這個(gè)按鈕那么顯示當(dāng)前日期和時(shí)間的Label將顯示一個(gè)“空時(shí)間”(即:“--/--/---- --:--:--”),此按鈕的響應(yīng)代碼為: privatevoidcmdRemoveDate_Click(objectsender,?EventArgs?args)?{????
????this.lblDate.Text?=?"--/--/----?--:--:--";
}
如果需要實(shí)現(xiàn)這樣一個(gè)需求那么我們前面的做法(簡單的將Label的EnableViewState屬性設(shè)置為false)將不能解決這個(gè)問題,因?yàn)槿绻脩敉ㄟ^按鈕取消了時(shí)間的顯示,由于Label的ViewState被禁用,那么就意味著Label的值在回傳之間不會(huì)被保存,所以在下次頁面回傳以后Label依然會(huì)顯示當(dāng)前的日期和時(shí)間。那么Joe需要怎么做呢?可憐的Joe總是被無窮無盡的需求折磨著。
實(shí)際上上面的例子描述的就是一個(gè)邏輯,Label控件必須按照邏輯來決定應(yīng)該顯示什么內(nèi)容。上面的邏輯我們簡化的說就是,對(duì)于Label的初值我們不希望它保留在ViewState中而以后如果出現(xiàn)了改變那么我們希望都保留在ViewState中,以便在頁面回傳的過程中進(jìn)行狀態(tài)的保留。從這個(gè)表述我們可以看出,如果我們能在控件的TrackViewState()被調(diào)用前為其賦初值,那么什么問題都解決了。但是前面我提到過,ASP.NET并沒有提供一種簡單的方法來實(shí)現(xiàn)這個(gè)過程(在TrackViewState()被調(diào)用前進(jìn)行操作)。在ASP.NET 2.0的版本中已經(jīng)為我們提供了一些先于OnInit階段的階段(如:OnPreInit階段),這里針對(duì)ASP.NET 1.1版本,我們確實(shí)沒有一個(gè)先于OnInit階段進(jìn)行控件的初值設(shè)置(其實(shí)這個(gè)表述是不正確的,在ASP.NET 1.1中你可以通過重寫DeterminePostBackMode方法來實(shí)現(xiàn)對(duì)控件進(jìn)行賦初值,由于這個(gè)方法先于OnInit方法,所以此時(shí)賦的初值是不會(huì)被記錄到ViewState中)。一下作者提供了另外兩種實(shí)現(xiàn)方法:
同樣在后臺(tái)編寫Label.OnInit事件對(duì)應(yīng)的響應(yīng)函數(shù)并對(duì)Label.Text賦初值也是可以的。
? 2.?創(chuàng)建用戶自定義組件(Create a custom control): publicclassDateTimeLabel?:?Label?
{????
????public?DateTimeLabel()?
????{????????
????????this.Text?=?DateTime.Now.ToString("MM/dd/yyyy?HH:mm:ss");????
????}
}
這里在構(gòu)造函數(shù)中就對(duì)Label.Text的屬性進(jìn)行初值賦值,一定是在TrackViewState()方法之前,所以這樣也可以達(dá)到我們前面提到的目的。
5.?以編碼的方式創(chuàng)建動(dòng)態(tài)控件(Initializing dynamically created controls programmatically)
{????
????protected?override?void?CreateChildControls()?
????{????????
????????Label?l?=?new?Label();?????????
????????this.Controls.Add(l);????????
????????l.Text?=?"Joe's?label!";????
????}
}
好了,我們現(xiàn)在考慮的是那些被動(dòng)態(tài)創(chuàng)建的控件(例子中是Label控件)什么時(shí)候開始跟蹤它的ViewState呢?我們知道我們可以在頁面生命周期的任何階段動(dòng)態(tài)生成控件并添加到頁面的控件樹中,但是ASP.NET中是在OnInit階段調(diào)用TrackViewState()以開始跟蹤控件ViewState的變化。那么我們這里動(dòng)態(tài)創(chuàng)建的控件是否會(huì)由于錯(cuò)過了OnInit事件從而導(dǎo)致不能對(duì)動(dòng)態(tài)生成的控件的狀態(tài)進(jìn)行跟蹤和持久化呢?答案是否定的,這個(gè)奧秘就是Controls.Add()方法,這個(gè)方法并不像我們?cè)瓉硎褂?span lang="en-us">ArrayList.Add方法僅僅是將一個(gè)Object添加到一個(gè)列表中,Controls.Add()方法在將子控件添加到當(dāng)前控件下后還需要調(diào)用一個(gè)叫做AddedControl()的方法,就是這個(gè)方法對(duì)于那些新加入的控件狀態(tài)進(jìn)行檢查,如果發(fā)現(xiàn)當(dāng)前控件的狀態(tài)落后于頁面的生命周期,那么將會(huì)調(diào)用對(duì)應(yīng)的方法使當(dāng)前控件的狀態(tài)和頁面聲明周期保持一致,這個(gè)過程叫做“追趕(catch up)”。比如我們舉一個(gè)稍稍極端的例子,我們?cè)陧撁嫔芷诘?span lang="en-us">OnPreRender階段動(dòng)態(tài)生成了一個(gè)控件并將其添加到當(dāng)前頁面的控件樹中,那么系統(tǒng)發(fā)現(xiàn)新添加的控件并不是出于OnPreRender狀態(tài)便會(huì)調(diào)用方法使這個(gè)控件經(jīng)歷LoadViewState,LoadPostBackData,OnLoad等方法(頁面聲明周期中的一些私有方法將被忽略),直到這個(gè)控件也到了OnPreRender狀態(tài)。其實(shí)通過查看Temporary ASP.NET Files中編譯過的ASP.NET aspx頁面的類代碼你就可以發(fā)現(xiàn)在創(chuàng)建頁面控件樹的時(shí)候,調(diào)用的是一個(gè)叫做__BuildControlTree()的方法,里面對(duì)于添加子控件使用的是AddParsedSubObject()方法,而這個(gè)方法實(shí)際就是調(diào)用了Controls.Add()方法,同樣的過程。
我們?cè)倩氐?span lang="en-us">Joe編寫的用戶自定義組件,由于CreateChildControls無法確定在何時(shí)被調(diào)用,如果頁面已經(jīng)執(zhí)行到了OnInit階段,那么只要調(diào)用了Controls.Add()方法那么這個(gè)控件馬上就會(huì)被調(diào)用TrackViewState()方法,并立即開始對(duì)ViewState進(jìn)行跟蹤。而Joe的代碼是在this.Contorls.Add(l)之后再對(duì)Label進(jìn)行初值賦值(l.Text = “Joe’s Label!”),這樣”Joe’s Label!”將被添加到ViewState進(jìn)行保存。那么知道了一切原因都源于Controls.Add()方法后,解決方法也就出來了,我們只要顛倒一些最后兩個(gè)語句的順序就可以解決問題,代碼如下所示:
{????
????protected?override?void?CreateChildControls()?
????{????????
????????Label?l?=?new?Label();????????
????????l.Text?=?"Joe's?label!";?????????
????????this.Controls.Add(l);????
????}
}
很玄妙是吧?理解了這個(gè)我們?cè)倩仡^看看我們前面提到的通過下拉菜單(DropDownList)列舉美國所有州的名稱的例子。在前面提供的解決方法中,我們是先禁用DropDownList的ViewState,然后在OnInit階段對(duì)DropDownList進(jìn)行數(shù)據(jù)綁定。那么我們這里又提供了一個(gè)新的解決方法。首先在頁面中去掉靜態(tài)聲明的DropDownList,然后在頁面生命周期OnLoad階段前的任何位置動(dòng)態(tài)生成DropDownList,并且對(duì)其進(jìn)行值的綁定,然后通過Controls.Add()方法將其添加到頁面控件樹中,同樣可以達(dá)到一樣的效果。
{????
????protected?override?void?OnInit(EventArgs?args)?
????{????????
????????DropDownList?states?=?new?DropDownList();????????
????????states.DataSource?=?this.GetUSStatesFromDatabase();????????
????????states.DataBind();?????????
????????this.Controls.Add(states);????
????}
}
這樣做的好處還有,由于DropDownList的EnableViewState = true,?所以DropDownList依然可以觸發(fā)諸如OnSelectedIndexChanged事件。你也可以對(duì)同樣的方法操作DataGrid控件,但是可能對(duì)于使用DataGrid的排序(sorting),分頁(paging)還有SelectedIndex屬性還是存在問題??(這幾個(gè)問題還沒有考究過)
?
和ViewState和平共處(BE VIEWSTATE FRIENDLY)到目前為止,如果你理解了這篇文章中所說的東西那么恭喜你,你已經(jīng)知道ViewState是怎樣實(shí)現(xiàn)其功能的了。知道了ViewState的工作原理我們就可以寫出更加優(yōu)化的代碼,而往往這些更優(yōu)的代碼比那些蹩足的代碼更加簡潔明了.
轉(zhuǎn)載于:https://www.cnblogs.com/lixinkun/archive/2012/08/03/2621311.html
總結(jié)
以上是生活随笔為你收集整理的asp.net ViewState详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 时钟同步及其应用(接上一篇)
- 下一篇: javascript数字验证(转)