打造通用ASP.NET資料分頁控制元件
對於幾乎所有的資料表現Web應用來說,組織好資料的顯示方式、避免給使用者帶來混亂的感覺就是最主要的目標之一。每個頁面顯示20條記錄當然是可以接受的,但每頁顯示10000條記錄就很容易給使用者帶來不便了。將資料分成多個頁面顯示,即對資料進行分頁,是解決此類問題的最常見的辦法。
一、慨述
ASP.NET本身只提供了一個支援資料分頁的控制元件,即DataGrid分頁控制元件,不過它比較適合Intranet環境使用,對於Internet環境來說,DataGrid分頁控制元件提供的功能似乎不足以構造出靈活的Web應用。其中一個原因是,DataGrid控制元件對Web設計者放置分頁控制元件的位置和分頁控制元件的外觀都有限制,例如,DataGrid控制元件不允許垂直放置分頁控制元件。另一個能夠發揮分頁技術優勢的控制元件是Repeater,Web開發者可以利用Repeater控制元件快速配置資料的顯示方式,但分頁功能卻需要開發者自己實現。資料來源在不斷地變化,資料表現方式也千差萬別,如果針對這些不斷變動的條件分別定製分頁控制元件,顯然太浪費時間了,構造一個不限於特定表現控制元件的通用分頁控制元件將極大地有利於節省時間。
一個優秀的通用資料控制元件不僅提供常規的分頁功能,而且還要能夠:
⑴ 提供“首頁”、“上一頁”、“下一頁”、“末頁”分頁導航按鈕。
⑵ 根據資料顯示情況調整自身的狀態,即具有資料敏感性。如果分頁控制元件被設定成每頁顯示10個記錄,但實際上只有9個記錄,那麼分頁控制元件不應該顯示出來;在資料分成多頁顯示的情況下,第一個頁面的“首頁”、“上一頁”按鈕不應顯示出來,最後一個頁面的“下一頁”、“末頁”按鈕也不應該顯示出來。
⑶ 不能依賴於特定的資料顯示控制元件。
⑷ 具有適應各種現有、將有資料來源的能力。
⑸ 應當能夠方便地配置顯示方式,輕鬆地整合到各種應用之中。
⑹ 當分頁就緒時,提醒其他控制元件。
⑺ 即使是缺乏經驗的Web設計者,也要能夠毫無困難地使用。
⑻ 提供有關分頁資訊的屬性資料。
目前市場上已經有一些提供上述功能的商業性控制元件,不過都價格不菲。對於許多開發者來說,自己構造一個通用的分頁控制元件是最理想的選擇。
圖一顯示了本文通用分頁控制元件的執行介面,其中用於顯示的控制元件是一個Repeater控制元件。分頁控制元件由兩類部件構成:四個導航按鈕,一組頁面編號連結。
圖一
使用者可以方便地改換顯示控制元件、改變分頁控制元件本身的外觀。例如,在圖二中,和分頁控制元件協作的顯示控制元件換成了一個DataGrid控制元件,頁面編號連結和四個導航按鈕分兩行顯示。
圖二
ASP.NET支援建立定製Web控制元件的三種方式:使用者控制元件,複合控制元件,自定義控制元件。第三種控制元件即自定義控制元件的名稱很容易引起誤解。實際上,所有這三種控制元件都應該算是自定義控制元件。複合控制元件和微軟所謂的自定義控制元件的不同之處在於,前者要用到CreateChildControls()方法,CreateChildControls()方法允許控制元件根據某些事件重新繪製自身。對於本文的通用分頁器,我們將使用複合控制元件。
下面的UML序列圖概括了通用分頁控制元件的一般機制。
圖三
雖然我們的目標是讓通用分頁控制元件不依賴於表現資料的控制元件,但很顯然,總得有某種方法讓分頁控制元件訪問資料。每一個從Control類繼承的控制元件都提供一個DataBinding事件。我們把分頁器本身註冊成DataBinding事件的監聽器,分頁器就可以獲知資料的情況並修改資料。由於所有從Control類繼承的控制元件都有這個DataBinding事件,所以分頁器控制元件達到了不依賴於特定資料表現控制元件的目標——換句話說,分頁器控制元件可以繫結到所有從Control類派生的控制元件,即它能夠繫結到幾乎所有的Web控制元件。
二、核心功能
當表現控制元件觸發DataBinding事件,分頁控制元件就可以獲取DataSource屬性。遺憾的是,微軟沒有提供所有資料繫結類實現的介面,諸如IdataSourceProvider之類,而且並非所有從Control或WebControl類繼承的控制元件都有一個DataSource屬性,因此向上定型成Control類沒有意義,唯一可行的辦法是通過Reflection API直接操作DataSoruce屬性。在討論事件控制代碼方法之前,應該指出的是,為了註冊事件控制代碼,首先必須獲得一個表現控制元件的引用。分頁控制元件顯露了一個簡單的字串屬性BindToControl:
public string BindToControl { get { if (_bindcontrol == null) throw new NullReferenceException("在使用分頁控制元件之前,請先通過設定BindToControl屬性繫結到一個控制元件。"); return _bindcontrol;} set{_bindcontrol=value;} } |
這個方法非常重要,所以最好能夠丟擲一個含義更明確的資訊,而不是丟擲標準的NullReferenceException異常。在分頁控制元件的OnInit方法中,我們解析了對錶現控制元件的引用。本例應當用OnInit事件控制代碼(而不是建構函式)來確保JIT編譯的aspx頁面已經設定了BindToControl。
protected override void OnInit(EventArgs e) { _boundcontrol = Parent.FindControl(BindToControl); BoundControl.DataBinding += new EventHandler(BoundControl_DataBound); base.OnInit(e); ... } |
搜尋表現控制元件的操作通過搜尋分頁控制元件的Parent控制元件完成,在這裡,Parent就是頁面本身。按照這種方式使用Parent比較危險,舉例來說,如果分頁控制元件嵌入到了另一個控制元件之中,例如嵌入到了Table控制元件之中,則Parent引用實際上將是一個對Table控制元件的引用。由於FindControl方法只搜尋當前的控制元件集合,除非表現控制元件就在該集合之中,否則不可能搜尋到。一種比較安全的方法是遞迴地搜尋各個控制元件集合,直至找到目標控制元件為止。
找到BoundControl之後,我們將分頁控制元件註冊成為DataBinding事件的監聽器。由於分頁控制元件要操作資料來源,所以該事件控制代碼應當是呼叫鏈中的最後一個,這一點很重要。不過,只要表現控制元件在OnInit事件控制代碼中註冊DataBinding的事件控制代碼(預設行為),分頁控制元件操作資料來源時就不會出現問題。
DataBound事件控制代碼負責獲取表現控制元件的DataSource屬性。
private void BoundControl_DataBound(object sender,System.EventArgs e) { if (HasParentControlCalledDataBinding) return; Type type = sender.GetType(); _datasource = type.GetProperty("DataSource"); if (_datasource == null) throw new NotSupportedException("分頁控制元件要求表現控制元件必需包含一個DataSource。"); object data = _datasource.GetGetMethod().Invoke(sender,null); _builder = Adapters[data.GetType()]; if (_builder == null) throw new NullReferenceException("沒有安裝適當的介面卡來處理下面的資料來源型別:"+data.GetType()); _builder.Source = data; ApplyDataSensitivityRules(); BindParent(); RaiseEvent(DataUpdate,this); } |
為了提供一個健壯的、可伸縮的接插式體系結構,我們將利用[GoF] Builder模式構造出一個解決方案。
圖四
IDataSourceAdapter介面定義了分頁控制元件操作資料所需的最基本的元素,相當於“插頭”。
publicinterface IDataSourceAdapter { int TotalCount{get;} object GetPagedData(int start,int end); } |
TotalCount屬性返回在處理資料之前資料來源所包含元素的總數,而GetPagedData方法返回原始資料的一個子集,例如:假設資料來源是一個包含20個元素的陣列,分頁控制元件將資料顯示成每頁10個元素,則第一頁的元素子集是陣列元素0-9,第二頁的元素子集是陣列元素10-19。DataViewAdapter提供了一個DataView型別的插頭:
internal class DataViewAdapter:IDataSourceAdapter { private DataView _view; internal DataViewAdapter(DataView view) { _view = view; } public int TotalCount { get{return (_view == null) ? 0 : _view.Table.Rows.Count;} } public object GetPagedData(int start, int end) { DataTable table = _view.Table.Clone(); for (int i = start;i<=end && i<= TotalCount;i++) { table.ImportRow(_view[i-1].Row); } return table; } } |
DataViewAdapter實現了IDataSourceAdapter的GetPagedData方法,該GetPagedData克隆原始的DataTable,將原始DataTable中的資料匯入到新的DataTable。該類的可見性有意地設定成internal,目的是為了向Web開發者隱藏實現細節,進而通過Builder類提供一個更簡單的介面。
public abstract class AdapterBuilder { private object _source; private void CheckForNull() { if (_source == null) throw new NullReferenceException("必須提供一個合法的資料來源"); } public virtual object Source { get { CheckForNull(); return _source;} set { _source = value; CheckForNull(); } } public abstract IDataSourceAdapter Adapter{get;} } |
AdapterBuilder抽象類為IdataSourceAdapter型別提供了一個更容易管理的介面,由於提高了抽象程度,我們不必再直接使用IdataSourceAdapter,同時AdapterBuilder還提供了在分頁資料之前執行預處理的指令。另外,該Builder還使得實際的實現類,例如DataViewAdapter,對分頁控制元件的使用者透明:
public class DataTableAdapterBuilder:AdapterBuilder { private DataViewAdapter _adapter; private DataViewAdapter ViewAdapter { get { if (_adapter == null) { DataTable table = (DataTable)Source; _adapter = new DataViewAdapter(table.DefaultView); } return _adapter; } } public override IDataSourceAdapter Adapter { get { return ViewAdapter; } } } public class DataViewAdapterBuilder:AdapterBuilder { private DataViewAdapter _adapter; private DataViewAdapter ViewAdapter { get { // 延遲例項化 if (_adapter == null) { _adapter = new DataViewAdapter((DataView)Source); } return _adapter; } } public override IDataSourceAdapter Adapter { get{return ViewAdapter;} } } |
DataView型別和DataTable型別的關係是如此密切,所以構造一個通用性的DataAdapter可能是有意義的,其實只要加入另一個處理DataTable的建構函式就足夠了。遺憾的是,當用戶需要不同的功能來處理某個DataTable時,就必須替換或繼承整個類。如果我們構造一個使用同一IdataSourceAdapter的新Builder,使用者在選擇如何實現介面卡時就擁有更多的自由。
在分頁控制元件中,尋找適當Builder類的操作由一個型別安全的集合完成。
public class AdapterCollection:DictionaryBase { private string GetKey(Type key) { return key.FullName; } public AdapterCollection() {} publicvoid Add(Type key,AdapterBuilder value) { Dictionary.Add(GetKey(key),value); } publicbool Contains(Type key) { return Dictionary.Contains(GetKey(key)); } publicvoid Remove(Type key) { Dictionary.Remove(GetKey(key)); } public AdapterBuilder this[Type key] { get{return (AdapterBuilder)Dictionary[GetKey(key)];} set{Dictionary[GetKey(key)]=value;} } } |
AdapterCollection依賴於DataSource型別,DataSource通過BoundControl_DataBound巧妙地引入。這裡使用的索引鍵是Type.FullName方法,確保了每一種型別索引鍵的唯一性,同時這也把保證每一種型別只有一個Builder的責任賦予了AdapterCollection。將Builder查詢加入BoundControl_DataBound方法,結果如下:
public AdapterCollection Adapters { get{return _adapters;} } private bool HasParentControlCalledDataBinding { get{return _builder != null;} } private void BoundControl_DataBound(object sender,System.EventArgs e) { if (HasParentControlCalledDataBinding) return; Type type = sender.GetType(); _datasource = type.GetProperty("DataSource"); if (_datasource == null) throw new NotSupportedException("分頁控制元件要求表現控制元件必需包含一個DataSource。"); object data = _datasource.GetGetMethod().Invoke(sender,null); _builder = Adapters[data.GetType()]; if (_builder == null) throw new NullReferenceException("沒有安裝適當的介面卡來處理下面的資料來源型別:"+data.GetType()); _builder.Source = data; ApplyDataSensitivityRules(); BindParent(); RaiseEvent(DataUpdate,this); } |
BoundControl_DataBound方法利用HasParentControlCalledDataBinding檢查是否已經建立了Builder,如果是,則不再執行尋找適當Builder的操作。Adapters表的初始化在建構函式中完成:
public Pager() { SelectedPager=new System.Web.UI.WebControls.Style(); UnselectedPager = new System.Web.UI.WebControls.Style(); _adapters = new AdapterCollection(); _adapters.Add(typeof(DataTable),new DataTableAdapterBuilder()); _adapters.Add(typeof(DataView),new DataViewAdapterBuilder()); } |
最後一個要實現的方法是BindParent,用來處理和返回資料。
private void BindParent() { _datasource.GetSetMethod().Invoke(BoundControl, new object[]{_builder.Adapter.GetPagedData(StartRow,ResultsToShow*CurrentPage)}); } |
這個方法很簡單,因為資料處理實際上是由Adapter完成的。這一過程結束後,我們還要用一次Reflection API,不過這一次是設定表現控制元件的DataSource屬性。 三、介面設計
至此為止,分頁控制元件的核心功能已經差不多實現,不過如果缺少適當的表現方式,分頁控制元件不會很有用。
為了有效地將表現方式與程式邏輯分離,最好的辦法莫過於使用模板,或者說得更具體一點,使用Itemplate介面。實際上,微軟清楚地瞭解模板的強大功能,幾乎每一個地方都用到了模板,甚至頁面解析器本身也不例外。遺憾的是,模板並不象有些人認為的那樣是一個簡單的概念,需要花些時間才能真正掌握它的精髓,好在這方面的資料比較多,所以這裡就不再贅述了。返回來看分頁控制元件,它有四個按鈕:首頁,前一頁,後一頁,末頁,當然另外還有各個頁面的編號。四個導航按鈕選自ImageButton類,而不是LinkButton類,從專業的Web設計角度來看,圖形按鈕顯然要比單調的連結更有用一些。
public ImageButton FirstButton{get {return First;}} public ImageButton LastButton{get {return Last;}} public ImageButton PreviousButton{get {return Previous;}} public ImageButton NextButton{get {return Next;}} |
頁面編號是動態構造的,這是因為它們依賴於資料來源中記錄數量的多少、每個頁面顯示的記錄數量。頁面編號將加入到一個Panel,Web設計者可以通過Panel來指定要在哪裡顯示頁面編號。有關建立頁面編號的過程稍後再詳細討論,現在我們需要為分頁控制元件提供一個模板,使得使用者能夠定製分頁控制元件的外觀。
[Template Container(typeof(LayoutContainer))] public ITemplate Layout { get{return (_layout;} set{_layout =value;} } public class LayoutContainer:Control,INamingContainer { public LayoutContainer() {this.ID = "Page";} } |
LayoutContainer類為模板提供了一個容器。一般而言,在模板容器中加入一個定製ID總是不會錯的,它將避免處理事件和進行頁面呼叫時出現的問題。下面的UML圖描述了分頁控制元件的表現機制。
圖五
建立模板的第一步是在aspx頁面中定義佈局:
<LAYOUT> <asp:ImageButton id="First" Runat="server" imageUrl="play2L_dis.gif" AlternateText="首頁"></asp:ImageButton> <asp:ImageButton id="Previous" Runat="server" imageUrl="play2L.gif" AlternateText="上一頁"></asp:ImageButton> <asp:ImageButton id="Next" Runat="server" imageUrl="play2.gif" AlternateText="下一頁"></asp:ImageButton> <asp:ImageButton id="Last" Runat="server" imageUrl="play2_dis.gif" AlternateText="末頁"></asp:ImageButton> <asp:Panel id="Pager" Runat="server"></asp:Panel> </LAYOUT> |
這個佈局例子不包含任何格式元素,例如表格等,實際應用當然可以(而且應該)加入格式元素,請參見稍後的更多說明。
Itemplate介面只提供了一個方法InstantiateIn,它解析模板並繫結容器。
private void InstantiateTemplate() { _container = new LayoutContainer(); Layout.InstantiateIn(_container); First = (ImageButton)_container.FindControl("First"); Previous = (ImageButton)_container.FindControl("Previous"); Next = (ImageButton)_container.FindControl("Next"); Last = (ImageButton)_container.FindControl("Last"); Holder = (Panel)_container.FindControl("Pager"); this.First.Click += new System.Web.UI.ImageClickEventHandler(this.First_Click); this.Last.Click += new System.Web.UI.ImageClickEventHandler(this.Last_Click); this.Next.Click += new System.Web.UI.ImageClickEventHandler(this.Next_Click); this.Previous.Click += new System.Web.UI.ImageClickEventHandler(this.Previous_Click); } |
控制元件的InstatiateTemplate方法要做的第一件事情是例項化模板,即呼叫Layout.InstantiateIn(_container)。容器其實也是一種控制元件,用法也和其他控制元件相似。InstantiateTemplate方法利用這一特點尋找四個導航按鈕,以及用來容納頁面編號的Panel。導航按鈕通過它們的ID找到,這是對分頁控制元件的一點小小的限制:導航按鈕必須有規定的ID,分別是First、Previous、Next、Last,另外,Panel的ID必須是Pager,否則就會找不到。遺憾的是,就我們選定的表現機制而言,這似乎是較好的處理方式了;但可以相信的是,只要提供適當的說明文件,這一小小限制不會帶來什麼問題。另外一種可選擇使用的辦法是:讓每一個按鈕從ImageButton類繼承,從而也就定義了一個新的型別;由於每一個按鈕是一種不同的型別,在容器中可以實現一個遞迴搜尋來尋找各種特定的按鈕,從而不必再用到按鈕的ID屬性。
找到四個按鈕之後,再把適當的事件控制代碼繫結到這些按鈕。在這裡必須做一個重要的決定,即何時呼叫InstantiateTemplate。一般地,這類方法應當在CreateChildControls方法中呼叫,因為CreateChildControls方法的主要用途就是這一類建立子控制元件的任務。由於分頁控制元件永遠不會修改其子控制元件,所以它不需要CreateChildControls提供的功能來根據某些事件修改顯示狀態。顯示子控制元件的速度總是越快越好,因此呼叫InstantiateTemplate方法的比較理想的位置是在OnInit事件中。
protected override void OnInit(EventArgs e) { _boundcontrol = Parent.FindControl(BindToControl); BoundControl.DataBinding += new EventHandler(BoundControl_DataBound); InstantiateTemplate(); Controls.Add(_container); base.OnInit(e); } |
OnInit方法除了呼叫InstantiateTemplate方法,它的另一個重要任務是將容器加入分頁控制元件。如果不將容器加入到分頁器的控制元件集合,由於Render方法永遠不會被呼叫,所以模板就不可能顯示出來。
模板還可以用程式設計的方式通過實現Itemplate介面定義,這一特性除了可作為提高靈活性的措施之外,還可以提供一個預設的模板,以便在使用者沒有通過aspx頁面提供模板時使用。
public class DefaultPagerLayout:ITemplate { private ImageButton Next; private ImageButton First; private ImageButton Last; private ImageButton Previous; private Panel Pager; public DefaultPagerLayout() { Next = new ImageButton(); First = new ImageButton(); Last = new ImageButton(); Previous = new ImageButton(); Pager = new Panel(); Next.ID="Next"; Next.AlternateText="下一頁";Next.ImageUrl="play2.gif"; First.ID="First"; First.AlternateText="首頁";First.ImageUrl="play2L_dis.gif"; Last.ID = "Last"; Last.AlternateText ="末頁";Last.ImageUrl="play2_dis.gif"; Previous.ID="Previous"; Previous.AlternateText="上一頁";Previous.ImageUrl="play2L.gif"; Pager.ID="Pager"; } public void InstantiateIn(Control control) { control.Controls.Clear(); Table table = new Table(); table.BorderWidth = Unit.Pixel(0); table.CellSpacing= 1; table.CellPadding =0; TableRow row = new TableRow(); row.VerticalAlign = VerticalAlign.Top; table.Rows.Add(row); TableCell cell = new TableCell(); cell.HorizontalAlign = HorizontalAlign.Right; cell.VerticalAlign = VerticalAlign.Middle; cell.Controls.Add(First); cell.Controls.Add(Previous); row.Cells.Add(cell); cell = new TableCell(); cell.HorizontalAlign= HorizontalAlign.Center; cell.Controls.Add(Pager); row.Cells.Add(cell); cell = new TableCell(); cell.VerticalAlign = VerticalAlign.Middle; cell.Controls.Add(Next); cell.Controls.Add(Last); row.Cells.Add(cell); control.Controls.Add(table); } } |
DefaultPagerLayout通過程式設計的方式提供了所有的導航元素,並將它們加入到aspx頁面,不過這一次導航元素用標準的HTML表格設定了格式。現在,如果使用者沒有提供一個表現模板,程式將自動提供一個預設的模板。
[TemplateContainer(typeof(LayoutContainer))] public ITemplate Layout { get{return (_layout == null)? new DefaultPagerLayout():_layout;} set{_layout =value;} } |
下面再來看看生成各個頁面編號的過程。分頁控制元件首先需要確定一些屬性值,通過這些屬性值來確定要生成多少不同的頁面編號。
public int CurrentPage { get { string cur = (string)ViewState["CurrentPage"]; return (cur == string.Empty || cur ==null)? 1 : int.Parse(cur); } set { ViewState["CurrentPage"] = value.ToString();} } public int PagersToShow { get{return _results;} set{_results = value;} } public int ResultsToShow { get{return _resultsperpage;} set{_resultsperpage = value;} } |
CurrentPage儲存的實際上是頁面編號的ViewState中的當前頁面,PagersToShow方法定義的屬性允許使用者指定要顯示多少頁面,而ResultsToShow定義的屬性則允許使用者指定每頁要顯示多少記錄,預設值是10。
NumberofPagersToGenerate返回當前應當生成的頁面編號的數量。
private int PagerSequence { get { return Convert.ToInt32 (Math.Ceiling((double)CurrentPage/(double)PagersToShow));} } private int NumberOfPagersToGenerate { get{return PagerSequence*PagersToShow;} } private int TotalPagesToShow { get{return Convert.ToInt32(Math.Ceiling((double)TotalResults/(double)_resultsperpage));} } public int TotalResults { get{return _builder.Adapter.TotalCount;} } |
TotalPagesToShow方法返回要顯示的總頁面數量,由使用者預設的ResultsToShow屬性調整。
雖然ASP.NET定義了一些預設的樣式,不過對於分頁控制元件的使用者它們可能不是很實用。使用者可以通過自定義樣式來調整分頁控制元件的外觀。
public Style UnSelectedPagerStyle {get {return UnselectedPager;}} public Style SelectedPagerStyle {get {return SelectedPager;}} |
UnSelectedPagerStyle提供了頁面編號未選中時所用的樣式,而SelectedPagerStyle提供了頁面編號被選中時所用的樣式。
private void GeneratePagers(WebControl control) { control.Controls.Clear(); int pager = (PagerSequence-1)* PagersToShow +1; for (;pager<=NumberOfPagersToGenerate && pager<=TotalPagesToShow;pager++) { LinkButton link = new LinkButton(); link.Text = pager.ToString(); link.ID = pager.ToString(); link.Click += new EventHandler(this.Pager_Click); if (link.ID.Equals(CurrentPage.ToString())) link.MergeStyle(SelectedPagerStyle); else link.MergeStyle(UnSelectedPagerStyle); control.Controls.Add(link); control.Controls.Add(new LiteralControl(" ")); } } private void GeneratePagers() { GeneratePagers(Holder); } |
GeneratePagers方法動態地建立所有頁面編號,頁面編號是LinkButton型別的按鈕。各個頁面編號的標籤和ID屬性通過迴圈賦值,同時,點選事件被繫結到適當的事件控制代碼。最後,頁面編號被加入到一個容器控制元件——在本例中是一個Panel物件。按鈕ID起到了標識哪一個按鈕觸發點選事件的作用。下面是事件控制代碼的定義:
private void Pager_Click(object sender, System.EventArgs e) { LinkButton button = (LinkButton) sender; CurrentPage = int.Parse(button.ID); RaiseEvent(PageChanged, this,new PageChangedEventArgs(CurrentPage,PagedEventInvoker.Pager)); Update(); } private void Next_Click(object sender, System.Web.UI.ImageClickEventArgs e) { if (CurrentPage<TotalPagesToShow) CurrentPage++; RaiseEvent(PageChanged, this,new PageChangedEventArgs(CurrentPage,PagedEventInvoker.Next)); Update(); } private void Previous_Click(object sender, System.Web.UI.ImageClickEventArgs e) { if (CurrentPage > 1) CurrentPage--; RaiseEvent(PageChanged, this,new PageChangedEventArgs(CurrentPage,PagedEventInvoker.Previous)); Update(); } private void First_Click(object sender, System.Web.UI.ImageClickEventArgs e) { CurrentPage = 1; RaiseEvent(PageChanged, this,new PageChangedEventArgs(CurrentPage,PagedEventInvoker.First)); Update(); } private void Last_Click(object sender, System.Web.UI.ImageClickEventArgs e) { CurrentPage = TotalPagesToShow; RaiseEvent(PageChanged, this,new PageChangedEventArgs(CurrentPage,PagedEventInvoker.Last)); Update(); } |
這些事件控制代碼都相似,它們首先更改分頁控制元件的當前頁面,然後重新整理繫結的控制元件。
private void Update() { if (!HasParentControlCalledDataBinding) return; ApplyDataSensitivityRules(); BindParent(); BoundControl.DataBind(); } |
首先,分頁控制元件通過呼叫HasParentControlCalledDataBinding方法檢查是否已經初始化了必要的介面卡。如果是,則將前面指出的根據資料顯示情況自動調整控制元件的規則應用到當前的控制元件,這些規則使得分頁控制元件根據BoundControl中資料的不同情況表現出不同的行為。雖然這些規則由分頁控制元件內部控制,但必要時可以用[GoF] State模式方便地移出到控制元件之外。
public bool IsDataSensitive { get{return _isdatasensitive;} set{_isdatasensitive = value;} } private bool IsPagerVisible { get{return (TotalPagesToShow != 1) && IsDataSensitive;} } private bool IsPreviousVisible { get { return (!IsDataSensitive)? true: (CurrentPage != 1); } } private bool IsNextVisible { get { return (!IsDataSensitive)? true: (CurrentPage != TotalPagesToShow); } } private void ApplyDataSensitivityRules() { FirstButton.Visible = IsPreviousVisible; PreviousButton.Visible = IsPreviousVisible; LastButton.Visible = IsNextVisible; NextButton.Visible = IsNextVisible; if (IsPagerVisible) GeneratePagers(); } |
ApplyDataSensitivityRules方法實施預定義的規則,諸如IsPagerVisible、IsPreviousVisible和IsNextVisible。預設情況下,分頁控制元件將啟用這些規則,但使用者可以通過設定IsDataSensitive屬性來關閉這些規則。
至此為止,分頁控制元件的顯示部分基本設計完畢。最後剩下的結束工作是提供幾個事件控制代碼,使得使用者能夠在各種分頁控制元件事件出現時進行必要的調整。
public delegate void PageDelegate(object sender,PageChangedEventArgs e); public enum PagedEventInvoker{Next,Previous,First,Last,Pager} public class PageChangedEventArgs:EventArgs { private int newpage; private Enum invoker; public PageChangedEventArgs(int newpage):base() { this.newpage = newpage; } public PageChangedEventArgs(int newpage,PagedEventInvoker invoker) { this.newpage = newpage; this.invoker = invoker; } public int NewPage {get{return newpage;}} public Enum EventInvoker{get{return invoker;}} } |
由於分頁控制元件需要返回自定義的事件引數,所以我們定義了一個專用的PageChangedEventArgs類。PageChangedEventArgs類返回PagedEventInvoker型別,PagedEventInvoker型別是可能觸發事件的控制元件的列舉量。為了處理自定義的事件引數,我們定義了一個新的delegate,即PageDelegate。事件按照下面的形式定義:
public event PageDelegate PageChanged; public event EventHandler DataUpdate; |
當事件沒有對應的事件監聽器時,ASP.NET會丟擲一個異常。分頁控制元件定義了下列RaiseEvent方法。
private void RaiseEvent(EventHandler e,object sender) { this.RaiseEvent(e,this,null); } private void RaiseEvent(EventHandler e,object sender, PageChangedEventArgs args) { if(e!=null) { e(sender,args); } } private void RaiseEvent(PageDelegate e,object sender) { this.RaiseEvent(e,this,null); } private void RaiseEvent(PageDelegate e,object sender, PageChangedEventArgs args) { if(e!=null) { e(sender,args); } } |
現在事件控制代碼可以通過呼叫各個RaiseEvent方法來觸發事件了。
四、應用例項
至此為止,分頁控制元件的設計已經全部完成,可以正式使用了。要使用該分頁控制元件,只要把它繫結到一個表現控制元件即可。
<asp:Repeater ID="repeater" Runat="server"> <ItemTemplate> 列1: <%# Convert.ToString(DataBinder.Eval(Container.DataItem,"Column1"))%> <br> 列2: <%# Convert.ToString(DataBinder.Eval(Container.DataItem,"Column2"))%> <br> 列3: <%# Convert.ToString(DataBinder.Eval(Container.DataItem,"Column3"))%> <br> <hr> </ItemTemplate> </asp:Repeater> <cc1:Pager id="pager" ResultsToShow="2" runat="server" BindToControl="repeater"> <SELECTEDPAGERSTYLE BackColor="Yellow" /> </cc1:Pager> |
上面的aspx頁面將分頁控制元件繫結到一個Repeater控制元件,設定每頁顯示的記錄數量為2,選中的頁面編號顏色為黃色,使用預設的佈局,效果如圖一。下面是另一個例子,它將分頁控制元件繫結到一個DataGrid,效果如圖二。
<asp:DataGrid ID="Grid" Runat="server"></asp:DataGrid> <cc1:Pager id="PagerGrid" ResultsToShow="2" runat="server" BindToControl="Grid"> <SELECTEDPAGERSTYLE BackColor="Red"></SELECTEDPAGERSTYLE> <LAYOUT> <asp:ImageButton id="First" Runat="server" imageUrl="play2L_dis.gif" AlternateText="首頁"></asp:ImageButton> <asp:ImageButton id="Previous" Runat="server" imageUrl="play2L.gif" AlternateText="上一頁"></asp:ImageButton> <asp:ImageButton id="Next" Runat="server" imageUrl="play2.gif" AlternateText="下一頁"></asp:ImageButton> <asp:ImageButton id="Last" Runat="server" imageUrl="play2_dis.gif" AlternateText="末頁"></asp:ImageButton> <asp:Panel id="Pager" Runat="server"></asp:Panel> </LAYOUT> </cc1:Pager> |
測試表明,分頁控制元件並不依賴於特定的表現控制元件,它可以方便地處理不同的資料來源,而且很容易使用,請讀者下載本文後面的原始碼參見完整的例子。
雖然學習開發自定義Web控制元件不是一件輕鬆的事情,但掌握這項技能帶來的好處不言而喻,只要稍微增加一些工作量,開發者就可以將普通的Web控制元件改換成多用途的通用控制元件,數十倍地提高工作效率,本文的分頁控制元件只是建立通用控制元件來滿足現有和將來表現需要的其中一個例子而已。