1. 引言
前幾次任務裡我們開發的星級控制項只能顯示一個條目的評分,在現實生活中,經常會遇到需要向使用者展示一系列資料的評分狀態,例如所示:
本次任務裡,我們將一起開發這樣一個控制項。
2. 分析
通過可以看到,該自訂控制項是一系列資料評分等級的列表,很顯然需要作為一個資料繫結控制項來實現才可以靈活的顯示多條資料,並且在該列表上方顯示了標題和當前的日期,為了允許使用者靈活的定義標題和二級標題(當前日期),有必要引入模版的概念,由使用者編輯模版,最終按照模版內容顯示。如此看來再使用DataBoundControl作為自訂控制項的基類就不太適合了,因為我們要在該控制項中包含多個子控制項,那麼我們應該選擇哪個類作為基類?
回憶一下第六天的任務中資料繫結控制項的類別關係圖,其中有一個繼承自DataBoudControl類的CompositeDataBoundControl類,該類是.NET Framework 2.0中新增的一個類,用作綁定到資料來源中的資料服務器控制項的基類,該定義定義如下:
public abstract class CompositeDataBoundControl : DataBoundControl, INamingContainer
可以看到,該在只是在繼承DataBoundControl類的基礎上實現了INamingContainer,這意味著該類所包含的子控制項都會產生唯一的ID屬性。
但是CompositeDataBoundControl是如何?資料繫結的呢,換句話說,如果某一個頁麵包含了一個複雜資料繫結控制項,在某一個伺服器端控制項引起回傳後,如何確保資料繫結控制項能正確的被填充呢?按照設計,ASP.NET中的資料繫結群組控制項只能從資料繫結中擷取資料,並且不會緩衝任何繫結資料,因此,需要提供一個特殊的方法來處理回傳事件。
再次回顧一下DataBoundControl這個類,我們來分析一下資料是如何被顯示出來的。在DataBoundControl類上重載了BaseDataBoundControl類上定義的PerformSelect方法,該方法如下所示:
protected override void PerformSelect(){ if (this.DataSourceID.Length == 0) { this.OnDataBinding(EventArgs.Empty); } DataSourceView data = this.GetData(); this._arguments = this.CreateDataSourceSelectArguments(); this._ignoreDataSourceViewChanged = true; base.RequiresDataBinding = false; this.MarkAsDataBound(); data.Select(this._arguments, new DataSourceViewSelectCallback(this.OnDataSourceViewSelectCallback));}
該方法的關鍵在於不論資料來源是通過DataSource屬性指定還是通過DataSourceID屬性指定,最終都是通過資料來源檢視對象(DataSourceView)來擷取資料,在擷取資料來源檢視之後調用了該對象的Select方法,以擷取可枚舉的資料集合(不論資料來源是以何種方式提供),該方法還接收多個輸入參數和回呼函數。
到現在為止,我們已經可以通過為控制項設定資料來源資料並且通過資料來源檢視找到要讀取的資料,那麼接下來要考慮的是,如果訪問這個可繫結資料集合以將其顯示到頁面上?
答案是在資料來源檢視上處理Select語句操作結果的回呼函數最後會調用一個可重載的受保護方法PerformDataBinding方法,該方法定義如下:
protected internal virtual void PerformDataBinding ( IEnumerable data)
PerformDataBinding方法接收一個IEnumerable類型的對象以訪問返回的資料列表,我們可以利用該對象迭代訪問資料並根據需要建立控制項的內部結構最終呈現給使用者。
對於資料繫結群組控制項,和獨自處理資料繫結的控制項不同,它不將資料儲存到視圖中,而是交由各個子控制項處理,此時,就不能通過簡單的重寫CreateChildControls方法實現了,通過.NET Reflection工具觀察CompositeDataBindControl類上該方法的實現:
protected internal override void CreateChildControls(){ this.Controls.Clear(); object obj2 = this.ViewState["_!ItemCount"]; if ((obj2 == null) && base.RequiresDataBinding) { this.EnsureDataBound(); } if ((obj2 != null) && (((int)obj2) != -1)) { DummyDataSource dataSource = new DummyDataSource((int)obj2); this.CreateChildControls(dataSource, false); base.ClearChildViewState(); }}
CreateChildControls方法有兩種工作模式:繫結模式和非繫結模式。
在繫結模式下,將會正常建立控制項樹,具體來說會在執行PerformDataBinding方法中調用重載的CreateChildControls方法,以手動的實現資料控制項層次的建立;
protected internal override void PerformDataBinding(IEnumerable data){ base.PerformDataBinding(data); this.Controls.Clear(); base.ClearChildViewState(); this.TrackViewState(); int num = this.CreateChildControls(data, true); base.ChildControlsCreated = true; this.ViewState["_!ItemCount"] = num;}
而在非繫結模式中,將從CreateChildControls方法中調用帶有兩個參數的重載:
DummyDataSource dataSource = new DummyDataSource((int) obj2);this.CreateChildControls(dataSource, false);base.ClearChildViewState();
在這種情況下,傳遞給CreateChildControls方法的第二個參數為false,這表示不會向控制項層次添加任何資料。ASP.NET回傳機制確保每一個子控制項正確的從視圖中恢複自己的值。
接下來觀察在CompositeDataBindControl類上定義的CreateChildControls方法的重載:
protected abstract int CreateChildControls ( IEnumerable dataSource, bool dataBinding)
該方法的參數說明如下:
屬性 |
描述 |
Attribute |
在最終的標記中,該屬性永久儲存為一個已編碼的HTML特性 |
EncodedInnerDefaultProperty |
該屬性儲存區為該控制項的內部文本。該屬性的值是編碼的HTML,只能將字串賦給該屬性 |
InnerDefaultProperty |
永久儲存為控制項中的內部文本的屬性並且是元素的預設屬性,只有一個屬效能夠指定為預設屬性 |
InnerProperty |
該屬性永久儲存為控制項中的一個嵌入標記,這是使用模版和風格的複雜物件常用的一個屬性。 |
模版屬性樣本如下所示:
private ITemplate _titleTemplate;[PersistenceMode(PersistenceMode.InnerProperty)][TemplateContainer(typeof(TitleTemplateContainer))]public ITemplate TitleTemplate{ get { return _titleTemplate; } set { _titleTemplate = value; }}
3. 實現
3.1 在解決方案ControlLibrary類庫中建立BarChartItem類用於表示控制項項:
using System;using System.Collections.ObjectModel;using System.Web.UI;using System.Web.UI.WebControls; public class BarChartItem:TableRow{ public BarChartItem(BarChartItemType itemType) { ItemType = itemType; } public BarChartItemType ItemType { get; set; } public object DataItem { get; set; }}
3.2 在BarChartItem類中使用了BarChartItemType枚舉以標識當前項的類型(類似於GridView中的標題、交替行等),編寫該枚舉實現:
public enum BarChartItemType{ Title, //標題 SubTitle, //二級標題 Item, //資料項目 Footer //頁尾}
3.3 定義該控制項項的集合類BarChartItemCollection:
public class BarChartItemCollection : Collection<BarChartItem>{}
3.4 為了能夠在建立項和綁定資料後暴露出相應的事件中使使用者能夠訪問資料項目資訊,編寫自訂事件參數類:
public class BarChartItemEventArgs : EventArgs{ private BarChartItem _item; public BarChartItemEventArgs(BarChartItem item) { _item = item; } public BarChartItem Item { get { return _item; } }}
3.5 定義模版容器類
public class TitleTemplateContainer : WebControl, INamingContainer{ private BarChart _parent; public TitleTemplateContainer(BarChart parent) { _parent = parent; } public string Title { get { return _parent.Title; } } public string SubTitle { get { return _parent.SubTitle; } } public BarChart BarChart { get { return _parent; } }}
3.6 在ControlLibrary類庫中建立BarChar類並定義私人變數,該類繼承自CompositeDataBoundControl類:
public class BarChart : CompositeDataBoundControl{ public event EventHandler<BarChartItemEventArgs> BarChartItemCreated; public event EventHandler<BarChartItemEventArgs> BarChartItemDataBound; private BarChartItemCollection _items; private ITemplate _titleTemplate; private TitleTemplateContainer _titleTemplateContainer; private TableItemStyle _titleStyle; private TableItemStyle _subtitleStyle; private TableItemStyle _labelStyle;}
3.7 定義相關屬性:
public string DataTextField{ get { object o = ViewState["DataTextField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataTextField"] = value; }} public string DataValueField{ get { object o = ViewState["DataValueField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataValueField"] = value; }} public BarChartItemCollection Items{ get { if (_items == null) { _items = new BarChartItemCollection(); } return _items; }} [Browsable(false)][PersistenceMode(PersistenceMode.InnerProperty)][TemplateContainer(typeof(TitleTemplateContainer))]public ITemplate TitleTemplate{ get { return _titleTemplate; } set { _titleTemplate = value; }} public string Title{ get { object o = ViewState["Title"]; return o == null ? string.Empty : (string)o; } set { ViewState["Title"] = value; }} public string SubTitle{ get { object o = ViewState["SubTitle"]; return o == null ? string.Empty : (string)o; } set { ViewState["SubTitle"] = value; }} [PersistenceMode(PersistenceMode.InnerProperty)][DesignerSerializationVisibility(DesignerSerializationVisibility.Content)][NotifyParentProperty(true)][Description("標題樣式")]public virtual TableItemStyle TitleStyle{ get { if (_titleStyle == null) { _titleStyle = new TableItemStyle(); } if (IsTrackingViewState) ((IStateManager)_titleStyle).TrackViewState(); return _titleStyle; }} [PersistenceMode(PersistenceMode.InnerProperty)][DesignerSerializationVisibility(DesignerSerializationVisibility.Content)][NotifyParentProperty(true)][Description("二級標題樣式")]public virtual TableItemStyle SubTitleStyle{ get { if (_subtitleStyle == null) { _subtitleStyle = new TableItemStyle(); } if (IsTrackingViewState) ((IStateManager)_subtitleStyle).TrackViewState(); return _subtitleStyle; }} [PersistenceMode(PersistenceMode.InnerProperty)][DesignerSerializationVisibility(DesignerSerializationVisibility.Content)][NotifyParentProperty(true)][Description("標籤樣式")]public virtual TableItemStyle LabelStyle{ get { if (_labelStyle == null) { _labelStyle = new TableItemStyle(); // Can initialize the style HERE } if (IsTrackingViewState) ((IStateManager)_labelStyle).TrackViewState(); return _labelStyle; }}
3.8 定義事件回應程式法:
protected virtual void OnBarChartCreated(BarChartItemEventArgs e){ if (BarChartItemCreated != null) BarChartItemCreated(this, e);} protected virtual void OnBarChartDataBound(BarChartItemEventArgs e){ if (BarChartItemDataBound != null) BarChartItemDataBound(this, e);}
3.9 編寫建立子控制項層次方法:
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding){ return CreateControlHierarchy(dataSource, dataBinding);} private int CreateControlHierarchy(IEnumerable dataSource, bool dataBinding){ if (dataSource == null) { RenderEmptyControl(); return 0; } Table t = new Table(); Controls.Add(t); //建立標題 CreateTitle(t); //建立二級標題 CreateSubTitle(t); //建立資料項目 int totalItems = CreateAllItems(t, dataSource, dataBinding); return totalItems;}
3.10 編寫建立控制項方法:
private void RenderEmptyControl(){ LiteralControl lbl = new LiteralControl("??????"); Controls.Add(lbl);} private void CreateTitle(Table t){ BarChartItem item = new BarChartItem(BarChartItemType.Title); t.Rows.Add(item); TableCell cell = new TableCell(); item.Cells.Add(cell); cell.ColumnSpan = 2; if (TitleTemplate != null) { _titleTemplateContainer = new TitleTemplateContainer(this); TitleTemplate.InstantiateIn(_titleTemplateContainer); cell.Controls.Add(_titleTemplateContainer); } else { cell.Text = Title; } item.DataBind();} private void CreateSubTitle(Table t){ BarChartItem item = new BarChartItem(BarChartItemType.SubTitle); t.Rows.Add(item); TableCell cell = new TableCell(); item.Cells.Add(cell); cell.ColumnSpan = 2; cell.Text = SubTitle;} private int CreateAllItems(Table t, IEnumerable data, bool useDataSource){ int itemCount = 0; Items.Clear(); foreach (object o in data) { BarChartItemType itemType = BarChartItemType.Item; BarChartItem item = CreateBarChartItem(t, itemType, o, useDataSource); _items.Add(item); itemCount++; } return itemCount;} private BarChartItem CreateBarChartItem(Table t, BarChartItemType itemType, object dataItem, bool useDataSource){ BarChartItem item = new BarChartItem(itemType); TableCell labelCell = CreateLabelCell(item); TableCell valueCell = CreateScoreCell(item); BarChartItemEventArgs argsCreated = new BarChartItemEventArgs(item); OnBarChartCreated(argsCreated); t.Rows.Add(item); if (useDataSource) { item.DataItem = dataItem; BindLabelCell(labelCell, dataItem); BindValueCell(valueCell, dataItem); BarChartItemEventArgs argsData = new BarChartItemEventArgs(item); OnBarChartDataBound(argsData); } return item;} private TableCell CreateLabelCell(BarChartItem item){ TableCell cell = new TableCell(); item.Cells.Add(cell); return cell;} private TableCell CreateScoreCell(BarChartItem item){ TableCell cell = new TableCell(); item.Cells.Add(cell); string starPath = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.Image.stars.gif"); Panel panBg = new Panel(); panBg.Style.Add(HtmlTextWriterStyle.Width, "80px"); panBg.Style.Add(HtmlTextWriterStyle.Height, "16px"); panBg.Style.Add(HtmlTextWriterStyle.TextAlign, "left"); panBg.Style.Add(HtmlTextWriterStyle.Overflow, "hidden"); panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panBg.Style.Add("background-position", "0px -32px"); panBg.Style.Add("background-repeat", "repeat-x"); cell.Controls.Add(panBg); Panel panCur = new Panel(); panCur.Style.Add(HtmlTextWriterStyle.Height, "16px"); panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panCur.Style.Add("background-position", "0px 0px"); panCur.Style.Add("background-repeat", "repeat-x"); panBg.Controls.Add(panCur); return cell;} private void BindLabelCell(TableCell cell, object dataItem){ if (!string.IsNullOrEmpty(DataTextField)) { string txt = Convert.ToString(DataBinder.GetPropertyValue(dataItem, DataTextField)); cell.Text = txt; }} /// <summary>/// ????????????/// </summary>/// <param name="cell"></param>/// <param name="dataItem"></param>private void BindValueCell(TableCell cell, object dataItem){ Panel panCur = (Panel)cell.Controls[0].Controls[0]; object o = null; if (!String.IsNullOrEmpty(DataValueField)) o = DataBinder.GetPropertyValue(dataItem, DataValueField); else return; int score = Convert.ToInt32(o); string width = score * 16 + "px"; panCur.Style.Add(HtmlTextWriterStyle.Width, width);} protected override void Render(HtmlTextWriter writer){ PrepareControlForRender(); base.Render(writer);} public void PrepareControlForRender(){ if (Controls.Count != 1) return; Table t = (Table)Controls[0]; t.CopyBaseAttributes(this); if (ControlStyleCreated) t.ApplyStyle(ControlStyle); t.Rows[0].Cells[0].MergeStyle(TitleStyle); t.Rows[1].Cells[0].MergeStyle(SubTitleStyle); // Apply style to the labels that render team names for (int i = 2; i < Items.Count; i++) { // Style team labels t.Rows[i].Cells[0].ApplyStyle(LabelStyle); }}
3.10 在頁面中聲明並定義自訂控制項,預覽結果。
<cc:BarChart ID="bar" runat="server" Font-Size="12px" Title="????????"> <TitleTemplate> <%# Container.Title %> <small>(<%# DateTime.Now.ToString() %>)</small> </TitleTemplate></cc:BarChart>
3.11 編寫後置代碼,預覽結果:
protected void Page_Load(object sender, EventArgs e){ if (!IsPostBack) this.BindData();} private void BindData(){ DataTable table = new DataTable(); DataColumn col = new DataColumn("Comment", typeof(string)); table.Columns.Add(col); col = new DataColumn("Score", typeof(int)); table.Columns.Add(col); Random ran=new Random(); for (int i = 0; i < 10; i++) { DataRow row = table.NewRow(); int num= ran.Next(0, 6); row[0] = "資料 "+i+" , "+num+" 星"; row[1] = num; table.Rows.Add(row); } table.AcceptChanges(); bar.DataSource = table; bar.DataTextField = "Comment"; bar.DataValueField = "Score"; bar.DataBind();}
4. 總結
複雜綁據繫結控制項可以通過擴充CompositeDataBindControl類實現,該類有一個特殊的CreateChildControls方法實現資料的綁定和恢複,在下一次任務裡,我們將嘗試擴充DataList和GridView以實現自訂分頁控制項。
ASP.NET自訂控制項系列文章
前言
第一天 簡單的星級控制項
第二天 帶有自訂樣式的星級控制項
第三天 使用控制項狀態的星級控制項
第四天 摺疊面板自訂控制項
第五天 可以評分的星級控制項
第六天 可以綁定資料來源的星級控制項
第七天 開發具有豐富特性的清單控制項
第八天 顯示多個條目星級評等的資料繫結控制項
第九天 自訂GridView
第十天 實現分頁功能的DataList
全部源碼下載
本系列文章PDF版本下載