asp.net
前言
ASP.NET的優點我說過很多次了,也就是各個控制項獨立負責自己內部的邏輯,這是一個好事情,因為它解決了原本ASP處理邏輯耦合度高的問題。然而這是需要代價的,那就是引入ASP.NET頁面生命週期,隨著控制項的多層嵌套,應用的複雜度增加,我們再次陷入泥潭!
問題
其實這個文章題目我兩個月前就寫下了,可是一直沒想寫完它,直到今天我在這個泥潭中泡了幾個小時,於是決定先從泥潭中跳出來把文章寫完,再跳進去繼續解決問題。問題是這樣的:
使用MS AJAX 1.0 Beta2 + 2.0 CTP建立一個項目,同時在Bin中放上Beta2的AjaxControlToolkit.dll。
扔上一個Accordion,放置幾個AccordionPane,設定一下CssClass。
在Page_Load中使用Page.LoadControl載入一個UserControl,然後添加到頁面上。
接著發現UserControl內的控制項無法正常觸發事件,陷入泥潭中……
首先要說明,如果僅僅做第3步那個UserControl肯定正常運作,那意味著問題出在ScriptManager或Accordion中出現了問題。
本文
想知道到底是什麼出問題了嗎?先聽我說說這個ASP.NET頁面生命週期的問題吧。
由於生命週期按階段劃分,任務在不同階段按部就班完成,所以我們的每一個操作都是階段相關的,有些操作僅能在特定的階段操作,有些操作在不同階段執行會導致不同的結果。當然,MS希望盡量消除這些階段間的差異,例如讓一個操作在儘可能多的階段中都能執行,並且儘可能減少在不同階段中操作引發的不同結果。然而這不可能完全做到,例如我們都知道ViewState讀寫限制為僅能在某些階段進行,於是依賴於ViewState的控制項屬性也就因此受到同樣的限制。
控制項屬性讀寫受階段限制,這很好接受,對吧?因為這僅僅是一層依賴關係。順著依賴關係推廣出去,情況會變得越來越複雜,限制的原因埋藏得越來越底層,接著我們發現複雜性這一問題在ASP.NET這種結構良好的體系中出現了,而消滅這種複雜性的銀彈還沒被發明。
作為控制項或組件的開發人員,我們當然有義務消除階段差異,讓下遊的開發人員面對更低的複雜性,而且我們也確實儘力去做了。控制項的每一層封裝,都包含著這種努力,並向上承諾儘可能低的階段差異。然而為了讓控制項看起來簡單易用,我們不可能將這些差異完整地記錄在文檔之中,我們嘗試去隱瞞細節,控制項被層層封裝時我們都這樣做。底層文檔沒告訴我的差異,我當然也沒必要寫到這一層的文檔上去;底層文檔提及了的差異,我儘力彌補了,即使彌補得不太好,也不寫到這一層的文檔上去。於是文檔就好像神話傳說一樣隨著世代相傳而改變,最終沒有人知道這個控制項依賴於某些底層的階段差異。
做過控制項開發的人都知道,有時候我們必鬚根據實際情況採用不同的方式構建看起來一樣的控制項。例如最簡單的資料控制項都會存在是否PostBack的構建差異,如果是非PostBack,則需要在DataBind時構建並將資料儲存到ViewState,如果是PostBack則根據ViewState直接構建,如果PostBack後又遇到了DataBind則需要清除原來的構建並重新根據新資料構建。再複雜一些的控制項,還會分步驟構建,預設情況下為了消除使用方的階段差異,部分構建步驟會儘可能靠前到Init時執行,而另外一部分構建步驟則儘可能延遲到PreRender時執行,中間部分則儘可能減少自己的變化以便使用方操作。然而事情不會那麼簡單,使用方的某些操作(通常是訪問某個屬性)如果依賴於某個構建步驟的完成,因此一旦這些操作出現,原本在PreRender才執行的特定構建步驟就要提前執行,當這樣的操作在不同階段進行多幾次,構建步驟就已經散落在頁面生命週期的各階段。
構建步驟可能散落於頁面生命週期的各階段對於控制項設計師來說是一個嚴峻的問題,這意味著他要保證任何一個構建步驟在任何一個階段執行都是無差異的,當然這不可能做到,於是又要引入別的機制來減少這種差異,複雜性就此產生了,接下來隨著複雜性的增加控制項設計師越來越無法確保較低的階段差異程度,這就到控制項使用者遭殃了,如果控制項使用者又再把控制項封裝,並且依然企圖降低階段差異程度,那麼災難也就發生了……
結果
我花了幾個小時在泥潭中泡了幾個小時,邊泡邊寫這篇文章,問題當然已經有結果了。
如果Accordion設定了HeaderCssClass或者ContentCssClass,那就會出問題,但如果為AccordionPane都加上以上兩個屬性,又不會有問題了。這樣的情況當然通過用Reflector查看這兩個類的代碼來解決,結果發現Accordion會檢測每一個AccordionPane是否有設定這兩個屬性,如果沒有就把AccordionPane的設定為和自己的一樣。在AccordionPane被設定時,會調用this.EnsureChildControls(),這是一個會導致構建步驟提前執行的方法,於是控制項構建的順序就改變了,不僅僅Accordion內部的順序改變了,整個Page的都改變了。由於控制項的ID是按順序自動分配的,包括我那個UserControl,構建順序的改變意味著ID的改變,也就相當於整個控制項樹都改變了,事件當然不能正常觸發。
最後的解決方案當然是為我那個UserControl指定ID。我花了那麼多個小時才發現自己做了件蠢事,一早開啟Trace來看控制項樹就應該能發覺UniqueID的變化。
總結
雖然這個問題看起來不是一個太好的例子,因為一開啟Trace就應該能找到問題的來源,但實際上它卻正好揭示了ASP.NET架構內部的“蝴蝶效應(Butterfly Effect)”——隨著複雜度的增加,任何一個細微的改變都會導致全域上的巨大變化。在設計ASP.NET的時候,MS可能也在想著解耦,在簡單的情況下這東西確實也解耦,然而在複雜的情況下卻正好背道而馳,這真的是很諷刺。