打造一個通用ASP.NET數據分頁控件

打造一個通用ASP.NET數據分頁控件
發佈於:2006-8-15 17:23:08 已被閱讀: <script src="http://www.dwww.cn/hits.asp?ID=2804" type="text/javascript"></script> 138 33
對於幾乎所有的數據表現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);
}

 

  在DataBound中,我們嘗試通過Reflection API獲得DataSource屬性,然後返回實際數據源的一個引用。現在雖然已經獲知了數據源,但分頁控件還必須知道如何操作該數據源。爲了讓分頁控件不依賴於特定的表現控件,問題複雜了很多。不過,如果讓分頁控件依賴於特定的數據源,那就背離了設計一個靈活的分頁控件的目標。我們要通過一個接插式的體系結構來確保分頁控件能夠處理各種數據源,無論是.NET提供的數據源,還是自定義的數據源。

  爲了提供一個健壯的、可伸縮的接插式體系結構,我們將利用[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("&nbsp;"));
}
}

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控件改換成多用途的通用控件,數十倍地提高工作效率,本文的分頁控件只是創建通用控件來滿足現有和將來表現需要的其中一個例子而已。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章