動態加載控件貌似給很多程序員都帶來了困擾,經常收到這樣的郵件,干脆就寫下面這個示 例來演示如何解決那些常見的問題吧。
其實常見的問題通常有這樣兩個:
1. 通常他們都通過一個按鈕來添加一個UserControl 並將它們加入PlaceHolder 容器的 Controls 中。然後頁面上就會有一個另外一個按鈕,這個按鈕什麼相關的事也沒做,就是做了 一次回發。這樣的情況動態添加的控件就不翼而飛了。
2. 今天收到了一封郵件說是要追加控件,和上面的情況看上去好像不一樣,但實質就是同 一回事。
原因:
其實網上有很多帖子都不約而同地解釋了這個問題,這裡我還是不厭其煩地解釋一下:
首先,要提到大家所熟知很多人一知半解的頁面生命周期,以至於很多居然還停留在將 ASP.NET 和Winform 一樣處理的層次上,因此就會有人試圖將變量存在實例字段中,然後一如 既往地指望它能夠用來共享數據,結果總是無功而返,以我所知這樣的人居然還不在少數,當 然了,咱博客園的素質相對偏高,這種問題一般不在話下。事實上每次頁面PostBack 都會從 Aspnet 線程池中返回一個空閒的用戶線程,用於處理用戶本次的請求。擺弄一下那種浏覽器進 度條會動的控件基本也都算是回發事件了。兩次回發之間可以當作沒有什麼關聯的。但是你總 能看到很多控件等在回發之後還能保持狀態比如文本框邊上有個按鈕。你填寫完了文本後狂點 那個按鈕,你會發現文本框中的文字還是你填寫的那些而不會被清空。這就不得不說到 ViewState 這種神奇的雙刃劍了。它的原理在MSDN 上講的很清楚,找不到的留言或發郵件給我 我再慢慢給你找……
然後呢?還是查MSDN, 關鍵字“TemplateControl.LoadControl ” 我們在用PlaceHolder 中動態添加控件的時候就會用到這個方法了。我們注意到這裡有一句:“ 在將控件加載到容器 控件時,該容器引發所添加控件的所有事件,直到所添加控件參與當前事件為止。但是,所添 加控件不參與回發數據處理。” 因為所添加的控件是不參與回發數據處理的,因此就會出現問 題1 中所遇到的按另一個按鈕就消失的現象了。問題2 其實也是一樣的問題,因為事實上它們 遇到的現象是一樣的,只不過它的需求有所不同罷了。(可以理解成一個是i=1; 另一個是 i+=1; )
綜上所述,問題的關鍵就是原本在頁面加載的時候所有的控件初始化操作都應該完成,動態 加載將加載的過程延遲到了事件被觸發之後,因此在頁面回發後,因為會有一次新的頁面加載 過程,顯然這時候動態加載的控件是不存在的,但是用戶預期的答案是顯示已經加載的信息。 這時候如果可能我們最好在加載的過程中進行控件的重新加載和數據綁定。常見的方法中我們 呢通常通過LoadControl 來動態加載控件,因此只要在頁面輸出之前的所有事件節點上我們都 可以加載我們的控件。但是推薦的則是Init 事件。在Load 事件的時候進行數據綁定。
解決:
既然問題的原因找到了,我們就應該解決它,現在關鍵就是在回發後PlaceHolder.Controls 的子集數量為0 ,也就是沒有子控件,也就是很明顯地控件跑沒了。那麼我們就應該在我們在 他們還在的時候將其存放起來。在經典的回發模型中,ViewState 通過將所有控件/ 其子控件 的各個屬性字段等都存放到ViewState 中了,在最後Render 的時候都一並丟給了用戶。數據包 括數據狀態都一並發到了客戶端,現在客戶點擊了一個能夠引起回發的按鈕或者下拉框按鈕, 所有這些數據狀態以及客戶修改(也許沒有修改,但我們假定客戶篡改過了)的數據都傳回客 戶端。因為回發發生了,因此在加載數據的階段IPostBackEventHandler 和 IPostBackDataHandler 接口所定義的方法(通常由服務器控件實現)都將被調用,然後就是一 系列的數據回填工作。用戶的數據又被重新做成了新的ViewState 放在頁面裡面又丟給了客戶 端。我曾經用一個比喻(相當拙劣的比喻,當時好像不是這樣比喻的)是白襯衫(花花公子正 版)被藍筆畫後,送去洗衣店,人家新拿了一件一樣的白襯衫(花花公子高仿),然後用藍筆 劃了一下還給你,事實上白襯衫不是你原來的那件了,但看上去還是無法分辨。因此我們這裡 也可以用類似的辦法來解決。但是真的可以嗎?用ViewState 不僅有眾所周知的性能問題,因 為ViewState 的存儲介質(其實是指它的內容存儲,可以理解成持久層)是頁面,而頁面是指 接受文本的一種載體(正如網頁事實上都是文本一樣的道理)因此會有序列化的問題。這就給 用戶控件的開發帶來了極大的不便。更關鍵的原因是不僅如此,因為UserControl 壓根沒有支 持序列化,因此你的控件即使精簡到沒有字段方法(就聲明了個名字夠精簡了吧)再加上序列 化特性,只要你繼承自UserControl ,就必然面臨無法序列化的尴尬。況且它的性能問題確實 也很值得關注。和ViewState 有類似性質的常見的還有Session 和HttpContext.Current.Cache 等緩存,或者自己實現一個靜態字典用於存儲也是一個不錯的選擇。用它們是可以解決問題的 ,在下面的代碼中將會用到。但這樣的方案事實上是存在很多問題的。大家都知道Session 是 有超時時間的,默認長度也就是幾十分鐘,而且Session 也有諸多其他方面的限制,因此用它 來做容量如此之大的控件存儲其實是非常不適合的。HttpContext.Current.Cache 是一個高級 的緩存對象,因為有完善的內部機制來限制其膨脹以及管理其內容,但也正因為這種管理比如 大小限制等原因會導致在生產環境中可能會遭遇嚴重的性能問題。緩存應該用來存取較小的常 用的數據,比如用戶名/ 密碼這樣的常用數據,而不是這種大個頭的東西。但是與ViewState 相似的性質讓它們有了承擔這份責任的義務。(家裡的大人都死光了,孩子也只好來當家了) 這讓我們想到了存儲介質,事實上磁盤文件,數據庫等都具有了同樣的性質。另一條思路是來 自簡單地加載思路,因為對動態添加的控件來說,它有一個很明顯的特征,它是動態添加的。 因此既然可以在按鈕事件處理程序中添加,同樣也就可以在頁面初始化事件處理程序中添加。 按照頁面的生命周期動態添加最好寫在Init 這時候理應做豐富的添加(不過不適合那種需要用 按鈕添加的用戶需求了)[ 另外一點有點郁悶的是在MSDN 中也是說應該在Init 而不是Load 中 動態添加,但是同樣是在MSDN 的《如何:以編程方式創建ASP.NET 用戶控件的實例》居然就用 了Load 事件來處理,因此這種區分對頁面開發人員事實上並不是那麼嚴謹的,事實上也不會出 現什麼問題,因此也就沒有人吹毛求疵了,而且Google 出來的答案估計90% 以上都是在Load 中寫的,一傳十十傳百的結果可能這個數值還在上升,所以就更沒必要計較了] 。剛剛打算幫 發郵件的兄弟直接找一個答案發現了有網友說在每個頁面都要做判斷搞加載,很煩很煩…… 所 以如果您的需求不是那種追求打開一個頁面兩天後再來點一下要追加或則重新加載控件的朋友 ,我的方案還是可以考慮的。當然如果你比較追求那種近乎變態的需求或者您的頁面和淘寶有 一樣大的訪問量的話,不凡試試我的方案,更好的解釋是,我的方案可以當作理解控件動態加 載原理解釋的一個入口罷了。
我的例子,因為代碼比較多,我就貼出如何調用的部分(也就是“ 如何用” 的代碼)源碼 可以在後面的鏈接中下載。
擴展性:雖然是為我那位郵友給出的答案,但是還是考慮了擴展性,我們可以嘗試擴展用磁 盤文件、網絡、或者數據庫的方式來作為存儲介質,當然,您必須為此實現部分接口。局限性 ,因為有存儲介質一說,因此不同容器托管方面不允許同時使用多種存儲介質,否則將會出現 兩個集合,因此就帶來了另一個擴展性,您可以自行實現擴展存儲之間的數據同步,不過做此 之前提醒您一下,不同的存儲介質可能存在不同的存儲能力,比如Session 有大小限制,而數 據庫簡直就是容量大王,這些數據之間的同步可能會引發新的問題,另者就是這樣的同步除了 看上去很酷之外並沒有什麼好處,將數據亂存的結果可能導致程序顯得混亂,更尴尬的是數據 同步所白白消耗掉的性能。當然如果您只是練練手的話您確實可以這麼做,做完記得告訴我一 下,哈哈,我也想不勞而獲。哈哈。下面貼一下代碼就不多做解釋了,因為如果你理解了上面 這些,看懂那些代碼就不可能有問題了。
public partial class _Default : System.Web.UI.Page
{
public ContainerManager.ContainerManager cm = new ContainerManager.ContainerManager();
protected void Page_Load(object sender, EventArgs e)
{
//重載控件(HttpContext.Current.Cache作為存儲介質)
cm.ReloadControls(HttpContext.Current.Cache, "PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls);
}
protected void btnInsertDynamicUserControl_Click(object sender, EventArgs e)
{
//Control c1 = LoadControl("DynamicUserControl.ascx");
//PlaceHolder_DynamicUserControlContainer.Controls.Add (c1);
int displayCount;
int.TryParse(txtNumber.Text, out displayCount);
if (displayCount == 0)
{
//追加控件(Session作為存儲介質)
Control c1 = LoadControl("DynamicUserControl.ascx");
cm.AppendControl(this.Session, "PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls, c1);
}
else if (displayCount == 1)
{
//追加控件(HttpContext.Current.Cache作為存儲介質)
Control c1 = LoadControl("DynamicUserControl.ascx");
Control c2 = LoadControl("WebUserControl.ascx");
cm.AppendControl(HttpContext.Current.Cache, "PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls, c1);
cm.AppendControl(HttpContext.Current.Cache, "PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls, c2);
}
else
{
//常見的動態加載控件後點擊其他回發事件就導致控件丟失
PlaceHolder_DynamicUserControlContainer.Controls.Clear();
Control c1 = LoadControl("DynamicUserControl.ascx");
PlaceHolder_DynamicUserControlContainer.Controls.Add(c1);
cm.CacheControls(HttpContext.Current.Cache, "PlaceHolder_DynamicUserControlContainer", PlaceHolder_DynamicUserControlContainer.Controls);
}
}
protected void btnUnloadStorage_Click(object sender, EventArgs e)
{
cm.Remove(HttpContext.Current.Cache, "PlaceHolder_DynamicUserControlContainer");
}
}