创建用于 ASP.NET 的分页程序控件

 
Dino Esposito
Wintellect

2003 年 10 月

适用于:
    Microsoft® ASP.NET

摘要:解决向任何 ASP.NET 控件添加分页功能的问题。还为开发复合 ASP.NET 控件提供了很多有用的提示和技巧。

下载本文的源代码(英文)。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)

从程序员的角度来看,Microsoft® SQL Server™ 查询的最大缺陷之一就是返回的行数通常比应用程序的用户界面实际可以容纳的行数要多得多。这种尴尬情形经常将开发人员陷于困境。开发人员是应该创建一个非常长的页面,让用户花时间去滚动浏览,还是应该通过设置一个手动分页机制来更好地解决这个问题?

哪种解决方案更好,在很大程度上取决于要检索的数据的特性。由多个项目(如搜索结果)组成的较长列表,最好通过各页大小相等、每页相对较短的多个页面显示。由单个项目(如文章的文本)组成的较长列表,如果整个插入在一个页面中,使用起来会更方便。最后得出的分析结果是,应该根据应用程序的总体用途来做决定。那么,Microsoft® ASP.NET 是如何解决数据分页问题的呢?

ASP.NET 提供了功能强大的数据绑定控件,以便将查询结果格式化为 HTML 标记。但是,这些数据绑定控件中只有一种控件(即 DataGrid 控件)本来就支持分页。其他控件(如 DataListRepeaterCheckBoxList)则不支持分页。这些控件及其他列表控件不支持分页,不是因为它们在结构上不支持分页,而是因为它们与 DataGrid 不同,不包含任何处理分页的特定代码。但是,处理分页的代码是相当样板化的,可以添加到所有这些控件中。

Scott Mitchell 在最近的一篇题目为“Creating a Pageable, Sortable DataGrid”(英文)的文章中,介绍了 DataGrid 分页。该文还引用了 Web 上的其他有用信息,为您提供了有关网格分页基础知识和其他信息。如果想查看如何使 DataList 控件可以进行分页的示例,可以查看此文章(英文)。该文演示了如何创建一个自定义的 DataList 控件,该控件具有当前索引和页面大小属性,并可以启动页面更改事件。

同样的代码也可以用于满足其他列表控件(如 ListBoxCheckBoxList)的分页需要。不过,向各个控件添加分页功能实际上并不是一种非常好的做法,因为,如上所述,分页代码是相当样板化的。因此,对于聪明的程序员来说,有什么方法比使用一种新的通用分页程序控件来实现所有这些控件的分页功能更好的呢?

本文中将建立一个分页程序控件,它将使合作者列表控件能够对 SQL Server 的查询结果进行分页。该控件名为 SqlPager,它支持两种类型的合作者控件 - 列表控件和基础数据列表控件。

SqlPager 控件的显著特点

SqlPager 控件是一个 ASP.NET 复合控件,包含一个单行表格。该行又包含两个单元格 - 导航条和页面描述符。该控件的用户界面呈条形,理想情况下,其宽度与合作者控件的宽度相同。导航条部分提供了可单击的元素,以便在页面之间移动;页面描述符部分为用户提供了有关当前显示的页面的一些反馈信息。

图 1:Visual Studio .NET 网页设计器中显示的 SqlPager 控件

DataGrid 控件的嵌入式分页程序一样,SqlPager 控件具有两种导航模式,即下一页/上一页和数字页面。此外,其特殊属性 PagerStyle 使您能够选择更方便的样式。该控件与列表控件协同工作。您可以通过 ControlToPaginate 字符串属性为分页程序指定一个这样的合作者控件。

SqlPager1.ControlToPaginate = "ListBox1"; 
        

一般情况下,分页程序首先获取 SQL Server 的查询结果,准备一个适当的记录页面,然后通过合作者控件的 DataSource 属性显示该页面。当用户单击以查看新页面时,分页程序将检索请求的数据并再次通过合作者控件来显示数据。分页机制对于列表控件是完全透明的。列表控件的数据源是通过编程方式进行更新的,任何时候都只包含适合当前页面的记录。

控件的分页引擎具有多个 public 属性,如 CurrentPageIndexItemsPerPagePageCount,通过这些属性来获取并设置当前页面的索引、每个页面的大小以及要显示的页面的总数。分页程序管理数据检索和分页所需的任何逻辑。

SelectCommand 属性设置获取数据所用的命令文本。ConnectionString 属性定义数据库的名称和位置以及连接凭据。执行查询时采用的方式取决于 PagingMode 属性的值。该属性的可能值为与其同名的 PagingMode 枚举的值 - CachedNonCached。如果选择 Cached 选项,则将使用数据适配器和 DataTable 对象检索整个结果集。可以选择将结果集放置在 ASP.NET 的 Cache 对象中,该结果集可以重复使用直到过期。如果选择 NonCached 选项,则查询只检索适合当前页面的记录。这时,ASP.NET 的 Cache 中不放置任何数据。NonCached 模式与 DataGrid 控件的自定义分页模式几乎相同。

下表列出 SqlPager 控件的全部编程接口。

表 1:SqlPager 控件的编程接口

名称 类型 说明
CacheDuration 属性 指示数据在 ASP.NET 的缓存中保留的秒数。只用于 Cached 模式。默认值为 60 秒。
ConnectionString 属性 用来访问所选择的 SQL Server 数据库的连接字符串。
ControlToPaginate 属性 同一个 .aspx 页面中的控件 ID,它将显示分页程序检索的记录页面。这是合作者控件。
CurrentPageIndex 属性 获取并设置基于 0 的页面索引。
ItemsPerPage 属性 获取并设置每页要显示的记录数量。默认值为每页显示 10 个项目。
PagerStyle 属性 该值指示分页程序用户界面的样式。它可以为 PagerStyle 枚举值:NextPrevNumericPages 之一。在 NextPrev 模式中,将显示 VCR 式的按钮,来转到第一页、上一页、下一页和最后一页。而在 NumericPages 模式中,将显示一个下拉列表,列出所有可用页面的索引。
PagingMode 属性 该值指示检索数据的方式。它可以为 PagingMode 枚举值:CachedNonCached 之一。如果为 Cached,则将使用数据适配器,且整个结果集将临时放置在 ASP.NET 缓存中。如果为 NonCached,则只检索当前页面中的记录。在这种情况下,不进行缓存。
SelectCommand 属性 用来进行查询的命令文本。最好为 SELECT-FROM-WHERE 形式。不支持 ORDER BY 子句。排序是通过 SortField 属性另外指定的。
SortField 属性 用来排序的字段的名称。此字段用于为查询提供动态的 ORDER BY 子句。排序是由 SQL Server 执行的。
ClearCache 方法 删除存储在 ASP.NET 缓存中的任何数据。
PageIndexChanged 事件 默认事件,当分页程序移动到另一个页面时发生。事件的数据结构为 PageChangedEventArgs 类,包含旧页面和新页面的索引。

由于 SqlPager 控件继承了 WebControl,因此它也具有很多与 UI 相关的属性来管理字体、边框和颜色。

生成 SqlPager 控件

将作为复合控件来生成 SqlPager 控件并让其继承 WebControl 类。复合控件是 ASP.NET 服务器控件所特有的,它是由一个或多个服务器控件组成。

public class SqlPager : WebControl, INamingContainer
{ ... }
        

除非生成完全自定义的控件或扩展现有的控件,否则,创建新控件时,大多数时间实际上是在生成复合控件。要创建 SqlPager,组合一个 Table 控件,并根据分页程序的样式,组合几个 LinkButton 控件或者一个 DropDownList 控件。

生成复合控件时,需要注意几条原则。首先,需要覆盖 CreateChildControls protected 方法。CreateChildControls 方法是从 Control 继承来的,当服务器控件为了显示而要创建子控件时或在返回后,将调用此方法。

protected override void CreateChildControls()
{
   // 清除现有的子控件及其 ViewState
   Controls.Clear();
   ClearChildViewState();

   // 生成控件树
   BuildControlHierarchy();
}
        

覆盖此方法时,需要执行几项重要的操作。创建并初始化任何所需的子控件实例并将它们添加到父控件的 Controls 集合中。但是,生成新控件树之前,应该删除任何现有的子控件并清除子控件可能留下的任何 ViewState 信息。

复合组件还需要实现 INamingContainer 接口,以便 ASP.NET 运行时可以为其创建一个新的命名范围。这就确保了复合控件中的所有控件都具有唯一的名称。这还将确保能够自动处理子控件的返回数据。

对于 SqlPager 控件来说,成为命名容器非常重要。事实上,SqlPager 包含一些 LinkButton 控件,并且需要获取并处理其单击事件以便导航页面。正如 ASP.NET 页面中的任何其他控件一样,LinkButton 也被赋予了一个 ID,用于标识处理返回事件的控件。

处理返回事件时,ASP.NET 运行时试图查找事件的目标 ID 与主窗体的任何直接子控件之间是否存在匹配关系。LinkButton 是分页程序的子控件,因此不能运行其服务器端的代码。这是否意味着只有窗体的直接子控件才能启动并处理服务器事件?当然不是,只要您使用命名容器。

通过使 SqlPager 控件实现 INamingContainer 接口,可以将嵌入式链接按钮的实际 ID 从“First”更改为“SqlPager1:First”。当用户单击以查看新页面时,返回事件将 SqlPager1:First 作为目标控件。实际上,运行时用来识别目标控件的算法比上面介绍的要复杂一些。运行时将事件目标控件的名称看作是用冒号分隔开的字符串。实际上,这种匹配是在窗体的子控件和用冒号分隔开的字符串(如 SqlPager1:First)的第一个标记之间进行的。由于分页程序是窗体的子控件,因此匹配时会成功,分页程序获取单击事件。如果您认为这种解释不够充分或者令人费解,只要下载 SqlPager 控件的源代码,删除 INamingContainer 标记接口并进行重新编译即可。您将看到分页程序能够返回,但不能内部处理单击事件。

INamingContainer 接口是一个不具备方法的标记接口,其实现只需在类声明中指定名称即可,无需进行任何其他操作。

复合控件的另一个重要方面是,它们通常不需要自定义逻辑来进行显示。复合控件的显示遵循其组成控件的显示。生成复合控件时,通常无需覆盖 Render 方法。

控件的 SqlPager 树由一个包含两个单元格的单行表格组成。该表格继承了分页程序的大部分可视设置,如前景颜色和背景颜色、边框、字体信息和宽度等。第一个单元格包含导航条,其结构取决于 PagerStyle 属性的值。如果分页程序的样式为 NextPrev,则导航条将由四个 VCR 式的按钮组成。否则,它将由一个下拉列表组成。

private void BuildControlHierarchy()
{
   // 生成环境表格(一行,两个单元格)
   Table t = new Table();
   t.Font.Name = this.Font.Name;
   t.Font.Size = this.Font.Size;
   t.BorderStyle = this.BorderStyle;
   t.BorderWidth = this.BorderWidth;
   t.BorderColor = this.BorderColor;
   t.Width = this.Width;
   t.Height = this.Height;
   t.BackColor = this.BackColor;
   t.ForeColor = this.ForeColor;

   // 生成表格中的行
   TableRow row = new TableRow();
   t.Rows.Add(row);

   // 生成带有导航条的单元格
   TableCell cellNavBar = new TableCell();
   if (PagerStyle == this.PagerStyle.NextPrev)
      BuildNextPrevUI(cellNavBar);
   else
      BuildNumericPagesUI(cellNavBar);
   row.Cells.Add(cellNavBar);

   // 生成带有页面索引的单元格
   TableCell cellPageDesc = new TableCell();
   cellPageDesc.HorizontalAlign = HorizontalAlign.Right;
   BuildCurrentPage(cellPageDesc);
   row.Cells.Add(cellPageDesc);

   // 将表格添加到控件树
   this.Controls.Add(t);
}
        

将各个控件添加到正确的 Controls 集合对于分页程序的正确显示极其重要。最外层的表格必须添加到分页程序的 Controls 集合中。链接按钮和下拉列表必须添加到相应表格单元格的 Controls 集合中。

下面给出了用来生成链接按钮导航条的代码。每个按钮都显示有一个 Webdings 字符,可以根据需要禁用,并被绑定到内部的 Click 事件处理程序。

private void BuildNextPrevUI(TableCell cell)
{
   bool isValidPage = ((CurrentPageIndex >=0) && 
                      (CurrentPageIndex <= TotalPages-1));
   bool canMoveBack = (CurrentPageIndex>0);
   bool canMoveForward = (CurrentPageIndex<TotalPages-1);

   // 显示 << 按钮
   LinkButton first = new LinkButton();
   first.ID = "First";
   first.Click += new EventHandler(first_Click);
   first.Font.Name = "webdings";
   first.Font.Size = FontUnit.Medium;
   first.ForeColor = ForeColor;
   first.ToolTip = "第一页";
   first.Text = "7";   
   first.Enabled = isValidPage && canMoveBack;
   cell.Controls.Add(first);
   :
}
        

分页程序的另一种样式(在下拉列表中列出数字页面)的生成方法如下所示:

private void BuildNumericPagesUI(TableCell cell)
{
   // 显示一个下拉列表
   DropDownList pageList = new DropDownList();
   pageList.ID = "PageList";
   pageList.AutoPostBack = true;
   pageList.SelectedIndexChanged += new EventHandler(PageList_Click);
   pageList.Font.Name = this.Font.Name;
   pageList.Font.Size = Font.Size;
   pageList.ForeColor = ForeColor;
   
   if (TotalPages <=0 || CurrentPageIndex == -1)
   {
      pageList.Items.Add("No pages");
      pageList.Enabled = false;
      pageList.SelectedIndex = 0; 
   }
   else // 填充列表
   {
      for(int i=1; i<=TotalPages; i++)
      {
         ListItem item = new ListItem(i.ToString(), (i-1).ToString());
         pageList.Items.Add(item);
      }
      pageList.SelectedIndex = CurrentPageIndex;
   }
}
        

所有事件处理程序(Click 和 SelectedIndexChanged)最终都会更改当前显示的页面。这两种方法都会调用一个公用的 private 方法 GoToPage

private void first_Click(object sender, EventArgs e)
{
   GoToPage(0);
}
private void PageList_Click(object sender, EventArgs e)
{
   DropDownList pageList = (DropDownList) sender;
   int pageIndex = Convert.ToInt32(pageList.SelectedItem.Value);
   GoToPage(pageIndex);
}
private void GoToPage(int pageIndex)
{
   // 准备事件数据
   PageChangedEventArgs e = new PageChangedEventArgs();
   e.OldPageIndex = CurrentPageIndex;
   e.NewPageIndex = pageIndex;

   // 更新当前的索引
   CurrentPageIndex = pageIndex;

   // 启动页面更改事件
   OnPageIndexChanged(e);

   // 绑定新数据
   DataBind();
}
        

其他导航按钮的处理程序与 first_Click 的区别仅在于它们传递给 GoToPage 方法的页码不同。GoToPage 方法负责处理 PageIndexChanged 事件,并负责启动数据绑定过程。它准备事件数据(旧页面和新页面索引)并触发事件。GoToPage 被定义为 private,但是可以使用 CurrentPageIndex 属性通过编程的方式更改显示的页面。

public int CurrentPageIndex
{
   get {return Convert.ToInt32(ViewState["CurrentPageIndex"]);}
   set {ViewState["CurrentPageIndex"] = value;}
}
        

与表 1 中所列的所有属性一样,CurrentPageIndex 属性的实现方法也相当简单。它将其内容保存到 ViewState 中并从中进行还原。在数据绑定过程中,会验证和使用页面索引。

数据绑定过程

DataBind 方法是所有 ASP.NET 控件公用的,对于数据绑定控件来说,它将触发用户界面的刷新以反映新数据。SqlPager 控件根据 SelectCommandConnectionString 属性的值,使用此方法启动数据检索操作。不言而喻,如果这些属性中的任何一个为空,该过程将终止。同样,如果合作者控件不存在,数据绑定过程将被取消。要查找合作者控件,DataBind 方法使用 Page 类中的 FindControl 函数。由此可见,合作者控件必须为主窗体的直接子控件。

进行分页显示的控件不能为任意的 ASP.NET 服务器控件。它必须为列表控件或基本数据列表控件。更一般来说,合作者控件必须具有 DataSource 属性并实现 DataBind 方法。可能进行分页的控件实际上只需要满足这些要求。Microsoft® .NET Framework 中所有继承 ListControlBaseDataList 的控件都满足第一个要求;而所有 Web 控件通过设计都满足 DataBind 要求。使用当前的实现方法,无法使用 SqlPager 控件来对 Repeater 进行分页。Repeater 与合作者控件 DataListDataGrid 不同,不继承 BaseDataList,也不提供列表控件的功能。下表列出了可以使用 SqlPager 进行分页的控件。

表 2:可以由 SqlPager 控件进行分页的数据绑定控件

控件 说明
CheckBoxList ListControl 派生而来,显示为复选框列表。
DropDownList ListControl 派生而来,显示为字符串下拉列表。
ListBox ListControl 派生而来,显示为字符串可滚动列表。
RadioButtonList ListControl 派生而来,显示为单选按钮列表。
DataList BaseDataList 派生而来,显示为模板化数据项目列表。
DataGrid BaseDataList 派生而来,显示为数据项目的表格网格。DataGrid 是唯一一个内置有功能强大的分页引擎的 ASP.NET 控件。

以下代码说明由 SqlPager 控件实现的数据绑定过程。

public override void DataBind()
{
   // 启动数据绑定事件
   base.DataBinding();

   // 数据绑定后必须重新创建控件
   ChildControlsCreated = false;

   // 确保控件存在且为列表控件
   _controlToPaginate = Page.FindControl(ControlToPaginate);
   if (_controlToPaginate == null)
      return;
   if (!(_controlToPaginate is BaseDataList || 
         _controlToPaginate is ListControl))
      return;

   // 确保具有足够的连接信息并指定查询
   if (ConnectionString == "" || SelectCommand == "")
      return;

   // 获取数据
   if (PagingMode == PagingMode.Cached)
      FetchAllData();
   else
      FetchPageData();

   // 将数据绑定到合作者控件
   BaseDataList baseDataListControl = null;
   ListControl listControl = null;
   if (_controlToPaginate is BaseDataList)
   {
       baseDataListControl = (BaseDataList) _controlToPaginate;
       baseDataListControl.DataSource = _dataSource; 
       baseDataListControl.DataBind();
       return;
   }
   if (_controlToPaginate is ListControl)
   {
       listControl = (ListControl) _controlToPaginate;
       listControl.Items.Clear(); 
       listControl.DataSource = _dataSource; 
       listControl.DataBind();
       return;
   }
}
        

根据 PagingMode 属性的值调用不同的获取例程。在任何情况下,结果集都绑定到 PagedDataSource 类的实例上。此类提供了一些用来对数据进行分页的功能。特别是,当整个数据集都存储在缓存中时,该类将自动检索当前页面的记录并返回布尔值,来提供有关第一页和最后一页的信息。稍后将回来介绍此类的内部结构。在上述列表中,帮助程序的 PagedDataSource 对象是由 _dataSource 变量表示的。

然后,SqlPager 控件经过计算得出合作者控件的类型,并将 PagedDataSource 对象的内容绑定到合作者控件的 DataSource 属性。

有时,上述的 DataBind 方法还将 ChildControlsCreated 属性重新设置为 false。那么,为什么要这样做呢?

当包含分页程序的页面返回时,所有控件都要重新创建;分页程序也不例外。通常情况下,所有控件及其子控件都是在准备显示页面之前创建的。在每个控件接收到 OnPreRender 通知之前的一瞬间,protected EnsureChildControls 方法将被调用,这样,每个控件都可以生成自己的控件树。此事件发生后,数据绑定过程完成,新数据已存储到缓存中。

但是,当由于单击分页程序的一个组成控件而使页面返回时(即用户单击以更改页面),就会生成分页程序的控件树,这时远未达到显示阶段。特别是,当处理相关的服务器端事件时,就必须生成控件树,因而是在数据绑定开始之前生成控件树。困难在于,数据绑定将修改页面索引,而这必须反映在用户界面中。如果不采取某些对策的话,当用户切换到另一页时,分页程序中的页面索引将不会被刷新。

有各种方法可以解决此问题,但重要的是要弄清楚问题及其真正的原因。您可以避免生成控件树,并在 Render 方法中生成所有输出。另外,您还可以修改树中受数据绑定更改影响的部分。本文选择了第三种方法,这种方法需要较少的代码,而且,不管控件的用户界面的哪个部分受到数据绑定更改的影响,都能够解决问题。通过将 ChildControlsCreated 属性设置为 false,可以使以前创建的任何控件树无效。这样,在显示之前将重新创建控件树。

分页引擎

SqlPager 控件支持两种检索数据的方法 - 缓存和非缓存。如果采用前者,选择命令原样执行,整个结果集将绑定到在内部进行分页的数据源对象上。PagedDataSource 对象将自动返回适合特定页面的记录。PagedDataSource 类也是在 DataGrid 默认分页机制背后运行的系统组件。

检索所有记录但只显示适合页面的几个记录,这通常不是一种明智的方法。由于 Web 应用程序的无状态特性,事实上,每次请求页面时都可能运行大量的查询。要使操作有效,采用缓存的数据检索方法必须依赖于某种缓存对象,ASP.NET 的 Cache 对象就是一个很好的候选对象。缓存技术的使用加快了应用程序的运行速度,但其提供的数据快照不能反映最新的更改。另外,它需要使用 Web 服务器上的较多内存。而且荒谬的是,如果大量的数据按会话缓存的话,甚至可能造成很大的问题。Cache 容器对于应用程序来说是全局的;如果数据按会话存储在其中,还需生成会话特有的项目名称。

Cache 对象的上面是完全支持过期策略的。换句话说,存储在缓存中的数据在一段时间后可以自动被释放。以下代码说明 SqlPager 类中用来获取数据并将其存储在缓存中的一个 private 方法。

private void FetchAllData()
{
   // 在 ASP.NET Cache 中查找数据
   DataTable data;
   data = (DataTable) Page.Cache[CacheKeyName];
   if (data == null)
   {
      // 使用 order-by 信息修改 SelectCommand
      AdjustSelectCommand(true);

      // 如果数据过期或从未被获取,则转到数据库
      SqlDataAdapter adapter = new SqlDataAdapter(SelectCommand, 
            ConnectionString);
      data = new DataTable();
      adapter.Fill(data);
      Page.Cache.Insert(CacheKeyName, data, null, 
           DateTime.Now.AddSeconds(CacheDuration), 
           System.Web.Caching.Cache.NoSlidingExpiration);
   }
      
   // 配置分页的数据源组件
   if (_dataSource == null)
      _dataSource = new PagedDataSource(); 
   _dataSource.DataSource = data.DefaultView;  
   _dataSource.AllowPaging = true;
   _dataSource.PageSize = ItemsPerPage;
   TotalPages = _dataSource.PageCount; 

   // 确保页面索引有效
   ValidatePageIndex();
   if (CurrentPageIndex == -1)
   {
      _dataSource = null;
      return;
   }

   // 选择要查看的页面
   _dataSource.CurrentPageIndex = CurrentPageIndex;
}
        

控件和请求的缓存项目名称是唯一的。它包括页面的 URL 和控件的 ID。在指定的时间(以秒计算)内,数据被绑定到缓存。要使项目过期,必须使用 Cache.Insert 方法。以下较简单的代码将项目添加到缓存,但不包括任何过期策略。

Page.Cache[CacheKeyName] = data;
        

PagedDataSource 对象通过其 DataSource 属性获取要进行分页的数据。值得注意的是,PagedDataSource 类的 DataSource 属性只接受 IEnumerable 对象。DataTable 不满足此要求,这就是为什么采取 DefaultView 属性的原因。

SelectCommand 属性确定在 SQL Server 数据库上运行的查询。此字符串最好为 SELECT-FROM-WHERE 形式。不支持 ORDER BY 子句,如果指定了该子句,将被删除。这正是 AdjustSelectCommand 方法所做的。使用 SortField 属性可以指定任何排序信息。AdjustSelectCommand 方法本身将根据 SortField 的值添加一个正确的 ORDER BY 子句。这样做有什么原因吗?

当分页程序以 NonCached 模式工作时,原始的查询将被修改以确保只检索当前页面的记录。在 SQL Server 上执行的实际查询文本将采取以下形式。

SELECT * FROM 
(SELECT TOP ItemsPerPage * FROM 
(SELECT TOP ItemsPerPage*CurrentPageIndex * FROM 
(SelectCommand) AS t0 
ORDER BY SortField ASC) AS t1
ORDER BY SortField DESC) AS t2 
ORDER BY SortField
        

该查询弥补了 SQL Server 2000 中 ROWNUM 子句的缺陷,并且对记录进行重新排序,使得只有 x 项目的“第 n 个”块经过正确排序后返回。您可以指定基础查询,分页程序将它分解为多个较小的页面。只有适合某个页面的记录被返回。正如您看到的那样,除了查询命令以外,上述查询需要处理排序字段。这就是为什么另外添加了 SortField 属性。此代码的唯一缺陷是默认情况为升序排序。通过使 ASC/DESC 关键字参数化,可以使此代码真的非常完美:

private void FetchPageData()
{
   // 需要经过验证的页面索引来获取数据。
   // 还需要实际的页数来验证页面索引。
   AdjustSelectCommand(false);
   VirtualRecordCount countInfo = CalculateVirtualRecordCount();
   TotalPages = countInfo.PageCount;

   // 验证页码(确保 CurrentPageIndex 有效或为“-1”)
   ValidatePageIndex();
   if (CurrentPageIndex == -1)
      return;

   // 准备并运行命令
   SqlCommand cmd = PrepareCommand(countInfo);
   if (cmd == null)
      return;
   SqlDataAdapter adapter = new SqlDataAdapter(cmd);
   DataTable data = new DataTable();
   adapter.Fill(data);

   // 配置分页的数据源组件
   if (_dataSource == null)
      _dataSource = new PagedDataSource(); 
   _dataSource.AllowCustomPaging = true;
   _dataSource.AllowPaging = true;
   _dataSource.CurrentPageIndex = 0;
   _dataSource.PageSize = ItemsPerPage;
   _dataSource.VirtualCount = countInfo.RecordCount;
   _dataSource.DataSource = data.DefaultView;   
}
        

NonCached 模式中,PagedDataSource 对象并不提供整个数据源,因此不能计算出要进行分页的总页数。进而必须对 AllowCustomPaging 属性进行标记,并提供数据源中的实际记录数量。通常,实际数量是使用 SELECT COUNT(*) 查询进行检索的。此模型与 DataGrid 的自定义分页几乎相同。此外,PagedDataSource 对象中选择的当前页面索引通常为 0,因为实际上已经存储了一页记录。

SqlPager 控件的实现方法就介绍到这里,下面我们介绍一下它的使用方法。

使用 SqlPager 控件

假设存在一个包含 ListBox 控件的示例页面。要使用分页程序,请确保 .aspx 页面正确地注册了该控件的程序集。

<%@ Register TagPrefix="expo" Namespace="DevCenter" Assembly="SqlPager" %>
        

控件的标记取决于实际设置的属性。以下标记是一个合理的示例:

<asp:listbox runat="server" id="ListBox1" 
     Width="300px" Height="168px"
     DataTextField="companyname" /> 
<br>
<expo:SqlPager runat="server" id="SqlPager1" Width="300px" 
   ControlToPaginate="ListBox1" 
   SelectCommand="SELECT customerid, companyname FROM customers" 
      ConnectionString="SERVER=localhost;DATABASE=northwind;UID=..."
      SortKeyField="companyname" />
<br>
<asp:button runat="server" id="LoadFirst1" Text="加载第一页" /> 
        

除了分页程序以外,页面还包含一个列表框和一个按钮。列表框将显示每个页面的内容;按钮只用于首次填充列表框。该按钮具有一个单击事件处理程序,定义如下。

private void LoadFirst1_Click(object sender, EventArgs e) {
   SqlPager1.CurrentPageIndex = 0;
   SqlPager1.DataBind(); 
}
        

图 2 显示操作中的页面。

图 2:与 ListBox 控件协同工作的 SqlPager 控件。

使用 DataList 控件可以生成一个更有意思的示例。目标是使用分页程序浏览每个 Northwind 职员的个人记录。该 DataList 如以下列表所示。

<asp:datalist runat="server" id="DataList1" Width="300px" 
     Font-Names="Verdana" Font-Size="8pt">
<ItemTemplate>
<table bgcolor="#f0f0f0" style="font-family:verdana;font-size:8pt;">
  <tr><td valign="top">
      <b><%# DataBinder.Eval(Container.DataItem, "LastName") + ", " + 
      DataBinder.Eval(Container.DataItem, "firstname") %></b></td></tr>
  
  <tr><td>
      <span style="color:blue;"><i>
      <%# DataBinder.Eval(Container.DataItem, "Title")%></i></span>
      <p><img style="float:right;" src='image.aspx?
          id=<%# DataBinder.Eval(Container.DataItem, "employeeid")%>' />
      <%# DataBinder.Eval(Container.DataItem, "Notes") %></td></tr>
</table>
</ItemTemplate>
</asp:datalist>
        

表格的第一行显示职员的姓名和职务,然后是相片,相片周围是注释。相片是使用特定的 .aspx 页面检索的,返回从数据库中获取的 JPEG 数据。

分页程序可以放置在页面中的任何位置。本例中将它放置在合作者 DataList 控件上方并紧挨着合作者控件。

图 3:SqlPager 对 DataList 控件进行分页

SqlPager 控件与 DataGrid 控件一起使用有意义吗?这要视情况而定。DataGrid 已经与一个基于本文中使用的 PagedDataSource 对象的嵌入式分页引擎一起工作。因此,如果您需要对以表格格式显示的单个记录集合进行分页时,就无需使用 SqlPager。但是,对于重要/复杂的方案,将这两个控件一起使用并不是一种牵强的想法。例如,如果您要向前一个屏幕快照添加一个 DataGrid 来显示由职员管理的订单,则可以在同一格页面上放置两个相关的分页引擎,一个要对职员进行分页,另一个用于滚动相关订单。

小结

不管您要生成哪种类型的应用程序(Web 应用程序、Microsoft® Windows® 应用程序或 Web 服务),您都很难下载和缓存要显示的整个数据源。有时,测试环境会使您相信这种解决方案真是太棒了,非常可取。但是测试环境也会误导。数据源的大小非常关键,应用程序的规模越大,数据源的大小越关键。

在 ASP.NET 中,只有 DataGrid 控件具有内置的分页功能。但是,分页引擎是由相当样板化的代码实现的,只要进行少量的处理,就可以进行推广并用于多个不同的控件。本文介绍的 SqlPager 控件就实现了此目标。它关注下载数据,将数据分成多个页面并通过合作者控件显示。该控件可以检索并缓存整个数据集,或者仅在 SQL Server 中查询要在选定的页面中显示的几个记录。我说的是 SQL Server,这是另一个重点。SqlPager 只能用于 SQL Server,不能用于通过 OLE DB 或 ODBC 来检索数据。也不能使用它访问 Oracle 或 DB2 存档。

要生成真正的通用 SQL 分页程序组件,应该形成数据访问层,并生成一种能够使用相应的数据提供程序创建连接、命令和适配器的工厂类。此外,要注意为各种 SQL 源设置一个分页引擎是最糟糕的做法。这里介绍的方法只适用于 SQL Server 7.0 和更高版本。TOP 子句在不同的环境下具有不同的特性。使用服务器游标和临时表格,它可适用于较大范围的 DBMS 系统。但那将使代码更为复杂。

作者简介

Dino Esposito 是一位来自意大利罗马的培训教师和顾问。他是 Wintellect(英文)小组的成员,专门研究 ASP.NET 和 ADO.NET,主要在欧洲和美国从事教学和咨询工作。此外,Dino 还负责管理 Wintellect 的 ADO.NET 课件,并为 MSDN Magazine(英文)的“Cutting Edge”专栏撰写文章。

posted @ 2006-08-20 21:30  Hermes.Liu  阅读(947)  评论(5编辑  收藏  举报