GridView 自定义分页的简洁解决方案






4.57/5 (35投票s)
本文展示了如何轻松扩展 GridView 以支持自定义分页,并消除了使用 ObjectDataSource 作为数据源的限制。

引言
ASP.NET 2.0 中的 GridView 控件在绑定到包含比页面大小更多项的数据源时,提供了对标准分页的强大支持。在具有小型数据集的简单应用程序中,这通常是一个足够的解决方案。然而,在您需要显示大约 10000 条记录的较大项目中,使用标准的内置分页功能拉取所有数据以绑定到 GridView 效率低下。我们需要自定义分页并动态处理数据块。查看 CodeProject、MSDN 和 Google 上可用的解决方案,我发现了两种建议。第一种建议是提供一个自定义寻呼器,您将在其中创建自己的页面导航控件,处理所有事件并处理显示。第二种建议强调在 ObjectDataSource 中使用 SelectCountMethod 返回虚拟项计数,GridView 将使用它来设置其分页。这种建议似乎足够和优雅,但如果您的数据源不是 ObjectDataSource,它就无法工作。在我们当前的项目中,我们的数据源主要是通过业务服务层返回的 DataTable 或某些业务对象的 GenericContainer(例如 List<T>),因此 ObjectDataSource 解决方案不适用。许多人也遇到了同样的问题,别无选择,他们只好创建自己的寻呼器。这非常令人沮丧,如果您有过类似的经历,您将理解我的意思并真正欣赏本文。
我认为自定义分页的一个巧妙解决方案是允许像在 DataGrid 中那样设置 VirtualItemCount 属性,并且不限制用户只能使用 ObjectDataSource,同时充分利用 GridView 中已内置的分页显示和交互。这激发了我编写 PagingGridView 控件和本文。
本文有意侧重于 GridView 的分页显示和交互方面,而不是数据块的检索。但是,为了完整性,我包含了使用 SQL Server 2005 的 ROW_NUMBER() 功能从 SQL Server 2005 检索页面行块的示例。
Using the Code
如果您将 PagingGridView 包含在 ASP.NET Web 应用程序项目或被 ASP.NET 网站项目引用的类库中,PagingGridView 组件将出现在您的工具箱中。要将其添加到您的页面,您可以像使用任何其他 Web 控件一样拖放它。
或者,如果您希望手动将代码添加到 ASCX/ASPX 文件中,首先您必须在文件开头注册标签,并包含由 TagPrefix 限定的 PagingGridView 控件元素。请参阅下面的示例
<%@ Register Assembly="PagingGridView" 
             Namespace="Fadrian.Web.Control" TagPrefix="cc1" %> 
... 
<cc1:PagingGridView ID="PagingGridView2" runat="server"/>
PagingGridView 代码解释
实现上述分页功能实际上非常简单。实现的关键在于 InitializePager 方法。PagingGridView 覆盖此方法并检查 CustomPaging 是否已打开。如果自定义分页已打开,那么我们需要调整一些设置,以便寻呼器将正确呈现数据源中的虚拟项以及当前页索引。
protected override void InitializePager(GridViewRow row, 
          int columnSpan, PagedDataSource pagedDataSource)
{
    // This method is called to initialise the pager
    // on the grid. We intercepted this and override
    // the values of pagedDataSource to achieve
    // the custom paging using the default pager supplied
    if (CustomPaging)
    {
        pagedDataSource.AllowCustomPaging = true;
        pagedDataSource.VirtualCount = VirtualItemCount;
        pagedDataSource.CurrentPageIndex = CurrentPageIndex;
    }
    base.InitializePager(row, columnSpan, pagedDataSource);
}
PagingGridView 公开了一个公共属性 VirtualItemCount。此属性的默认值为 -1,用户可以将其设置为任何整数值。如果此值设置为除 -1 以外的任何值,则 CustomPaging 属性将返回 true 以指示此控件已启用 CustomPaging。
public int VirtualItemCount
{
    get 
    {
        if (ViewState["pgv_vitemcount"] == null)
            ViewState["pgv_vitemcount"] = -1;
        return Convert.ToInt32(ViewState["pgv_vitemcount"]);
    }
    set { ViewState["pgv_vitemcount"] = value; }
}
private bool CustomPaging
{
    get { return (VirtualItemCount != -1); }
}
此控件中有一个内部属性 CurrentPageIndex 用于存储当前页索引。这里提出的问题是为什么我们不直接使用 PageIndex?PageIndex 存储 GridView 的当前 PageIndex,但在自定义分页场景中,每次我们绑定新的数据源(调用 DataBind)时,如果数据源中的项数小于或等于 PageSize,PageIndex 将重置为 0。在我们的情况下,我们只拉取页面块数据,数据源中的项数总是与 PageSize 相同,因此 PageIndex 将始终重置。我们通过引入 CurrentPageIndex 解决了这个问题,并且每次设置 DataSource 时,我们都会捕获该值并将其存储到 ViewState。
private int CurrentPageIndex
{
    get
    {
        if (ViewState["pgv_pageindex"] == null)
            ViewState["pgv_pageindex"] = 0;
        return Convert.ToInt32(ViewState["pgv_pageindex"]);
    }
    set { ViewState["pgv_pageindex"] = value; }
} 
public override object DataSource 
{
   get { return base.DataSource; }
   set
   {
      base.DataSource = value;
      // we store the page index here so we dont lost it in databind
      CurrentPageIndex = PageIndex;
   }
}
数据源和分页数据
本文不打算深入探讨如何从数据库检索分页数据的详细信息;相反,它在此处提供信息是为了完整演示如何使用 PagingGridView 控件处理来自 SQL Server 2005 数据库的数据。为了使示例代码保持简单,所有查询都以代码形式编写,避免了 SQL 注入的风险或与使用存储过程相比的效率开销。
为了支持自定义分页,我们至少需要两件事:我们想要显示的总记录数(我们将其设置为 VirtualItemCount)和用于显示特定页面项的数据。下面的代码展示了 GetRowCount 方法,它只是一个简单的 SELECT COUNT (*) SQL 语句来检索行数。
GetDataPage 方法的实现是使用 SQL Server 2005 的 ROW_NUMBER() 功能,为要在网格上显示的特定页面检索特定的记录块。此方法中的 SQL 语句检索内部 Select 语句中按 ROW_NUM 排序的**顶部** x 条感兴趣的记录,而外部 Select 语句使用 WHERE 子句进一步过滤行。例如,如果我们要检索 PageIndex = 3 的记录块,并且 PageSize 设置为 20,则我们想要显示的记录块是第 61-80 行。使用相同的示例,当执行下面的代码中的 SQL 时,内部 Select 将检索“TOP 80”行,然后外部 Select 通过“ROW_NUM > 60”表达式过滤掉所有 <= 60 的行,以返回 20 条记录(第 61-80 行)。
有关使用 ROW_NUMBER() 的更多信息,请参阅 MSDN 或其他在线文章。
private const string demoConnString = 
     @"Integrated Security=SSPI;Persist Security Info=False;" + 
     @"Initial Catalog=NorthwindSQL;Data Source=localhost\SQLEXPRESS";
private const string demoTableName = "Customers";
private const string demoTableDefaultOrderBy = "CustomerID";
private int GetRowCount()
{
    using (SqlConnection conn = new SqlConnection(demoConnString))
    {
        conn.Open();
        SqlCommand comm = new SqlCommand(@"SELECT COUNT(*) FROM " + demoTableName, conn);
        int count = Convert.ToInt32(comm.ExecuteScalar());
        conn.Close();
        return count;
    }
} 
private DataTable GetDataPage(int pageIndex, int pageSize, string sortExpression)
{
    using (SqlConnection conn = new SqlConnection(demoConnString))
    {
        // We always need a default sort field for ROW_NUMBER() to work correctly
        if (sortExpression.Trim().Length == 0)
        sortExpression = demoTableDefaultOrderBy;
        conn.Open();
        string commandText = string.Format(
            "SELECT * FROM (select TOP {0} ROW_NUMBER() OVER (ORDER BY {1}) as ROW_NUM, * " 
            +"FROM {2} ORDER BY ROW_NUM) innerSelect WHERE ROW_NUM > {3}",
        ((pageIndex + 1) * pageSize), 
        sortExpression, 
        demoTableName,
        (pageIndex * pageSize)); 
        SqlDataAdapter adapter = new SqlDataAdapter(commandText, conn);
        DataTable dt = new DataTable();
        adapter.Fill(dt);
        conn.Close();
        dt.Columns.Remove("ROW_NUM");
        return dt;
    }
}
如果您想知道本文的示例数据从何而来,我通过打开 Northwind Access 数据库并使用 Upsizing 向导在我的 SQL Server 2005 Express 中创建了一个新的数据库(包含完整架构和数据),从而创建了 NorthwindSQL 数据库。您可以重复此过程以重新创建数据来测试代码,或者简单地修改 demoConnString、demoTableName 和 demoTableDefaultOrderBy 以反映您的数据存储。
实际应用
为了在您的目标页面中启用自定义分页,您必须在代码中设置 VirtualItemCount 或在 PagingGridView 控件的属性窗口中设置它。在下面的示例中,我们将 VirtualItemCount 设置为一个方法返回的值,该方法返回我们要检索的总记录的行数。
语法上,我们对 PagingGridView 的 DataSource 和 DataBind 进行编码,与 GridView 完全相同。您只需要清楚地记住,当我们分配 DataSource 时,无论是 DataTable 还是 GenericContainer,数据集都应只包含该页面的数据项;否则,我们只是在浪费我们为启用 CustomPaging 所做的所有努力 :)
protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
        PagingGridView1.VirtualItemCount = GetRowCount();
        BindPagingGrid();
    }
} 
protected void PagingGridView1_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    PagingGridView1.PageIndex = e.NewPageIndex;
    BindPagingGrid();
} 
private void BindPagingGrid()
{
    PagingGridView1.DataSource = GetDataPage(PagingGridView1.PageIndex,
    PagingGridView1.PageSize, PagingGridView1.OrderBy);
    PagingGridView1.DataBind();
}


