位置:海鸟网 > IT > ASP.NET >

一个通用ASP.NET数据分页控件

    对于几乎所有的数据表现Web应用来说,组织好数据的显示方式、避免给用户带来混乱的感觉就是最主要的目标之一。每个页面显示20条记录当然是可以接受的,但每页显示10000条记录就很容易给用户带来不便了。将数据分成多个页面显示,即对数据进行分页,是解决此类问题的最常见的办法。 

  一、慨述 

  ASP.NET本身只提供了一个支持数据分页的控件,即DataGrid分页控件,不过它比较适合Intranet环境使用,对于Internet环境来说,DataGrid分页控件提供的功能似乎不足以构造出灵活的Web应用。其中一个原因是,DataGrid控件对Web设计者放置分页控件的位置和分页控件的外观都有限制,例如,DataGrid控件不允许垂直放置分页控件。另一个能够发挥分页技术优势的控件是Repeater,Web开发者可以利用Repeater控件快速配置数据的显示方式,但分页功能却需要开发者自己实现。数据源在不断地变化,数据表现方式也千差万别,如果针对这些不断变动的条件分别定制分页控件,显然太浪费时间了,构造一个不限于特定表现控件的通用分页控件将极大地有利于节省时间。 

  一个优秀的通用数据控件不仅提供常规的分页功能,而且还要能够:

  ⑴ 提供“首页”、“上一页”、“下一页”、“末页”分页导航按钮。 

  ⑵ 根据数据显示情况调整自身的状态,即具有数据敏感性。如果分页控件被设置成每页显示10个记录,但实际上只有9个记录,那么分页控件不应该显示出来;在数据分成多页显示的情况下,第一个页面的“首页”、“上一页”按钮不应显示出来,最后一个页面的“下一页”、“末页”按钮也不应该显示出来。 

  ⑶ 不能依赖于特定的数据显示控件。 

  ⑷ 具有适应各种现有、将有数据源的能力。 

  ⑸ 应当能够方便地配置显示方式,轻松地集成到各种应用之中。 

  ⑹ 当分页就绪时,提醒其他控件。 

  ⑺ 即使是缺乏经验的Web设计者,也要能够毫无困难地使用。 

  ⑻ 提供有关分页信息的属性数据。 

  目前市场上已经有一些提供上述功能的商业性控件,不过都价格不菲。对于许多开发者来说,自己构造一个通用的分页控件是最理想的选择。 

  图一显示了本文通用分页控件的运行界面,其中用于显示的控件是一个Repeater控件。分页控件由两类部件构成:四个导航按钮,一组页面编号链接。 


图五 

  创建模板的第一步是在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控件改换成多用途的通用控件,数十倍地提高工作效率,本文的分页控件只是创建通用控件来满足现有和将来表现需要的其中一个例子而已。