深入理解 ASP.NET 動態控件

(Part 1 - 感性認識)  

正如我在《我喜歡的教材與我討厭的教材》中所說的,我討厭那種標題之後直入理論部分並開始寫“定理1、定理2、定理3”的做法,所以在我自己的文章也絕對不會這樣寫。我認爲感性認識是理性認識不可缺乏的基礎條件,所以在很理論性的解釋ASP.NET頁面生命週期之前,先通過一些大家可能都遇到過的例子給大家一個感性認識。

動態控件遇到的第一類問題就是跨頁面生命週期時無法自動保存,你必須每次手動創建。舉個簡單的例子,例如現在我有一個DropDownList,有三個ListItem,值分別是"0", "1", "2",在我設置了AutoPostBack之後,我希望SelectedIndexChanged時根據我選擇的ListItem數值動態創建相應數量的TextBox,簡單的代碼如下:
protected void dropDownList_SelectedIndexChanged(object sender, EventArgs e)
{
  for (int i = 0; i < dropDownList.SelectedIndex; i++)
  {
    TextBox dynamicTextBox = new TextBox();
    this.Form.Controls.Add(dynamicTextBox);
  }
}
需要解釋一下的是,直接用dropDownList.SelectedIndex是爲了省事,因爲ListItem的值本身也就是從0開始的順序整數。

測試一下我們這個小小的ASP.NET程序有沒有問題,結果當然是沒問題的,你選擇了哪個數值就真的會有相應數量的TextBox出現,好簡單哦!我們再扔一個Button到頁面上看看又會怎樣,這時候你就會發現如果通過點擊Button導致PostBack,那麼動態創建的TextBox就沒掉了,看起來事情並不如我們期望的那麼簡單。

“我們已經知道這個問題啦,快點給出解決方案啦”——如果你急需要一個解決方案,請直接看本篇文章的最後幾段。我知道很多人是因爲當前有一個棘手的問題纔來翻看這類文章的,但我也不能因此而忽視了另外一部分人的需求——他們希望由淺入深地瞭解這個問題,並且得到解決方案的同時得到完整解釋。

接下來我們繼續來看第二類問題,動態創建控件的事件觸發不正常。我們又來寫一段簡單代碼:
protected void Page_Load(object sender, EventArgs e)
{
  TextBox dynamicTextBox = new TestingTextBox();
  dynamicTextBox.ID = "DynamicTextBox"
  dynamicTextBox.Text = "InitData"
  dynamicTextBox.TextChanged += new EventHandler(dynamicTextBox_TextChanged);
  this.Form.Controls.Add(dynamicTextBox);
}
void dynamicTextBox_TextChanged(object sender, EventArgs e)
{
  this.Trace.Write("DynamicTextBox", "TextChanged");
}
由於用到了Trace,測試的時候別忘記把Trace打開哦。

我們再扔一個LinkButton到頁面上,目的僅僅是爲了觸發PostBack,然後看看事件是否正常。奇怪的事情發生了,在修改TextBox的值之前,無論怎麼點那個LinkButton,一切都非常正常,TextChanged事件確實不發生。修改了TextBox的值之後點LinkButton,事情也還正常,TextChanged事件發生了。但之後就出問題了,無論你是否修改了TextBox的值,TextChanged總是在每一次PostBack時都被觸發。

這個問題很怪異對嗎?事件既非完全不觸發,也非總是觸發。其實答案隱藏在我之前那篇《深入理解 ViewState》裏面,去讀一讀那篇文章,或許你自己也能夠解釋爲什麼會這樣。

動態創建的控件或許還存在第三類、第四類問題,在此就不一一列舉了。我相信被動態控件問題困擾過的ASP.NET程序員絕對不少,而未遇到過此類問題的程序員看到上述兩個問題也未必能給出解決方案和正確解釋。

在提供問題的解決方案之前首先要說明一點,作爲ASP.NET程序員的你需要在某一時刻某一地方讓控件動態出現時,就立即在該處寫代碼動態創建並添加控件,這往往都是錯誤的做法。正確的做法是向後退三步再擡頭看,這時候你看到的就不是你要讓控件動態出現的那一個準確的時刻和地方,你應該看到ASP.NET頁面生命週期的全貌,接着你就應該清楚你的代碼該加去哪裏了。

好了,是時候給出最直接的解決方案了,唯一的解決方案就是讓你看清楚ASP.NET頁面生命週期的全貌,而其中最佳的入門方式就是學習控件設計。雖然上面把動態控件說成一個複雜的問題,然而大家天天都在用動態控件,只不過動態控件已經被封裝到一個靜態控件裏了。例如複雜的GridView控件,它會自動根據每一列的性質來生成對應控件,如果是模板列還要分析模板中的內容來生成模板中定義的控件,這些控件都算是動態控件,爲什麼PostBack不會讓他們自動消失,爲什麼爲它們添加的事件從來不會錯誤觸發,在你學習完控件設計之後就會一清二楚。

關於控件設計,我推薦大家買Wrox(樂思)的書來看,是以控件設計爲主題的那兩本,不會很厚,很快能看完。如果你在使用的是ASP.NET 1.x,或者你一定要看中文版的書,那麼ASP.NET服務器控件高級編程將是一本很適合你的書。至於ASP.NET 2.0的則有Professional ASP.NET 2.0 Server Control and Component Development,英文版今年8月才發佈,根據清華出版社的慣例至少要等半年纔可能有對應中文版。

既然連解決方案都給出了,這個系列的文章繼續寫下去還有什麼意義嗎?書上能給你的只是一個臨摹着去做就不會出錯的模式,以及一個聽起來很合理的解釋。到底爲什麼臨摹這種模式去做就符合ASP.NET的大模式(主要是編譯模型和頁面生命週期),ASP.NET的大模式到底是怎樣的,這就是我接下來要寫的東西。

Part 2 - ASP.NET 支持

在上一篇中,我們知道了HTTP屬性與客戶端緩存的關係,現在就可以着手用ASP.NET來控制這種緩存。需要注意的是,ASP.NET的Cache是用於服務器端緩存的,所以和我們正在討論的事情完全無關,我們在這裏要討論的是如何通過HTTP屬性控制客戶端緩存。

頁面緩存

在ASP.NET中,如果你需要添加HTTP屬性,可以使用HttpResponse.AppendHeader方法,例如在Page的代碼中直接執行Response.AppendHeader。HttpResponse.AddHeader方法是與之等效的,不過僅用於與ASP代碼兼容,所以我的建議你最好不要使用。通過AppendHeader方法,你可以將上述Last-Modified屬性和ETag屬性寫入返回中。

接着我們考慮如何從請求中讀上述屬性然後判斷如何返回。我們可以使用HttpRequest.ServerVariables讀取請求中的屬性,然後和當前的值比較,如果比較結果表明內容無變化,我們就可以設置HttpResponse.StatusCode爲304,然後返回空內容;如果比較結果表明內容變化了,那就還是按一般的方式完成整個返回。

這很麻煩,對吧?所以ASP.NET內置了HttpCachePolicy類,讓我們可以直接控制有關屬性,我們可以通過HttpResponse.Cache訪問此類的實例,而如果在Page中我們可以直接通過Reponse.Cache訪問它。這個類的使用方式在MSDN中有詳細的描述,所以我就不再解釋了。由於它的實現也依靠上述HTTP屬性,所以使用AppendHeader控制上述屬性時,就會破壞掉HttpCachePolicy中的設置(如果你設置了的話)。因此直接使用AppendHeader與通過HttpCachePolicy間接控制這兩個方法中,同一時間最好僅用其中的一個,如果你需要靈活性就使用前者,如果你需要簡單設置就是用後者。

資源緩存

ASP.NET內置了Cache和HttpCachePolicy,這讓Page的緩存已經足夠方便,所以讓我們來看一看非Page該怎麼緩存。事實上資源文件(例如js和css)的最大可能請求數量比Page要多得多,因爲一個Page通常鏈接幾個資源文件。

編譯嵌入資源

我們先來看看編譯控件是如何緩存資源的。系統自帶的很多控件都是帶有資源的,因爲他們需要這些小圖片、腳本或樣式來確保它們的正常運行,這些資源編譯時選擇爲嵌入到dll中,之後無論控件發佈到哪都會附帶有這些資源。這些嵌入到dll中的資源以特定的形式引用,在控件呈現爲HTML代碼時就成了WebResource.axd開頭鏈接,例如:
<script
  src="/WebResource.axd?d=7wVzVzBOs3_HEjhM5umRSQ2&amp;t=632962899860156250"
  type="text/javascript">
</script>
WebResource.axd註冊爲由AssemblyResourceLoader處理,這個IHttpHandler專門負責從dll中將資源文件提取出來,然後返回給客戶端。

留意WebResource.axd後面的兩個參數,d是資源的標示,它表明了當前請求的是哪個資源;t是該dll最後編譯的時間戳,如果dll重新編譯了t就會跟着改變,這就讓瀏覽器知道這是一個新的URL,不應該再使用原來的緩存。

需要強調的是,這並非是一個具有兼容性的做法,它只能確保資源更新時緩存過期,但不能確保沒更新的資源成功緩存。根據RFC2616,瀏覽器操作分爲安全與不安全兩類,GET和HEAD應該是安全的,因爲除了獲取信息它們不對外界造成任何影響;POST、PUT以及DELETE是不安全的,因爲它們對外界造成影響,所以你刷新POST後的頁面時瀏覽器會提示你是否確認再次提交數據。RFC2616中提到,對於安全操作除非服務器端顯式聲明過期,否則客戶端有權直接取緩存來顯示,因爲無論客戶端是從服務期端取還是從緩存取都應該是不對外界造成任何影響的,然而有一種情況除外——就是當URL中存在QueryString時。

當URL中存在QueryString時,這個請求被認爲是可能對外界造成影響的,所以當客戶端進行這個請求時必須通過服務器端完成,也就是不允許使用緩存。RFC2616如是說了,但並非每一個瀏覽器都如此做了。IE和Firefox違反RFC2616對有QueryString的URL進行緩存,而Opera和Safari則遵守此規矩每次重新獲取內容。也就是說,ASP.NET的這種資源地址在Opera和Safari中是決不會被緩存的,例如你的ASP.NET應用在MasterPage使用了ASP.NET AJAX的ScriptManager,那麼打開每個頁面時有關的腳本文件都要從新下載。

非編譯嵌入資源

如果我們當前在寫一個ASP.NET網站,有些資源是直接以文件形式存在的,不是編譯嵌入到dll中的,那麼我們就沒辦法享受上述系統提供的便利了,但我們可以自己實現類似的機制,並避免上述某些瀏覽器不緩存資源的問題。詳細的實現方式將在本系列文章的下一篇中討論,如果你不想錯過其中的精彩內容,請訂閱Cat in dotNET

(Part 3 - 頁面生命週期)

前言

在上一篇文章中,承諾了這一篇開始講解釋器的,不過看來要按着一個大框架來寫文章不那麼容易,沒仔細推研究過就寫出來的內容似乎很應付式。所以我決定恢復我原來的寫作習慣,我覺得哪部分的內容已經成熟了,那就把它release出來,沒成熟的就繼續留在我的draft裏面。這次要講的是頁面生命週期,動態控件對此關注的當然是動態與靜態控件在生命週期中加載的差別。

一般加載

雖然一般加載過程已經被說過很多次了,但我在這裏還要說,希望能把每一個階段的特點描繪出來,讓大家加深印象。

一般加載分爲以下幾個主要階段(粗體標出的階段的特殊性後面解釋):

  1. Init - 初始化,是否爲動態控件就以此爲分界,Init之前加入到控件樹的控件其處理過程就和ASPX中靜態聲明的一致,因爲靜態控件也就是在Init前加入的。
  2. LoadViewState - 加載ViewState。
  3. ProcessPostData - 處理PostData,倒不如說是加載PostData,因爲此階段控件多數僅加載PostData,順便判斷PostData是否有改變,別的處理不在此階段作。
  4. Load - 加載,讓ASP.NET程序員盡情發揮創意的地方,包括如何糟蹋ASP.NET這個框架。
  5. ProcessPostData Second Try - 第二次嘗試處理PostData,和第一次所做的一樣,不過第一次執行時已在控件樹上的控件不會受到第二次打擾。
  6. Raise ChangedEvents - 冒泡Changed類事件,這裏指的是由於PostData變更而引起的Changed類事件。
  7. Raise PostBackEvent - 冒泡PostBack類事件,除了Changed類以外的所有事件都在這裏引發。
  8. PreRender - 預呈現,這名字不怎麼好記,改爲“末日審判”或許會好一些,因爲作爲上帝的程序員在這裏判決每一個變量的最終值。
  9. SaveViewState - 保存ViewState,判決執行的階段,變量最終值在此保存,判入地獄的變量無權進入ViewState這個天堂並從此消失。
  10. Render - 呈現,可能是生命週期中最無法解耦的一個階段。
  11. Unload - 卸載,有加載自然有卸載,但其實沒有多少人知道它的存在。

這11個主要階段可以簡單分爲3大步驟:

  1. 加載數據:LoadViewState, ProcessPostData, ProcessPostData Second Try
  2. 處理數據:Raise ChangedEvents, Raise PostBackEvent
  3. 保存數據:SaveViewState

這3大步驟構成了ASP.NET頁面處理體系,其中第2步的處理數據是基於事件冒泡的形式,也正是ASP.NET比ASP先進的地方。ASP.NET把是否處理以及如何處理分離開來了:控件內部的邏輯決定是否處理,如果要處理就觸發事件;控件外部的邏輯決定如何處理,僅當事件觸發時纔會被執行。

追趕加載

與其說動態加載,不如說追趕加載,因爲動態加載的過程包含追趕加載,這是和靜態加載的主要區別。每一個控件內部都保存着它當前的加載進度,也就是它到達了上述的哪一個階段,當我們執行Control.Controls.Add方法來將一個控件添加到另一個控件中時,父控件就會檢查子控件的加載進度,如果子控件的加載進度比自己的慢了,就會要求子控件追趕上來,所以叫做追趕加載。

在上面11個主要階段中,用粗體標出的階段就是追趕加載時必須補回執行的階段,而其他則是追趕加載時錯過了就忽略的階段。正是由於有一些階段不被包括在追趕加載中,所以如果我們的控件要使用到這些階段,就必須保證在這些階段之前加載。也就是說,如果控件要處理PostData,包括加載PostData及根據PostData觸發事件,則必須趕上ProcessPostData Second Try,這意味着它必須在Load的時候加載。否則一旦錯過ProcessPostData Second Try,一個控件將在PostBack中表現得和非PostBack時一樣,完全不知道有PostData這回事。

結論

其實結論已經說了,在此再強調一遍:如果你的控件要能成功觸發事件,必須在Load階段加載,如果在Load階段之後(例如另一個控件的事件中)加載,那麼此控件的事件無法正常觸發。

問題與實驗

先解答上次的問題與實驗:

  1. this.Page.Controls.Add(this.Page.LoadControl("~/MyUserControl.ascx"));是正確的做法。ASCX與ASPX的編譯方式是類似的,MyUserControl類只是一箇中間過程,僅包含C#代碼的編譯結果,不包含ASCX的邏輯。而使用Page.LoadControl方法獲得的類纔是一個UserControl的最終編譯結果,包含了ASCX的邏輯。
  2. 這個實驗我自己也沒去做過,有興趣的朋友可以自己做一下看看結果如何。

然後是本次的問題與實驗。

  1. 如果要求頁面上有一個Button,點擊後出現一個CheckBox,要這個CheckBox能夠正常觸發CheckChanged事件,應該怎麼做?注意,不要使用隱藏控件的方法,因爲隱藏控件所生成的HTML和ViewState是要佔用空間的,我希望這個CheckBox在Button被點擊之後纔在頁面生命週期裏出現。
  2. 爲什麼ICallbackHandler在Beta2中僅有RaiseCallbackEvent一個事件,而到了正式版中被拆分爲RaiseCallbackEvent和GetCallbackResult兩個事件?(提示:這和頁面生命週期的階段劃分有關)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章