1. 引言
在前幾次任務裡我們開發一個星級控制項並逐漸為其增加一新的特性,在本次任務裡,我們將開發一個較複雜的自訂控制項,該自訂控制項需要實現摺疊面板的功能。使用者可以向面板控制項中自由添加控制項,該控制項呈現出來後會根據使用者佈建決定是否顯示摺疊按鈕,如果允許則使用者可以點擊按鈕展開/摺疊按鈕以顯示或隱藏面板,並且可以在伺服器端捕捉到展開/摺疊事件以進行更多的控制,該控制項運行如下:
2. 分析
我們在確定該控制項最終能夠使用HTML呈現出來之後,接下來要考慮的是為該自訂控制項選擇一個合適的基類。從要實現的功能來看,該控制項分為兩部分,一部分是包含展開/摺疊按鈕的標題列,另一部分是包含使用者放置按鈕的容器,很顯然這個容器我們可以使用Panel類,為了保留使用者直接定義Panel中子標記的特性,使自訂控制項直接繼承自Panel類,根據設定決定是否顯示標題列,並且根據面板狀態顯示恰當的展開或摺疊表徵圖。
由於Panel可以看作一個容器控制項,那麼當頁面上使用多個該控制項時會產生什麼樣的結果,容器中的子控制項還能保證是唯一的嗎?可以嘗試編寫一個繼承自Panel類的自訂控制項CustomerControl並向其控制項集合(Controls屬性)中添加一個ID為txt的文字框,當在頁面上放置兩個這樣的自訂控制項時會產生了兩個文字框(毫無疑問,因為是兩個自訂控制項),但是它們的id屬性均為txt,類似於以下代碼:
<span id="cc1"><input name="txt" type="text" id="txt" /></span> <span id="cc2"><input name="txt" type="text" id="txt" /></span> |
為了避免這種情況發生,只需要使CustomerControl類實現INamingContainer介面,再次預覽頁面,將會發現產生的文字框已經具有唯一的id了:
<span id="cc1"><input name="cc1$txt" type="text" id="cc1_txt" /></span> <span id="cc2"><input name="cc2$txt" type="text" id="cc2_txt" /></span> |
因此,我們的自訂控制項也需要實現INamingContainer介面,以確保所有子控制項ID屬性唯一。正如讀者所看到的,只需要標記實現INamingContainer介面而不需要編寫任何額外的方法,這也就是所謂的“標記介面”,與此類似的還有ISerializable等。
為了使摺疊控制項更靈活,允許開發人員設定是否可以展開/摺疊控制項,同時編寫屬性使得使用者可以不使用伺服器端提交方式,僅在用戶端執行展開/摺疊操作。還需要即使在使用者禁止檢視狀態的情況下仍然能夠記住控制項的展開/摺疊狀態,因此需要使用控制項狀態儲存狀態設定,最後提供一些額外的屬性協助設定標題的樣式。
由此得出,該自訂控制項應具有以下屬性:
屬性 |
描述 |
EnableDropDown |
是否允許展開/摺疊 |
EnableClientScript |
是否允許使用用戶端指令碼執行展開/摺疊動作 |
ShowExpanded |
初始狀態是否被展開 |
Cpation |
標題 |
CaptionBackColor |
標題背景色 |
CaptionForeColor |
標題前景色彩 |
為了能夠當使用者展開或摺疊時引發伺服器端事件,可以簡單的使用伺服器端圖片按鈕控制項顯示展開/摺疊表徵圖,使用者在處理該伺服器端事件時可能需要知曉當前面板的狀態(展開或摺疊,可以簡單的使用布爾型變數標識),需要通常事件參數的某個屬性來標識,但是現有的事件參數類不適合完成此項功能,所以我們同樣要編寫自己的事件類別,並且定義面板狀態屬性。
|
.NET中的事件基於委託,在ASP.NET中可以使用EventHandler委託(與此相關還定義了泛型委派)定義事件,事件參數包含了與事件有關的資料。有關委託和事件及泛型的知識請參閱相關書籍。 |
最後需要考慮的是如果使用提交引發伺服器端事件,如何將該事件暴露給開發人員處理。實際上對於這種需求有多種實現方式:
- 實現IPostBackEventHandler介面以處理回傳事件。
- 將子控制項事件作為頂層事件公開。
- 使用冒泡法將事件沿包含層次向上傳播到合適的位置引發。
在本次任務中我們將使用第二種方式,另外兩種方式將在以後的任務中介紹。為了將子控制項事件作為頂層事件公開,需要經過以下幾個步驟:
- 為自訂控制項定義事件
- 為了確保事件在引發時已經被訂閱編寫輔助方法檢查事件是否為空白(null)
- 編寫子控制項事件處理常式,根據需要建置事件參數引發事件(調用第2步中輔助方法)並執行其他的操作。
以上是對面板控制項的分析,接下來我們將按照分析的結果實現該控制項。
3. 實現
3.1 在解決方案ControlLibrary類庫中添加ExtendPanel類,並根據分析定義相關屬性:
public class ExtendPanel : Panel, INamingContainer { [Themeable(false)] public bool EnableDropDown { get { object o = ViewState["EnableDropDown"]; if (o == null) return false; return (bool)o; } set { ViewState["EnableDropDown"] = value; } }
// 是否使用用戶端指令碼 [Themeable(false)] public bool EnableClientScript { get { object o = ViewState["EnableClientScript"]; if (o == null) return false; return (bool)o; } set { ViewState["EnableClientScript"] = value; } }
// 是否被展開 [Themeable(false)] public bool ShowExpanded { get { object o = ViewState["ShowExpanded"]; if (o == null) return true; return (bool)o; } set { ViewState["ShowExpanded"] = value; } }
//標題 [Themeable(false)] public string Caption { get { object o = ViewState["Caption"]; if (o == null) return "Panel"; return (string)o; } set { ViewState["Caption"] = value; } }
//標題背景色 public Color CaptionBackColor { get { object o = ViewState["CaptionBackColor"]; if (o == null) return Color.SkyBlue; return (Color)o; } set { ViewState["CaptionBackColor"] = value; } }
//標題前景色彩 public Color CaptionForeColor { get { object o = ViewState["CaptionForeColor"]; if (o == null) return Color.White; return (Color)o; } set { ViewState["CaptionForeColor"] = value; } } } |
對於EnableDropdown等屬性在主題中定義不會影響到控制項的呈現樣式,所以這些屬性使用Themable特性進行定義,該特性指定屬性不受到主題和控制面板的影響。
3.2 接下來定義布爾型私人變數用於標識當前面板是展開狀態還是摺疊狀態,為了避免檢視狀態的影響,將該屬性值儲存在控制項狀態中:
private bool _panelDisplayed;
protected override void OnInit(EventArgs e) { base.OnInit(e); Page.RegisterRequiresControlState(this);//註冊控制項狀態 }
protected override object SaveControlState() { Pair p = new Pair(); p.First = base.SaveControlState();
p.Second = _panelDisplayed;
return p; }
protected override void LoadControlState(object savedState) { if (savedState == null) return;
Pair p = (Pair)savedState;
base.LoadControlState(p.First);
_panelDisplayed = (bool)p.Second; } |
3.3 重寫CreateChildControls方法,根據允許下拉(EnableDropDown)屬性設定決定是否顯示標題列:
protected override void CreateChildControls() { if (EnableDropDown)//如果允許下拉則建立標題條 { base.CreateChildControls(); CreateControlHierarchy(); } else { base.CreateChildControls();//如果不允許下拉,則顯示原有控制項 } } |
3.4 在CreateChildControls方法中調用了CreateControlHierarchy方法用於建立子控制項層次,該方法首先執行了兩部分操作,建立表格並添加第一行顯示標題和操作表徵圖;將原Panel容器中的控制項加入到表格的第二行中,同時將表格添加到控制項集合中並清空原有控制項:
protected virtual void CreateControlHierarchy() { Table t = new Table();
TableRow row1 = new TableRow(); t.Rows.Add(row1);
TableCell cell1 = new TableCell(); row1.Cells.Add(cell1); cell1.Text = " " + Caption;
TableCell cell2 = new TableCell(); row1.Cells.Add(cell2); cell2.HorizontalAlign = HorizontalAlign.Right;
TableRow row2 = new TableRow(); t.Rows.Add(row2); TableCell body = new TableCell(); body.ID = "Body"; row2.Cells.Add(body); body.ColumnSpan = 2;
Control[] rg = new Control[Controls.Count]; Controls.CopyTo(rg, 0);
foreach (Control ctl in rg) body.Controls.Add(ctl);
Controls.Clear(); Controls.Add(t); |
由於伺服器端控制項均是參考型別,所以不能直接將控制項添加到某個控制項集合中,而必須調用Controls.CopyTo將原有控制項複製到目標控制項數組中,否則在調用Controls.Clear方法時仍然會將控制項移除而導致錯誤的運行結果。
3.5 接下來在該方法中判斷是否使用用戶端展開/摺疊動作(EnableClientScript)屬性,如果該屬性為false,即表明要執行伺服器提交動作,因此向標題列儲存格中添加伺服器端圖片按鈕,並處理該控制項點擊事件(稍後會實現該事件處理常式):
WebControl img; if (!EnableClientScript) { img = new ImageButton(); ((ImageButton)img).Click += new ImageClickEventHandler(OnClick); cell2.Controls.Add(img); } |
3.6 如果在用戶端實現展開/摺疊動作則則向頁面註冊用戶端指令碼並附加到表徵圖的點擊事件上:
else { img = new System.Web.UI.WebControls.Image(); img.ID = "Icon"; cell2.Controls.Add(img);
// 添加樣式 img.Attributes["onmouseover"] = "this.style.cursor = \"hand\";"; img.Attributes["onmouseout"] = "this.style.cursor = \"\";"; img.Attributes["onclick"] = "__toggle()"; if (!Page.ClientScript.IsClientScriptBlockRegistered("__toggle")) { string js = BuildScript(body); Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "__toggle", js, true); } } |
在以上程式碼片段中調用了BuildScript方法用於產生JavaScript指令碼,以下是該方法實現(當然可以使用資源檔實現):
private string BuildScript(TableCell body) { StringBuilder sb = new StringBuilder(); sb.AppendLine("function __toggle() {"); sb.AppendFormat(" var body = document.getElementById(\"{0}\");\r\n", body.ClientID); sb.AppendLine(" var display = body.style.display;"); sb.AppendLine(" if (display == \"\") {"); sb.AppendLine(" body.style.display = \"none\";"); sb.AppendLine(" } else {"); sb.AppendLine(" body.style.display = \"\";"); sb.AppendLine(" }"); sb.AppendLine("}");
return sb.ToString(); } |
3.7 在CreateControlHierarchy方法的最後根據當前面板狀態顯示或隱藏面板內容並顯示相應的按鈕圖片,調用了兩個方法:
ShowChildControls(_panelDisplayed);
ShowImageButton(_panelDisplayed);
}
3.8 實現ShowChildControls方法以顯示或隱藏面板內容:
private void ShowChildControls(bool display)
{
if (Controls.Count != 1)
return;
Table t = (Table)Controls[0];
TableRow r = t.Rows[1];
TableCell body = r.Cells[0];
body.Style["display"] = (display ? "" : "none");
}
3.9 在ControlLibrary類庫中添加Image目錄,將collapse.bmp和expand.bmp圖片放置到該目錄中,設定產生動作為嵌入並在AssemblyInfo.cs中添加資源檔的註冊,實現ShowImageButton根據面板狀態顯示恰當的表徵圖:
private void ShowImageButton(bool display)
{
if (Controls.Count != 1)
return;
Table t = (Table)Controls[0];
TableRow r = t.Rows[0];
TableCell icon = r.Cells[1];
//設定相應圖片
System.Web.UI.WebControls.Image img =
(System.Web.UI.WebControls.Image)icon.Controls[0];
string imageName = "ControlLibrary.Image.expand.bmp";
if (display && !EnableClientScript)
imageName = "ControlLibrary.Image.collapse.bmp";
img.ImageUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
imageName);
}
3.10 接下來進行伺服器端點擊事件的處理,首先在ExtendPanel類中使用泛型委派聲明事件,該泛型委派意味著使用PanelClickEventArgs作為事件參數類型,並且該類必須繼承自EventArgs類:
public event EventHandler<PanelClickEventArgs> PanelClick;
定義PanelClientEventArgs事件參數類並添加BeingClosed屬性標識面板狀態:
public class PanelClickEventArgs : EventArgs
{
public bool BeingClosed
{
get;
set;
}
}
3.11 根據自控制項事件的分析,在ExtendPanel類中定義輔助方法檢查事件是否已被訂閱:
protected virtual void OnPanelClick(PanelClickEventArgs args)
{
if (PanelClick != null)
PanelClick(this, args);
}
3.12 觸發伺服器端圖片按鈕點擊事件時,在事件處理方法OnClick中建置事件參數類後調用OnPanelClick方法,並且更新面板和表徵圖顯示,如此訂閱的事件處理常式就可以參與到圖片按鈕點擊過程中:
private void OnClick(object sender, ImageClickEventArgs e)
{
PanelClickEventArgs args = new PanelClickEventArgs();
args.BeingClosed = _panelDisplayed;
OnPanelClick(args);
//更新顯示狀態
_panelDisplayed = !_panelDisplayed;
ShowChildControls(_panelDisplayed);
ShowImageButton(_panelDisplayed);
}
3.13 最後重寫Render方法呈現控制項,該方法調用PreparentControlForRendering方法將控制項的樣式應用的建立的表格上並根據屬性設定標題的前景色彩和背景色:
protected override void Render(HtmlTextWriter writer)
{
PrepareControlForRendering();
base.Render(writer);
}
protected virtual void PrepareControlForRendering()
{
if (Controls.Count != 1)
return;
// 應用樣式
Table t = (Table)Controls[0];
t.CopyBaseAttributes(this);
if (ControlStyleCreated)
t.ApplyStyle(ControlStyle);
t.CellPadding = 1;
t.CellSpacing = 0;
//設定標題樣式
TableRow row1 = t.Rows[0];
row1.BackColor = CaptionBackColor;
row1.ForeColor = CaptionForeColor;
}
3.14 在解決方案的Web網站中建立測試頁,聲明並定義自訂面板控制項,測試回合結果。
4. 總結
在本次任務裡我們建立了一個可以摺疊/展開的自訂面板控制項,應用EventHandler泛型委派和自訂事件類別將圖片的點擊事件公開為面板的頂層事件,這樣使用者就可以訂閱該事件進行自訂處理。該面板控制項兩個比較重要的屬性是EnableDropDown和EnableClientScript屬性,根據前者決定是否需要顯示附加的標題列,後者決定是產生JavaScript指令碼以在用戶端進行展開/摺疊操作,還是使用圖片按鈕處理伺服器點擊事件。如果點擊事件提交到伺服器端處理,則使用自訂事件類別儲存面板狀態並由圖片按鈕的點擊事件引發麵板頂層事件,使使用者能夠自訂點擊事件的處理。
在下次任務裡,我們將介紹事件處理的另外一種方式——使用IPostEventHandler介面,在原有星級控制項的基礎上增加評分的功能,允許使用者自由選擇評分並引發伺服器端事件,以能夠得到使用者選擇的分數。
ASP.NET自訂控制項系列文章
前言
第一天 簡單的星級控制項
第二天 帶有自訂樣式的星級控制項
第三天 使用控制項狀態的星級控制項
第四天 摺疊面板自訂控制項
第五天 可以評分的星級控制項
第六天 可以綁定資料來源的星級控制項
第七天 開發具有豐富特性的清單控制項
第八天 顯示多個條目星級評等的資料繫結控制項
第九天 自訂GridView
第十天 實現分頁功能的DataList
全部源碼下載
本系列文章PDF版本下載