Web Application 三部曲:動態生成控制項技術
作者:朱明中
動態生成控制項(Dynamic Control Creation)
在 Web Client 的前端介面中,經常出現有連續且類似(或相同)的使用者介面,例如訂單商品輸入,客戶資料輸入等等表單,這些表單都有幾個特性:
- 介面控制項組合相同(或相似)。
- 數量不固定,或者是數量很大(通常數量大於 10 個就很多了)。
- 通常以 Master-Detail(主從式使用者介面)居多。
例如像是這樣的問題:
各位程式開發達人: 小弟最近遇到一個問題,就是要讓使用者自行新增控制項到網頁中,舉例來說我在網頁上方維護 VS 2005 server control list 讓使用者選擇,選定後在網頁下方 create 該控制項,並讓使用者設定屬性,我再將使用者設定存入資料庫,有沒有比較簡便的方式或 function 可以達成此目標呢?? Source:http://forums.microsoft.com/msdn-cht/ShowPost.aspx?PostID=729081&SiteID=14 |
這種需求通常會出現在類似問卷建立系統,購物車系統等等地方。
實作動態控制項必要的背景知識
若要在 Windows Application 中實作這種控制項相對來說比較簡單,因為 Windows Application 是一種具有狀態記錄的應用程式,在表單中經過 Form_Load 程式產生的控制項,都會被記錄住(有類別層級的區域變數),在後續的程式處理都可以使用。
然而,Web Application 是一個 State-less(無狀態)的應用程式,所有在Web應用程式中產生的控制項,在伺服器產生HTML回傳到 Web Client 後,伺服器就會丟棄這次的處理結果,包含變數與所耗用的資源,讓 Web Server 可以處理更多的用戶端要求的動作,這也表示,控制項的維護,要由 Web Application 來負責,而不是由 Web Server 負責。
所以 Web Application 的開發人員需要處理許多動態控制項的問題,而這些問題在處理上會有些複雜,諸如:
- 控制項建立(Control Creation),在頁面處理時,產生控制項。
- 控制項命名(Control Naming)與套用事件處理常式。
- 控制項資料存取(Control Access)。
- 控制項資料與狀態保存(Control State Persistent)。
A. 控制項建立
在每次提交 Request 到 Web Server 時,都需要在頁面載入時期,將控制項重新生成,讓 ASP.NET 知道有這個控制項存在,否則無法在資料載入時期將資料套用到控制項中(後面會提到)。
B. 控制項命名
控制項在生成之時,都需要設定控制項的 ID,通常在大量的控制項生成時,ID 數量會變得許多,在設計上通常以具規則性的方式來做,才可以在後續處理上方便許多,否則只會讓處理工作更複雜。
C. 控制項資料存取
ASP.NET 在執行完成頁面載入之後,就會開始資料設定,將資料設定給控制項,但若在控制項生成階段未產生控制項時,這個動作就會被忽略掉。
D. 控制項資料與狀態保存
在 ASP.NET 輸出資料之後,所有控制項的資料都會釋放掉,開發人員需要利用一些方法或機制來保存狀態與資料,狀態的控制在複雜的應用程式是個較不好處理的工作。
另外,在使用者介面之中,有一種特殊的控制項,可以收納其他的控制項在其中,就如同收納箱可以放許多小東西一樣,這種控制項稱為收納器或者容器(Container)控制項,在容器控制項中還可以放容器控制項,成為巢狀的控制項層級。容器控制項由容器類別組成,容器類別實作了 IContainer 介面,提供了可收納控制項或元件的基礎支援。
ASP.NET 的 Page 本身就是一個容器控制項,可以收納許多控制項,例如 Web Control、HTML Control、User Control 與 Custom Control。
使用 ASP.NET Page API 來處理控制項工作
在動態控制項處理的過程中,ASP.NET 提供了幾個重要的屬性與方法,讓開發人員在動態控制項處理上能夠更方便,雖然老實說沒有方便到哪裡去,基於容器控制項的特性,開發人員可以透過 Page 所提供的 API 來進行控制項的處理工作。
- 找尋控制項(Searching Control)
Page.FindControl() 允許開發人員以控制項 ID,搜尋在頁面中的控制項,但這個函式只會搜尋在 Page 中的控制項,若要找的控制項是放在 Page 裡面的容器控制項時,將不會被 Page.FindControl() 找到,此時將需要使用遞迴(Recursive)方法來搜尋在每個容器中的控制項。
要判斷是不是容器控制項也很簡單,只要判斷 Controls.Count 是否大於零就可以了。 - 列舉控制項(Enumerating Control)
列舉控制項是由根物件(或者指定的物件)向下,列出所有控制項的方法,這在要依順序來存取控制項時是很好用的方法,同時也可以知道目前控制項的分布狀況。 - 型別處理與控制項轉型(Type processing and casting for Control)
當使用 Page.FindControl() 與列舉控制項的時候,通常不會固定在某個控制項類別,因為每個網頁都會有許多控制項,而不是只有一種控制項,最常使用的就是 Control 類別,它是所有控制項的父類別,所有的控制項都要繼承它來實作。
在Page.FindControl() 找到控制項時,它會回傳一個 Control 物件,代表搜尋到的控制項,開發人員需要將它轉型成需要的類別才可以使用,否則就只能使用 Control 物件可用的方法與屬性,物件轉型的方法,C# 可以使用 as 運算子,Visual Basic 則可以使用 CType() 陳述式。// C# Control ctl = Page.FindControl(“TextBox1”); TextBox TextBox1 = ctl as TextBox; ‘ Visual Basic Dim ctl As Control = Page.FindControl(“TextBox1”) Dim TextBox1 As TextBox = CType(ctl, TextBox)
綜合前面所說明的,一個可以進行深度搜尋的程式碼列示如下:
// C# public Control DepthFindControl(string ControlID, Control BaseControl) { Control result = BaseControl.FindControl(ControlID); if (result == null) { foreach (Control ctl in Page.Controls) { if (ctl.Controls.Count > 0) result = DepthFindControl(ControlID, ctl); } } return result; } 'Visual Basic Public Function DepthFindControl(ControlID As String, BaseControl As Control) As Control Dim result As Control = BaseControl.FindControl(ControlID); If result Is Nothing Then For Each ctl As Control In Page.Controls If ctl.Controls.Count > 0 Then result = DepthFindControl(ControlID, ctl) End If Next ctl End If Return result End Function
一個可以進行深度列舉(列舉出所有的Web Control)的程式碼列示如下:
//C# public List EnumControls(List ctlContainer, Control BaseControl) { if (ctlContainer == null) ctlContainer = new List(); ctlContainer.Add(BaseControl); foreach (Control ctl in BaseControl.Controls) ctlContainer = EnumControls(ctlContainer, ctl); return ctlContainer; } 'Visual Basic Public Function EnumControls(ctlContainer As List(Of Control), BaseControl As Control) As List(Of Control) If ctlContainer Is Nothing Then ctlContainer = new List(Of Control)() End If ctlContainer.Add(BaseControl) For Each ctl As Control In BaseControl.Controls ctlContainer = EnumControls(ctlContainer, ctl) Next ctl Return ctlContainer; End Function
建立動態控制項
動態控制項建立其實很簡單,只要使用一個新增物件的指令就可以了:
// C# Button cmdNewButton = new Button(); // for Web Control Control ctl = Page.LoadControl(“UserCtl.ascx”); // for user control. ‘ Visual Basic Dim cmdNewButton As Button = New Button(); // for Web Control Dim ctl As Control = Page.LoadControl(“UserCtl.ascx”); // for user control.
不過困難總是隱藏在看起來很簡單的程式碼中,因為 Web-Based 應用程式的特性,如果這段程式碼是放在 Page.IsPostBack 判斷式中:
// C# if (!Page.IsPostBack) { Button cmdNewButton = new Button(); cmdNewButton.Text = “Dynamic Button”; cmdNewButton.Click += new EventHandler(this.Dynamic_Click); // PageContent是一個PlaceHolder控制項 this.PageContent.Controls.Add(cmdNewButton); }
當你按下這個按鈕後,可能就會大吃一驚,因為按鈕不見了,連按鈕的事件常式都沒有被呼叫到:
但我們如果改成:
// C# Button cmdNewButton = new Button(); cmdNewButton.Text = “Dynamic Button”; cmdNewButton.Click += new EventHandler(this.Dynamic_Click); // PageContent是一個PlaceHolder控制項 this.PageContent.Controls.Add(cmdNewButton);
就可以順利執行了:
你也許會覺得很奇怪,明明已經建立了控制項,為什麼會不見?其實,這就是 Web-Based 應用程式的特性,這在首篇(首部曲)文章中就已經解釋過,Web-Based 不會主動幫你記住任何狀態,不管是資料,控制項或是使用者狀態等等,這是開發人員的工作。也就是說,在每一次 PostBack 時,都要重新將控制項建立起來,ASP.NET 才會處理由用戶端回傳的指令,也才會執行到控制項所繫結的事件處理常式,否則指令會被捨棄掉。
那麼,要把控制項生成放在哪一個事件常式呢?網路上有一些說法,說要放在 Page_Init,但也有說要放在 Page_Load 事件常式,筆者說:都可以,理由待我說來。
ASP.NET 的 Page 事件順序,由初始化到完成,其順序可由下圖組成:
為什麼我會說都可以呢?因為一般控制項通常都只會使用 Load 事件,來讓 ASP.NET Page 載入控制項,而很少使用 Init 事件來初始化,對像 Button 這樣的 Web 控制項來說,不太需要使用 Init 事件,一般的使用者控制項,也不見得會用到 Init 事件,對於這種控制項來說,不需要一定要在 Page_Init 事件來建立控制項,在 Page_Load 也可以,所以前面的範例碼可以用。
然而,這有個例外,若控制項有使用到 Init 事件時,就需要把它放到 Page_Init 來呼叫,所以在MSDN上的一篇技術文章: "Creating Dynamic Data Entry User Interfaces" 中,建議將動態控制項生成的程式放在 Page_Init 中,但筆者認為不一定,看應用程式使用的控制項為何來決定。
動態控制項設計的考量
隨著使用者介面的複雜度提升,若頁面中都充斥著動態生成的控制項,會讓程式設計的複雜度提高,在狀態維護的難度上也會提高,所以適當的使用動態控制項,才是最重要的觀念。任何使用者介面的創新或發展,都會有其瓶頸之處,如何在不造成瓶頸前善用控制項,這考驗著系統設計與開發人員的智慧,尤其是 Web 應用程式和 Windows 應用程式之間本質上的不同。
本文提筆至此,下回將進一步提出一些簡單的動態控制項生成的技巧,分成用戶端與伺服器端的簡單應用。