ASP.Net WebForm溫故知新學習筆記:二、ViewState與UpdatePanel探秘
原文地址:http://www.cnblogs.com/edisonchou/p/3901559.html
開篇:經歷了上一篇《aspx與服務器控件探秘》後,我們了解了aspx和服務器控件背後的故事。這篇我們開始走進WebForm狀態保持的一大法寶—ViewState,對其刨根究底一下。然後,再對曾經很流行的ASP.Net AJAX方案中的利器—UpdatePanel這個神奇的區域一探究竟。
一、隱藏的狀態—ViewState探秘
1.1 從Http的無狀態說起
Http是一個無狀態協議,同一個會話的連續兩個請求互相不了解,它們由最新實例化的環境進行解析,除了應用本身可能已經存儲在全局對象中的所有信息外,該環境不保存與會話有關的任何信息
PS:這裏我們可以這樣來理解:假如我們去一個大型商場購物購買某個產品,第一次去的時候是A銷售員接待了我們,帶領我們來到XX產品的櫃臺並為我們推薦了XX產品;等我們回去使用XX產品後,覺得XX產品真心不錯。第二次我們又去,但是這次卻找不到上次那個A銷售員了,相反商場分配了另一個B銷售員來接待我們,他不知道我們上次選擇了XX產品,相反它卻一個勁地向我們推薦YY產品並把我們帶向YY產品的櫃臺;這個時候,我們一般會說:我擦,把上次那個妹子給我叫來!
基於Http協議的無狀態特性,我們在ASP.Net的開發中也會經常碰到這種情況:用戶上一次提交的東西,下次再提交時服務器就不記得了。很多時候,我們感到很不解?後來,我們發現原來每一次的請求服務器都開啟了不同的線程來處理,也就是說每次都會new一個XXX.aspx.cs中的類對象實例來進行處理(上一次new出來為我們處理的page對象也許早就被服務器銷毀了)。比如,我們在xxx.aspx.cs代碼中寫入了一個int類型的number成員(初始為0),每次請求我們都想讓這個number自增一下,然後重新返回給瀏覽器。但就是這麽一個簡單的夢想,我們卻無法輕易的實現。
那麽,到底怎麽來破呢?大神們已經為我們想好了策略,我們可以使用隱藏域字段、Cookie、Session等來保存狀態。而偉大的Microsoft還在ASP.Net中幫我們封裝了ViewState
1.2 青春四處綻放—無處不在的ViewState
(1)類似於Dictionary的一種數據結構
如果你曾經使用過Dictionary或者Session的話,那麽你應該了解這種Key/Value的數據結構。這裏並沒有什麽高深的理論,ViewState通過String類型的數據作為索引。ViewState對應項中的值可以存儲任何類型的值(參數是Object類型),實施上任何類型的值存儲到ViewState中都會被裝箱為Object類型。
例如,這裏我們可以改寫上面那個按鈕事件中的代碼:
1 protected void btnGetNumber_Click(object sender, EventArgs e) 2 { 3 //number++; 4 //this.lblNumber.Text = number.ToString(); 5 6 object age = this.ViewState["age"]; 7 if (age == null) 8 { 9 age = 1; 10 } 11 else 12 { 13 age = Convert.ToInt32(age) + 1; 14 } 15 this.ViewState["age"] = age; 16 this.lblNumber.Text = age.ToString(); 17 }
這裏,我們借助ViewState存儲了age的狀態值,第一次來我給你返回1,後面再來我就加1再返回給你。於是,在上一節我們所提到的那個問題(無法記住上次的number值,每次都返回1)就解決了。
PS:ViewState不能存儲所有的數據類型,僅支持以下的這幾種: String、Integer、Boolean、Array、ArrayList、Hashtable以及一些自定義類型
我們都知道,Dictionary和Session都是存儲在服務器端的。那麽,我們不禁要問,既然我們在服務器端給ViewState增加了一個Key/Value對,並返回給瀏覽器端,ViewState又是存儲在什麽位置的呢?
(2)大隱隱於市的“頁面級”隱藏字段
跟Session和Dictionary的存儲位置不同,ViewState的作用域是頁面,也就是說ViewState是存儲在瀏覽器的頁面之中的(這裏相比Session等,耗費的服務器資源較少,也算是ViewState的優點之一吧),當你關閉某個aspx文件後,那麽屬於這個aspx的ViewState也就不存在了。或許,這麽說來,我們還不是很了解,現在我們來實地看看。
①首先,如果頁面上有一個runat="server"的form,當用戶請求這個頁面時,服務器會自動添加一個_ViewState的隱藏域返回給瀏覽器。但是,我們發現這個ViewState的value看起來像一串亂碼?這是為什麽呢?這是因為服務器在向瀏覽器返回html之前,對ViewState中的內容進行了Base64的加密編碼;
②其次,當用戶點擊頁面中的某個按鈕提交表單時,瀏覽器會將這個_VIEWSTATE的隱藏域也一起提交到服務端;服務器端在解析請求時,會將瀏覽器提交過來的ViewState進行反序列化後填充到ViewState屬性中(比如下圖中,我們可以通過一個軟件將_VIEWSTATE解碼得到一個如下圖所示的樹形結構);再根據業務處理需要,從這個屬性中根據索引找到具體的Value值並對其進行操作;操作完成後,再將ViewState進行Base64編碼再次返回給瀏覽器端;
③因此,我們可以得出一個結論:VIEWSTATE適用於同一個頁面在不關閉的情況下多次與服務器交互(PostBack)。這裏我們也可以通過下圖來溫習一下ViewState的流程,ViewState存放著“事故現場”,下次可以方便地“還原現場”,將無狀態的Http模擬成了有狀態的,也讓廣大的初學者了解不到無狀態的這個特性。
1.3 喜歡就會放肆—又愛又恨的ViewState!
事實上,除了我們手動在服務器端向ViewState屬性中添加的K/V對數據,我們在aspx.cs代碼中為某些服務器控件設置的值(例如:為Repeater設置DataSource中存入的數據集、為Label所設置的Text內容等,但不包括:TextBox、CheckBox、CheckboxList、RadioButtonList)都存入了ViewState中。這樣做的話,我們下次再向服務器提交請求時,現有表單中所有的服務器控件狀態都會記錄在ViewState中提交到服務器,在服務器端可以方便地對這些服務器控件進行有狀態的操作並返回,這無疑是讓我們歡喜的,因為方便了我們的開發過程,提高了我們的開發效率;
但有人說:“喜歡就會放肆”,ViewState讓人又愛又恨啊。例如,在我們使用Repeater的過程中,WebForm會自動將DataSource(數據源,你可以理解為一個集合)存儲到ViewState中並返回給瀏覽器。可以參考下面的例子來實地理解一下:
①含有Repeater的aspx頁面:
1 <form id="form1" runat="server"> 2 <div align="center"> 3 <table class="test"> 4 <tr class="first"> 5 <td> 6 ID 7 </td> 8 <td> 9 產品名稱 10 </td> 11 <td> 12 產品描述 13 </td> 14 <td> 15 刪除 16 </td> 17 </tr> 18 <asp:Repeater ID="repeaterProducts" runat="server"> 19 <ItemTemplate> 20 <tr> 21 <td> 22 <%#Eval("Id") %> 23 </td> 24 <td> 25 <%#Eval("Name") %> 26 </td> 27 <td> 28 <%#Eval("Msg") %> 29 </td> 30 <td> 31 <a href=‘Product.ashx?Action=Delete&Id=<%#Eval("Id") %>‘>刪除</a> 32 </td> 33 </tr> 34 </ItemTemplate> 35 </asp:Repeater> 36 </table> 37 </div> 38 </form>
②後臺代碼模擬從數據庫中取得數據集合並綁定到Repeater中:
1 protected void Page_Load(object sender, EventArgs e) 2 { 3 if (!IsPostBack) 4 { 5 this.repeaterProducts.DataSource = this.GetProductList(); 6 this.repeaterProducts.DataBind(); 7 } 8 } 9 10 private IList<Product> GetProductList() 11 { 12 IList<Product> productList = new List<Product>(); 13 productList.Add(new Product() { Id = 1, Name = "康師傅方便面", Msg = "就是這個味兒!" }); 14 productList.Add(new Product() { Id = 2, Name = "統一方便面", Msg = "還是那個味兒!" }); 15 productList.Add(new Product() { Id = 3, Name = "白象方便面", Msg = "大骨濃湯啊!" }); 16 productList.Add(new Product() { Id = 4, Name = "日本方便面", Msg = "不只是愛情動作片!" }); 17 productList.Add(new Product() { Id = 5, Name = "臺灣方便面", Msg = "馬英九誇我好吃!" }); 18 19 return productList; 20 }
編譯生成後,通過查看此頁面的html代碼,可以明顯看到一長串的_VIEWSTATE隱藏域。將此_VIEWSTATE復制到ViewStateDecoder中進行反編碼,可以發現它確實存儲了Repeater中的數據集合。這裏我們不禁要問:展示數據既然已經渲染成了html,為何還要存儲在ViewState隱藏域中?如果我們的數據集合是一百行、一千行數據的話,那ViewState隱藏域豈不很大(100k?200k?)?但不幸的是,這是ViewState的設計機制,要想依靠它來保持狀態,它就會將服務器控件的狀態包括數據集合都存儲到其中,在瀏覽器和服務器之間來回傳遞保持狀態。
這裏就涉及到網站的性能問題的探討了:由於ViewState存儲在頁本身,因此如果存儲較大的值,用戶請求顯示頁面的速度會減慢(這對於互聯網系統來說,就是一個噩夢。你會選擇一個1秒內響應的網站瀏覽還是5秒內響應的網站?)。又因為ViewState會隨同Form表單一同回傳給服務器,如果ViewState很大的話,Http報文也會很大,網站流量消耗也會增大。
那麽,有沒有一種方法可以讓ViewState克制一下呢?別急,請看下面的介紹。
1.4 但愛就是克制—禁用還是不禁用ViewState?
剛剛說到,因為ViewState會一定程度上影響性能,所以我們可以在不需要的時候禁用 ViewState。默認情況下 ViewState 將被啟用,並且是由每個控件(而非頁面開發人員)來決定存儲在 ViewState 中的內容。有時,這一信息對應用程序並沒有什麽用處(例如上面提到的Repeater的數據集合,已經渲染生成了html顯示,還存儲了一份副本在ViewState裏邊)。盡管也沒什麽害處,但卻會明顯增加發送到瀏覽器的頁面的大小。因此如果不需要用ViewState,最好還是將它關閉,特別是當 ViewState 很大的時候。當然,ViewState幫我們實現了某些服務器控件狀態保持,因此在非必需的情況下,還是可以適度使用的,特別是在開發企業內部信息系統的場景。
那麽,怎樣來禁用ViewState呢?禁用ViewState又有什麽策略呢?下面我們一一來探討。
①頁面級禁用ViewState:在aspx的首部的Page指令集中添加EnableViewState="false",該頁面中所有控件的狀態都不會存入ViewState的,頁面一下就會清爽許多;
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="RepeaterViewState.aspx.cs" Inherits="WebFormDemo.RepeaterViewState" EnableViewState="false" %>
禁用後,再次查看生成的html代碼,我們會發現:咦,_VIEWSTATE還在那兒,但是明顯比先前的體積小了不少!
再將這個瘦身後的_VIEWSTATE復制到ViewStateDecoder中進行反編碼查看,我們會發現,只保存了一個最基本的信息,Repeater的那些數據集合沒有存入進去了。
PS:為什麽禁用ViewState之後,頁面源代碼中仍然有_VIEWSTATE的隱藏域?
這是因為就算禁用了viewstate,aspx頁面中還是會有一個服務器控件在那裏使用,這就是<form runat="server">。這時,如果你將form去掉runat="server",將其變為普通html標簽,那麽頁面就幹凈了,從此_VIEWSTATE這個隱藏域徹底消失在你的頁面中。
②控件級禁用ViewState:在某些場景中,我們只希望禁用某個控件(例如Repater)的ViewState,其他控件仍然通過ViewState保持狀態。這時,我們可以給指定的控件設置一個屬性EnableViewState="false"即可;
<asp:Repeater ID="repeaterProducts" runat="server" EnableViewState="false"> </asp:Repeater>
③全局級禁用ViewState:園子裏的大神老趙(Jeffrey Zhao)曾經說過,“我如果新建一個WebForm項目,做的第一件事情就是去Web.config中將enableViewState設置為false從而將ViewState全局關閉”。那麽,我們如果希望將網站中所有頁面的ViewState都禁用,總不可能去一個一個頁面得修改Page指令吧?ASP.Net為我們提供了一個配置,我們只需要在Web.config的system.web中增加一句配置即可:
<pages enableViewState="false" />
PS:開發中也可以采用大神老趙的做法,先禁用,再選擇性啟用,畢竟沒有非要ViewState才能幹成的事兒!
④真正的禁用ViewState:剛剛我們的三種方法實踐後,在頁面還是出現_VIEWSTATE的隱藏域,盡管它保留了最基本的信息。那麽,我們可能會問?怎樣才能徹底地真正地禁用ViewState,根本就別給我生成_VIEWSTATE的隱藏域。答案是有的,將<form runat="server"/>的runat="server"去掉,就不會出現了,但那樣又會偏離WebForm的開發模式,大部分的服務器控件都無法正常使用,開發效率又會有所損失。
綜上所述,在實際開發中應該權衡利弊,特殊情況特殊分析(到底這個場景該不該禁用ViewState),選擇是否禁用ViewState,采用何種方式禁用ViewState。對於ViewState的探秘本篇就到此為止,由於我本人理解的也不是很深刻,所以希望各位園友如果有理解,可以回復出來大家探討共同進步。
二、飛來的利器—UpdatePanel探秘
2.1 從一個簡單四則運算計算器說起
假如有以下一個場景,我們要做一個簡單的四則計算器。aspx頁面代碼和後端邏輯代碼如下:
(1)aspx頁面代碼
<html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>AJAX計算器</title> </head> <body> <form id="form1" runat="server"> <div align="center"> <asp:TextBox ID="txtNumber1" runat="server"></asp:TextBox> <asp:DropDownList ID="ddlFunc" runat="server"> <asp:ListItem Value="0">+</asp:ListItem> <asp:ListItem Value="1">-</asp:ListItem> <asp:ListItem Value="2">*</asp:ListItem> <asp:ListItem Value="3">/</asp:ListItem> </asp:DropDownList> <asp:TextBox ID="txtNumber2" runat="server"></asp:TextBox> <asp:Button ID="btnGetResult" runat="server" Text="=" Width="50" onclick="btnGetResult_Click" /> <asp:Label ID="lblResult" runat="server" Text="" Font-Bold="true"></asp:Label> </div> </form> </body> </html>
(2)後置邏輯代碼
public partial class AjaxCalculator : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btnGetResult_Click(object sender, EventArgs e) { int number1 = Convert.ToInt32(this.txtNumber1.Text); int number2 = Convert.ToInt32(this.txtNumber2.Text); int result = 0; switch(this.ddlFunc.SelectedValue) { case "0": result = number1 + number2; break; case "1": result = number1 - number2; break; case "2": result = number1 * number2; break; case "3": if(number2 == 0) { throw new DivideByZeroException("除數不能為0!"); } result = number1 / number2; break; } this.lblResult.Text = result.ToString(); } }
生成後運行該頁面,可以達到以下的效果。我們輸入兩個數字後,選擇是加法、減法、還是乘除法後,點擊=按鈕,即可刷新頁面顯示運算結果。
在WebForm中,每一次點擊runat="server"的按鈕都會將調用form.submit將請求提交到服務器,服務器會返回新的頁面html進行頁面重繪。這是一個整頁的刷新操作,不符合AJAX的風格需求。因此,我們想要將其改為AJAX版本的,除了使用基本的XMLHttpRequest外,我們還可以使用基於JQuery的AJAX方案,這些都是輕量級的原生態的AJAX技術方案。但我們偉大的微軟(我哭啊,真是為我們考慮啊,連AJAX方案都為我們解決了,而且還提供了AJAX控件供我們使用,我們拖控件的習慣可以用到AJAX方案上了!!!)還為我們提供了一套叫做ASP.Net AJAX的技術方案,通過這套方案,我們可以在ASP.Net很容易地實現AJAX效果,甚至都不需要我們懂JavaScript。因此,也就出現了前些年,很多WebForm開發者陸續使用ASP.Net AJAX Extension進行AJAX開發,紛紛表示:AJAX如此簡單,我等豈能不會?但是,雖然它簡單易行,由於其性能問題一直被人詬病,而我們這些菜鳥也未能了解其性能問題的原因,本著知其然也知其所以然的目標,現在我們來使用它並剖析它一下。
2.2 天上掉下個林妹妹—使用UpdatePanel控件
不得不說,UpdatePanel真的是天上掉下的林妹妹,一個神奇的控件!有了它,我們可以將頁面中需要進行局部刷新的內容放到其ContentTemplate中,一個需要整頁刷新的操作便可以成為局部刷新。現在,我們首先來使用其改造剛剛的簡單四則計算器頁面。
(1)加入UpdatePanel,並將計算器html內容拖入ContentTemplate中
<form id="form1" runat="server"> <div align="center"> <asp:ScriptManager ID="scriptManager" runat="server"> </asp:ScriptManager> <asp:UpdatePanel ID="updatePanel" runat="server"> <ContentTemplate> <asp:TextBox ID="txtNumber1" runat="server"></asp:TextBox> <asp:DropDownList ID="ddlFunc" runat="server"> <asp:ListItem Value="0">+</asp:ListItem> <asp:ListItem Value="1">-</asp:ListItem> <asp:ListItem Value="2">*</asp:ListItem> <asp:ListItem Value="3">/</asp:ListItem> </asp:DropDownList> <asp:TextBox ID="txtNumber2" runat="server"></asp:TextBox> <asp:Button ID="btnGetResult" runat="server" Text="=" Width="50" OnClick="btnGetResult_Click" /> <asp:Label ID="lblResult" runat="server" Text="" Font-Bold="true"></asp:Label> </ContentTemplate> </asp:UpdatePanel> </div> </form>
(2)運行該頁面,通過開發人員工具查看Http請求
通過查看請求報文,我們了解到此次的請求響應不再是返回整頁的html內容,而只是我們放在了UpdatePanel裏面的html內容,頁面也沒有再刷新,於是不禁感嘆一句:AJAX,So easy!媽媽再也不用擔心我的頁面了!
2.3 直到看見XmlHttpRequest才是唯一的答案—UpdatePanel原來如此
正當我們沈浸在UpdatePanel為我們提供的神奇的AJAX世界裏時,我們不禁對UpdatePanel為我們做了哪些工作產生了興趣。
(1)首先,我們知道AJAX的核心對象是XmlHttpRequest,那麽原生態的AJAX請求的JS方法是如何寫的呢?
function ajax(url, onsuccess) { var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(‘Microsoft.XMLHTTP‘); //創建XMLHTTP對象,考慮兼容性。XHR xmlhttp.open("POST", url, true); //“準備”向服務器的xx.ashx發出Post請求(GET可能會有緩存問題)。這裏還沒有發出請求 //AJAX是異步的,並不是等到服務器端返回才繼續執行 xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4) //readyState == 4 表示服務器返回完成數據了。之前可能會經歷2(請求已發送,正在處理中)、3(響應中已有部分數據可用了,但是服務器還沒有完成響應的生成) { if (xmlhttp.status == 200) //如果Http狀態碼為200則是成功 { onsuccess(xmlhttp.responseText); } else { alert("AJAX服務器返回錯誤!"); } } } //不要以為if (xmlhttp.readyState == 4) {在send之前執行!!!! xmlhttp.send(); //這時才開始發送請求。並不等於服務器端返回。請求發出去了,我不等!去監聽onreadystatechange吧! }
(2)其次,通過查看運行頁面的html,我們可以發現加入UpdatePanel後,我們的html中多了這麽幾個js引用。
(3)既然我們知道要發AJAX請求,必然會涉及到XmlHttpRequest。那麽,我們就在這幾個js中取看看是否有涉及到XmlHttpRequest。通過查看,我們找到了這樣一個似曾相識的js方法:
function Sys$Net$XMLHttpExecutor$executeRequest() { /// <summary locid="M:J#Sys.Net.XMLHttpExecutor.executeRequest" /> if (arguments.length !== 0) throw Error.parameterCount(); this._webRequest = this.get_webRequest(); if (this._started) { throw Error.invalidOperation(String.format(Sys.Res.cannotCallOnceStarted, ‘executeRequest‘)); } if (this._webRequest === null) { throw Error.invalidOperation(Sys.Res.nullWebRequest); } var body = this._webRequest.get_body(); var headers = this._webRequest.get_headers(); this._xmlHttpRequest = new XMLHttpRequest(); this._xmlHttpRequest.onreadystatechange = this._onReadyStateChange; var verb = this._webRequest.get_httpVerb(); this._xmlHttpRequest.open(verb, this._webRequest.getResolvedUrl(), true ); this._xmlHttpRequest.setRequestHeader("X-Requested-With", "XMLHttpRequest"); if (headers) { for (var header in headers) { var val = headers[header]; if (typeof(val) !== "function") this._xmlHttpRequest.setRequestHeader(header, val); } } if (verb.toLowerCase() === "post") { if ((headers === null) || !headers[‘Content-Type‘]) { this._xmlHttpRequest.setRequestHeader(‘Content-Type‘, ‘application/x-www-form-urlencoded; charset=utf-8‘); } if (!body) { body = ""; } } var timeout = this._webRequest.get_timeout(); if (timeout > 0) { this._timer = window.setTimeout(Function.createDelegate(this, this._onTimeout), timeout); } this._xmlHttpRequest.send(body); this._started = true; }
由以上的方法名我們可以猜到,此方法是一個執行AJAX請求的方法。在此方法中,創建了XmlHttpRequest對象,也使用了open方法指明以GET還是POST方法向服務器哪個處理程序發送請求,並且也為該請求指定了請求成功後需要執行的回調函數方法(onreadystatechange),最後調用send方法正式發送請求
由此,我們可以初步分析出一個結論:UpdatePanel本質還是幫我們封裝了以XmlHttpRequest為核心的一系列方法幫我們將CodeBehind中的同步事件變為了異步操作,並通過DOM更新指定的HTML內容,使得我們可以方便地實現AJAX效果。
但是,我們也不由發出感嘆:本來可以很簡單地使用XmlHttpRequest來實現的東西,為什麽使用UpdatePanel會引入這麽多js,並且為我們返回的東西還是那麽多(比如上面的例子,我只需要的數據是一個結果,卻給我返回一部分無用的html,還有一系列的hiddenId之類的數據)。在對性能要求較高的應用場合,如果使用UpdatePanel來實現AJAX會增加服務器的負載,並且會消耗掉不必要的網絡流量(比如每次請求都會來回都會發送ViewState裏的數據,在性能和數據量上都會造成損失)。園子裏的浪子曾經在他的博文《遠離UpdatePanel帶給我的噩夢》裏邊寫到:“UpdatePanel在頁面小的時候還是很好用的,而當頁面控件數不斷上升的時候,UpdatePanel就開始直線下降,我們現在頁面有4,5百個控件,每做一次PostBack需要長達15秒鐘之長,實在讓人無法忍受。”
那麽,有木有方式可以替換UpdatePanel呢?其實答案很簡單,那就是使用基於XmlHttpRequest的js方法,再加上一定的js回調函數即可。這就要求我們掌握javascript,不能只做拖UpdatePanel控件的程序員。現在基於js的JQuery庫也早已為我們封裝了XmlHttpRequest,提供了ajax開發的一系列方法供我們調用,相當於UpdatePanel的“重量級”來說,可謂是輕了不少,是一個“輕量級”的AJAX開發方式。通過借助jQuery Ajax+ashx可以方便地在.Net中進行Ajax開發,並且具有不錯的性能,這也是我實習所在的企業中經常用到的方式。
三、學習總結
本篇主要學習了WebForm中的狀態保持法寶—ViewState,以及曾經的ASP.Net AJAX方案的利器—UpdatePanel,雖然一直在說這個不好,那個別用。但是,微軟之所以為我們提供了這些東西,肯定有它存在的理由,並不一定都是不好的東西。所謂利器在手,沒有一點內功心法的人還是使用不好它,無法發揮出其100%的優勢。因此,身為.Net學習者的我們,不能滿足於微軟為我們所提供的便利,要知其然也知其所以然,做一個上進的程序員,加油吧!
ASP.Net WebForm溫故知新學習筆記:二、ViewState與UpdatePanel探秘