通过大量数据有效分页 (C#)

作者 :Scott Mitchell

下载 PDF

处理大量数据时,数据表示控件的默认分页选项不适用,因为其基础数据源控件会检索所有记录,即使只显示一部分数据。 在这种情况下,我们必须转向自定义分页。

简介

如前面的教程中所述,分页可以通过以下两种方式之一实现:

  • 只需选中数据 Web 控件智能标记中的“启用分页”选项即可实现默认分页;但是,每当查看数据页时,ObjectDataSource 都会检索所有记录,即使页面中只显示其中一部分记录
  • 自定义分页 仅检索数据库中需要为用户请求的特定数据页显示的记录,从而提高默认分页的性能;但是,与默认分页相比,自定义分页的实现工作量要多一点

由于易于实现,只需检查一个复选框即可完成! 默认分页是一个有吸引力的选项。 不过,检索所有记录的天真方法使得在分页足够大量的数据或具有许多并发用户的站点时,这是一个难以置信的选择。 在这种情况下,我们必须转向自定义分页,以提供响应系统。

自定义分页的挑战是能够编写一个查询,该查询返回特定数据页所需的精确记录集。 幸运的是,Microsoft SQL Server 2005 为排名结果提供了新的关键字 (keyword) ,使我们能够编写可以有效检索记录子集的查询。 本教程介绍如何使用此新SQL Server 2005 关键字 (keyword) 在 GridView 控件中实现自定义分页。 虽然自定义分页的用户界面与默认分页的用户界面相同,但使用自定义分页从一个页面单步执行到下一个页面的速度可能比默认分页快几个数量级。

注意

自定义分页所表现出的确切性能提升取决于正在分页的记录总数以及数据库服务器上放置的负载。 在本教程结束时,我们将介绍一些粗略的指标,这些指标展示了通过自定义分页获得的性能优势。

步骤 1:了解自定义分页过程

分页浏览数据时,页面中显示的精确记录取决于所请求的数据页以及每页显示的记录数。 例如,假设我们想要分页浏览 81 个产品,每页显示 10 个产品。 查看第一页时,我们希望产品 1 到 10:查看第二页时,我们会对产品 11 到 20 等感兴趣。

有三个变量指示需要检索哪些记录以及如何呈现分页接口:

  • 开始行 索引 要显示的数据页中第一行的索引;可以通过将页面索引乘以每页显示的记录并添加一个来计算此索引。 例如,一次分页浏览记录 10 时,对于页面索引为 0) 的第一页 (,起始行索引为 0 * 10 + 1 或 1;对于第二页 (其页索引为 1) ,则起始行索引为 1 * 10 + 1 或 11。
  • 最大行 数:每页要显示的最大记录数。 此变量称为最大行数,因为对于最后一页,返回的记录可能比页面大小少。 例如,分页浏览 81 个产品每页 10 条记录时,第九页和最后一页将只有一条记录。 但是,任何页面都不会显示超过“最大行数”值的记录数。
  • 总记录 计数 正在分页的记录总数。 虽然不需要此变量来确定要为给定页面检索哪些记录,但它确实指示分页接口。 例如,如果有 81 个产品正在分页,则分页界面知道在分页 UI 中显示 9 个页码。

使用默认分页时,起始行索引计算为页面索引和页面大小加 1 的乘积,而“最大行数”只是页面大小。 由于默认分页在呈现任何数据页时从数据库中检索所有记录,因此每行的索引是已知的,因此,移动到“开始行索引”行是一项简单任务。 此外,“总记录计数”随时可用,因为它只是 DataTable (或任何用于保存数据库结果) 对象中的记录数。

给定起始行索引和最大行数变量,自定义分页实现必须仅返回从起始行索引开始的精确记录子集,之后最多返回最大行数的记录数。 自定义分页提供两个挑战:

  • 我们必须能够有效地将行索引与正在分页的整个数据中的每一行相关联,以便我们可以开始在指定的起始行索引处返回记录
  • 我们需要提供正在分页的记录总数

在接下来的两个步骤中,我们将检查响应这两个挑战所需的 SQL 脚本。 除了 SQL 脚本,我们还需要在 DAL 和 BLL 中实现方法。

步骤 2:返回正在分页的记录总数

在了解如何检索所显示页面的记录的精确子集之前,让我们先看一下如何返回正在分页的记录总数。 需要此信息才能正确配置分页用户界面。 可以使用聚合函数获取特定 SQL 查询返回的COUNT记录总数。 例如,若要确定表中的记录 Products 总数,可以使用以下查询:

SELECT COUNT(*)
FROM Products

让我们向 DAL 添加一个返回此信息的方法。 具体而言,我们将创建一个名为 的 TotalNumberOfProducts() DAL 方法,该方法执行上面所示的 SELECT 语句。

首先,打开 文件夹中的 Northwind.xsd Typed DataSet 文件 App_Code/DAL 。 接下来,右键单击ProductsTableAdapterDesigner并选择“添加查询”。 正如我们在前面的教程中看到的,这将允许我们向 DAL 添加新方法,在调用该方法时,该方法将执行特定的 SQL 语句或存储过程。 与前面的教程中的 TableAdapter 方法一样,对于此方法,请选择使用即席 SQL 语句。

使用临时 SQL 语句

图 1:使用即席 SQL 语句

在下一个屏幕上,我们可以指定要创建的查询类型。 由于此查询将返回单个标量值,因此表中的记录 Products 总数选择 SELECT 返回单一值选项的 。

将查询配置为使用返回单个值的 SELECT 语句

图 2:将查询配置为使用返回单个值的 SELECT 语句

指示要使用的查询类型后,接下来必须指定查询。

使用 SELECT COUNT (*) FROM Products 查询

图 3:使用 SELECT COUNT (*) FROM Products 查询

最后,指定 方法的名称。 如前所述,让我们使用 TotalNumberOfProducts

将 DAL 方法命名为 TotalNumberOfProducts

图 4:将 DAL 方法命名为 TotalNumberOfProducts

单击“完成”后,向导会将 方法添加到 TotalNumberOfProducts DAL。 DAL 中返回标量的方法返回可以为 null 的类型,以防 SQL 查询的结果为 NULL。 但是,我们的 COUNT 查询将始终返回非NULL 值;无论,DAL 方法都将返回可为 null 的整数。

除了 DAL 方法外,我们还需要在 BLL 中使用 方法。 ProductsBLL打开类文件并添加一个TotalNumberOfProducts方法,该方法只需向下调用 DAL 方法TotalNumberOfProducts

public int TotalNumberOfProducts()
{
    return Adapter.TotalNumberOfProducts().GetValueOrDefault();
}

DAL 方法 TotalNumberOfProducts 返回可为 null 的整数;但是,我们创建了 ProductsBLL 类 s TotalNumberOfProducts 方法,以便它返回标准整数。 因此,我们需要让 ProductsBLL 类的 方法 TotalNumberOfProducts 返回 DAL 方法 TotalNumberOfProducts 返回的可为 null 整数的值部分。 对 GetValueOrDefault() 的调用返回可为空整数的值(如果存在);但是,如果可为 null 整数为 null,则返回默认整数值 0。

步骤 3:返回记录的精确子集

下一个任务是在 DAL 和 BLL 中创建接受前面讨论的起始行索引和最大行数变量的方法,并返回相应的记录。 在执行此操作之前,让我们先看一下所需的 SQL 脚本。 我们面临的挑战是,我们必须能够有效地为正在分页的整个结果中的每一行分配索引,以便我们可以仅返回从起始行索引 (开始到最大记录数) 记录数的记录。

如果数据库表中已有用作行索引的列,则这不是一个挑战。 乍一看,我们可能认为 Products 表字段 ProductID 就足够了,因为第一个产品具有 ProductID 1,第二个产品为 2,依此而行。 但是,删除产品会在序列中留下一个空白,使此方法为空。

有两种常规方法可用于有效地将行索引与要分页通过的数据相关联,从而允许检索记录的精确子集:

  • 使用 2005 SQL Server 2005 SQL ServerROW_NUMBER()新关键字ROW_NUMBER()关键字 (keyword) 根据某些顺序将排名与返回的每个记录相关联。 此排名可用作每行的行索引。

  • 使用表变量和SET ROWCOUNTSQL Server 语句SET ROWCOUNT可用于指定查询在终止前应处理的总记录数;表变量是本地 T-SQL 变量,可以保存表格数据,类似于临时表。 此方法同样适用于 Microsoft SQL Server 2005 和 SQL Server 2000 (,而ROW_NUMBER()此方法仅适用于 SQL Server 2005) 。

    此处的想法是创建一个表变量,该变量具有一个 IDENTITY 列和列,用于对表进行分页访问其数据的主键。 接下来,将数据分页的表的内容转储到表变量中,从而通过 IDENTITY 表中每个记录的列) 将连续行索引 (关联。 填充表变量后, SELECT 可以执行表变量上的语句(与基础表联接)来提取特定记录。 语句 SET ROWCOUNT 用于智能地限制需要转储到表变量中的记录数。

    此方法的效率基于所请求的页码,因为 SET ROWCOUNT 为该值分配了“开始行索引”和“最大行数”的值。 在对低编号页面(例如前几页数据)进行分页时,此方法非常高效。 但是,在检索接近尾部的页面时,它会表现出类似于分页的默认性能。

本教程使用 ROW_NUMBER() 关键字 (keyword) 实现自定义分页。 有关使用表变量和技术 SET ROWCOUNT 的详细信息,请参阅 高效分页处理大量数据

ROW_NUMBER() 关键字 (keyword) 使用以下语法将排名与按特定顺序返回的每个记录相关联:

SELECT columnList,
       ROW_NUMBER() OVER(orderByClause)
FROM TableName

ROW_NUMBER() 返回一个数值,该值指定与指示的排序相关的每条记录的排名。 例如,若要查看从最昂贵到最低价格排序的每个产品的排名,可以使用以下查询:

SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
FROM Products

图 5 显示了在 Visual Studio 中通过查询窗口运行时的此查询结果。 请注意,产品按价格排序,以及每行的价格排名。

包含每个返回记录的价格排名

图 5:包含每个返回记录的价格排名

注意

ROW_NUMBER()只是SQL Server 2005 年可用的众多新排名函数之一。 有关 以及其他排名函数的ROW_NUMBER()更深入的讨论,请阅读使用 Microsoft SQL Server 2005 返回排名结果

在上面的示例) 中OVER,按 子句中的UnitPrice指定ORDER BY列对结果 (进行排名时,SQL Server必须对结果进行排序。 如果列上有一个聚集索引 (,) 结果按顺序排序,或者存在覆盖索引,则这是一个快速操作,但否则成本可能更高。 为了帮助提高足够大的查询的性能,请考虑为结果排序依据的列添加非聚集索引。 有关性能注意事项的详细信息,请参阅 SQL Server 2005 中的排名函数和性能

返回的 ROW_NUMBER() 排名信息不能直接在 WHERE 子句中使用。 但是,派生表可用于返回 ROW_NUMBER() 结果,结果随后会显示在 子句中 WHERE 。 例如,以下查询使用派生表返回 ProductName 和 UnitPrice 列以及 ROW_NUMBER() 结果,然后使用 WHERE 子句仅返回价格排名在 11 和 20 之间的产品:

SELECT PriceRank, ProductName, UnitPrice
FROM
   (SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
    FROM Products
   ) AS ProductsWithRowNumber
WHERE PriceRank BETWEEN 11 AND 20

进一步扩展此概念,我们可以利用此方法检索特定的数据页,给定所需的起始行索引和最大行值:

SELECT PriceRank, ProductName, UnitPrice
FROM
   (SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
    FROM Products
   ) AS ProductsWithRowNumber
WHERE PriceRank > <i>StartRowIndex</i> AND
    PriceRank <= (<i>StartRowIndex</i> + <i>MaximumRows</i>)

注意

如本教程稍后所述,StartRowIndexObjectDataSource 提供的 从零开始编制索引,而 ROW_NUMBER() SQL Server 2005 返回的值从 1 开始编制索引。 因此, WHERE 子句返回严格大于StartRowIndex且小于或等于 StartRowIndex + MaximumRows的记录。PriceRank

既然我们讨论了如何在 ROW_NUMBER() 给定起始行索引和最大行值的情况下检索特定数据页,现在需要将此逻辑作为 DAL 和 BLL 中的方法实现。

创建此查询时,必须确定结果的排名顺序;让我们按名称按字母顺序对产品进行排序。 这意味着,使用本教程中的自定义分页实现,我们将无法创建自定义分页报表,但也可以进行排序。 不过,在下一教程中,我们将了解如何提供此类功能。

在上一部分中,我们创建了 DAL 方法作为即席 SQL 语句。 遗憾的是,TableAdapter 向导使用的 Visual Studio 中的 T-SQL 分析程序不喜欢 OVER 函数使用的 ROW_NUMBER() 语法。 因此,我们必须将此 DAL 方法创建为存储过程。 从“视图”菜单中选择“服务器资源管理器” (或按 Ctrl+Alt+S) 并展开 NORTHWND.MDF 节点。 若要添加新存储过程,请右键单击“存储过程”节点并选择“添加新存储过程” (请参阅图 6) 。

添加新存储过程以分页浏览产品

图 6:添加新存储过程以分页浏览产品

此存储过程应接受两个整数输入参数 - 并使用@maximumRows按字段排序的函数,仅返回大于指定@startRowIndex且小于或等于 + @maximumRow@startRowIndexs 的行。ProductNameROW_NUMBER()@startRowIndex 在新存储过程中输入以下脚本,然后单击“保存”图标将存储过程添加到数据库。

CREATE PROCEDURE dbo.GetProductsPaged
(
    @startRowIndex int,
    @maximumRows int
)
AS
    SELECT     ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
               UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
               CategoryName, SupplierName
FROM
   (
       SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
              UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
              (SELECT CategoryName
               FROM Categories
               WHERE Categories.CategoryID = Products.CategoryID) AS CategoryName,
              (SELECT CompanyName
               FROM Suppliers
               WHERE Suppliers.SupplierID = Products.SupplierID) AS SupplierName,
              ROW_NUMBER() OVER (ORDER BY ProductName) AS RowRank
        FROM Products
    ) AS ProductsWithRowNumbers
WHERE RowRank > @startRowIndex AND RowRank <= (@startRowIndex + @maximumRows)

创建存储过程后,请花点时间对其进行测试。在服务器 GetProductsPaged 资源管理器中右键单击存储过程名称,然后选择“执行”选项。 然后,Visual Studio 将提示输入参数, @startRowIndex@maximumRow (请参阅图 7) 。 尝试不同的值并检查结果。

输入 <span class=@startRowIndex and @maximumRows Parameters 的值“/>

图 7:输入 @startRowIndex 和 @maximumRows 参数的值

选择这些输入参数值后,“输出”窗口将显示结果。 图 8 显示了为 和 @maximumRows 参数传入 10 @startRowIndex 时的结果。

将返回将显示在第二页数据中的记录

图 8:单击查看 全尺寸图像 (返回第二页数据中显示的记录)

创建此存储过程后,我们已准备好创建 ProductsTableAdapter 方法。 打开 Northwind.xsd “类型化数据集”,右键单击 , ProductsTableAdapter然后选择“添加查询”选项。 使用现有存储过程创建查询,而不是使用即席 SQL 语句创建查询。

使用现有存储过程创建 DAL 方法

图 9:使用现有存储过程创建 DAL 方法

接下来,系统会提示选择要调用的存储过程。 GetProductsPaged从下拉列表中选择存储过程。

从 Drop-Down 列表中选择 GetProductsPaged 存储过程

图 10:从 Drop-Down 列表中选择 GetProductsPaged 存储过程

然后,下一个屏幕会询问存储过程返回哪些类型的数据:表格数据、单个值或无值。 GetProductsPaged由于存储过程可以返回多个记录,因此指示它返回表格数据。

指示存储过程返回表格数据

图 11:指示存储过程返回表格数据

最后,指示要创建的方法的名称。 与前面的教程一样,请继续使用填充 DataTable 和返回 DataTable 创建方法。 将第一个方法和 FillPaged 第二个 GetProductsPaged命名为 。

将方法命名为 FillPaged 和 GetProductsPaged

图 12:将方法命名为 FillPaged 和 GetProductsPaged

除了创建 DAL 方法以返回产品的特定页面外,我们还需要在 BLL 中提供此类功能。 与 DAL 方法一样,BLL 的 GetProductsPaged 方法必须接受两个整数输入来指定起始行索引和最大行数,并且必须仅返回位于指定范围内的记录。 在 ProductsBLL 类中创建此类 BLL 方法,该方法仅向下调用 DAL s GetProductsPaged 方法,如下所示:

[System.ComponentModel.DataObjectMethodAttribute(
    System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsPaged(int startRowIndex, int maximumRows)
{
    return Adapter.GetProductsPaged(startRowIndex, maximumRows);
}

可以为 BLL 方法的输入参数使用任何名称,但正如我们稍后将看到的,在将 ObjectDataSource 配置为使用此方法时,选择使用 startRowIndexmaximumRows 保存我们额外的工作。

步骤 4:将 ObjectDataSource 配置为使用自定义分页

完成用于访问特定记录子集的 BLL 和 DAL 方法后,我们准备创建一个 GridView 控件,该控件使用自定义分页浏览其基础记录。 首先打开 文件夹中的页面EfficientPaging.aspxPagingAndSorting,向页面添加 GridView,并将其配置为使用新的 ObjectDataSource 控件。 在我们过去的教程中,我们经常将 ObjectDataSource 配置为使用 ProductsBLL 类 s GetProducts 方法。 但是,这一次,我们希望改用 GetProductsPaged 方法,因为 GetProducts 该方法返回数据库中 的所有 产品,而 GetProductsPaged 只返回记录的特定子集。

将 ObjectDataSource 配置为使用 ProductsBLL 类 GetProductsPaged 方法

图 13:将 ObjectDataSource 配置为使用 ProductsBLL 类 getProductsPaged 方法

由于我们重新创建只读 GridView,请花点时间将“插入”、“更新”和“删除”选项卡中的方法下拉列表设置为 (None) 。

接下来,ObjectDataSource 向导会提示我们输入方法 的startRowIndexGetProductsPagedmaximumRows输入参数值。 这些输入参数实际上将由 GridView 自动设置,因此只需将源设置为“无”并单击“完成”即可。

将输入参数源保留为“无”

图 14:将输入参数源保留为“无”

完成 ObjectDataSource 向导后,GridView 将包含每个产品数据字段的 BoundField 或 CheckBoxField。 根据需要随意定制 GridView 外观。 我已选择仅ProductName显示 、、CategoryNameSupplierNameQuantityPerUnitUnitPrice BoundFields。 此外,通过选中 GridView 智能标记中的“启用分页”复选框,将 GridView 配置为支持分页。 进行这些更改后,GridView 和 ObjectDataSource 声明性标记应如下所示:

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True">
    <Columns>
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category"
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier"
            SortExpression="SupplierName" />
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}"
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProductsPaged"
    TypeName="ProductsBLL">
    <SelectParameters>
        <asp:Parameter Name="startRowIndex" Type="Int32" />
        <asp:Parameter Name="maximumRows" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

但是,如果通过浏览器访问页面,则找不到 GridView。

未显示 GridView

图 15:未显示 GridView

缺少 GridView,因为 ObjectDataSource 当前使用 0 作为 和 maximumRows 输入参数的值。GetProductsPagedstartRowIndex 因此,生成的 SQL 查询不返回任何记录,因此不显示 GridView。

若要解决此问题,我们需要将 ObjectDataSource 配置为使用自定义分页。 这可以通过以下步骤完成:

  1. 将 ObjectDataSource 属性 EnablePaging 设置为此, true 向 ObjectDataSource 指示它必须传递给 SelectMethod 两个附加参数:一个用于指定起始行索引 (StartRowIndexParameterName) ,另一个用于指定最大行数 (MaximumRowsParameterName) 。
  2. 设置 ObjectDataSource 和 StartRowIndexParameterNameMaximumRowsParameterName Properties 相应地StartRowIndexParameterNameMaximumRowsParameterName 属性指示传入 SelectMethod 的输入参数的名称,以便进行自定义分页。 默认情况下,这些参数名称为 startIndexRowmaximumRows,这就是为什么在 BLL 中创建 GetProductsPaged 方法时,我将这些值用于输入参数。 如果选择对 BLL 方法GetProductsPaged使用不同的参数名称,例如 startIndexmaxRows,则需要相应地设置 ObjectDataSource 和MaximumRowsParameterNameStartRowIndexParameterName属性 (例如 startIndex for StartRowIndexParameterName 和 maxRows for MaximumRowsParameterName) 。
  3. 将 ObjectDataSource 的 SelectCountMethod 属性 设置为方法的名称,该方法返回正在分页的记录总数 (TotalNumberOfProducts) 回想一下 ProductsBLL ,类方法 TotalNumberOfProducts 使用执行 SELECT COUNT(*) FROM Products 查询的 DAL 方法返回正在分页的记录总数。 ObjectDataSource 需要此信息才能正确呈现分页接口。
  4. 通过向导配置 ObjectDataSource 时,从 ObjectDataSource 的声明性标记中删除 和 maximumRows<asp:Parameter> Elements,Visual Studio 会自动为方法的输入参数添加了两个元素。startRowIndex<asp:Parameter>GetProductsPaged 通过将 设置为 EnablePagingtrue,将自动传递这些参数;如果这些参数也出现在声明性语法中,则 ObjectDataSource 将尝试将 四个 参数传递给 GetProductsPaged 方法,将两个参数传递给 TotalNumberOfProducts 方法。 如果忘记删除这些<asp:Parameter>元素,则通过浏览器访问页面时,会收到如下错误消息:ObjectDataSource“ObjectDataSource1”找不到具有参数:startRowIndex、maximumRows 的非泛型方法“TotalNumberOfProducts”。

进行这些更改后,ObjectDataSource 的声明性语法应如下所示:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL"
    SelectMethod="GetProductsPaged" EnablePaging="True"
    SelectCountMethod="TotalNumberOfProducts">
</asp:ObjectDataSource>

请注意, EnablePaging 已设置 和 SelectCountMethod 属性, <asp:Parameter> 并且元素已被删除。 图 16 显示了进行这些更改后属性窗口的屏幕截图。

若要使用自定义分页,请配置 ObjectDataSource 控件

图 16:若要使用自定义分页,请配置 ObjectDataSource 控件

进行这些更改后,请通过浏览器访问此页面。 应会看到列出 10 个产品,按字母顺序排序。 花点时间逐页浏览数据。 尽管从最终用户的角度来看,默认分页和自定义分页之间没有视觉差异,但自定义分页通过大量数据更高效地分页,因为它只检索需要为给定页面显示的记录。

数据按产品名称排序,使用自定义分页进行分页

图 17:按产品名称排序的数据使用自定义分页 (单击查看全尺寸图像)

注意

使用自定义分页时,ObjectDataSource 返回的页面 SelectCountMethod 计数值存储在 GridView 的视图状态中。 其他 GridView 变量 PageIndexEditIndexSelectedIndexDataKeys 集合等存储在控件状态中,无论 GridView EnableViewState 属性的值如何,该状态都会持久保存。 PageCount由于该值使用视图状态跨回发保留,因此在使用包含指向最后一页的链接的分页接口时,必须启用 GridView 的视图状态。 (如果分页界面不包含指向最后一页的直接链接,则可以禁用 view state.)

单击最后一个页面链接会导致回发,并指示 GridView 更新其 PageIndex 属性。 如果单击最后一个页面链接,GridView 会将其 PageIndex 属性分配给一个小于其 PageCount 属性的值。 禁用视图状态后 PageCount ,该值在回发中丢失, PageIndex 而是为 分配最大整数值。 接下来,GridView 尝试通过将 和 PageCount 属性相乘PageSize来确定起始行索引。 这会导致 , OverflowException 因为积超出了允许的最大整数大小。

实现自定义分页和排序

我们当前的自定义分页实现要求在创建 GetProductsPaged 存储过程时静态指定数据的分页顺序。 但是,你可能已注意到 GridView 智能标记除了“启用分页”选项外,还包含“启用排序”复选框。 遗憾的是,使用当前的自定义分页实现向 GridView 添加排序支持只会对当前查看的数据页上的记录进行排序。 例如,如果将 GridView 配置为也支持分页,则在查看数据的第一页时,按产品名称降序排序,则会反转第 1 页产品的顺序。 如图 18 所示,此类显示,当按反向字母顺序排序时,卡纳冯老虎作为第一个产品,这忽略了卡纳文老虎之后的其他 71 个产品,按字母顺序排列:排序中仅考虑第一页上的这些记录。

仅对当前页上显示的数据进行排序

图 18:仅对当前页上显示的数据进行排序 (单击以查看全尺寸图像)

排序仅适用于数据的当前页,因为排序是在从 BLL s GetProductsPaged 方法检索数据之后进行的,此方法仅返回特定页面的这些记录。 若要正确实现排序,我们需要将排序表达式传递给 方法, GetProductsPaged 以便在返回特定数据页之前可以适当地对数据进行排名。 我们将在下一教程中了解如何完成此操作。

实现自定义分页和删除

如果在使用自定义分页技术对数据进行分页的 GridView 中启用删除功能,你会发现在从最后一页中删除最后一条记录时,GridView 会消失,而不是适当地递减 GridView s PageIndex。 若要重现此 bug,请为刚刚创建的教程启用删除。 转到最后一页 (第 9 页) ,在该页中应看到单个产品,因为我们一次分页 81 个产品,一次 10 个产品。 删除此产品。

删除最后一个产品后,GridView 自动转到第八页,并且此类功能将随默认分页一起显示。 但是,使用自定义分页时,在最后一页上删除最后一个产品后,GridView 只会从屏幕上完全消失。 发生这种情况的确切 原因 有点超出本教程的范围:有关此问题来源的低级别详细信息,请参阅 使用自定义分页从 GridView 删除最后一页的最后一条记录 。 总之,这是由于单击“删除”按钮时 GridView 执行的以下步骤序列造成的:

  1. 删除记录
  2. 获取要为指定的 PageIndex 和 显示的相应记录 PageSize
  3. 检查以确保 PageIndex 不超过数据源中的数据页数;如果超出,则会自动递减 GridView 的 PageIndex 属性
  4. 使用步骤 2 中获取的记录将相应的数据页绑定到 GridView

问题源于以下事实:在步骤 2 PageIndex 中,在抓取要显示的记录时使用的 仍然是 PageIndex 刚刚删除其唯一记录的最后一页的 。 因此,在步骤 2 中, 不会返回任何 记录,因为最后一页的数据不再包含任何记录。 然后,在步骤 3 中,GridView 会意识到其 PageIndex 属性大于数据源 (中的总页数,因为我们删除了最后一页中的最后一条记录) ,因此会递减其 PageIndex 属性。 在步骤 4 中,GridView 尝试将自身绑定到在步骤 2 中检索到的数据;但是,在步骤 2 中未返回任何记录,因此导致 GridView 为空。 使用默认分页时,此问题不会显现,因为在步骤 2 中 ,所有 记录都是从数据源检索的。

若要解决此问题,我们有两个选项。 第一种是为 GridView 事件处理程序 RowDeleted 创建事件处理程序,用于确定刚刚删除的页面中显示的记录数。 如果只有一条记录,则刚删除的记录必须是最后一条记录,我们需要递减 GridView s PageIndex。 当然,如果删除操作实际成功,我们只想更新 PageIndex ,这可以通过确保 e.Exception 属性为 null来确定。

此方法之所以有效,是因为它在步骤 1 之后、步骤 2 之前更新 PageIndex 。 因此,在步骤 2 中,返回相应的记录集。 若要实现此目的,请使用如下所示的代码:

protected void GridView1_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
    // If we just deleted the last row in the GridView, decrement the PageIndex
    if (e.Exception == null && GridView1.Rows.Count == 1)
        // we just deleted the last row
        GridView1.PageIndex = Math.Max(0, GridView1.PageIndex - 1);
}

另一种解决方法是为 ObjectDataSource 事件 RowDeleted 创建事件处理程序,并将 属性设置为 AffectedRows 值 1。 删除步骤 1 中的记录后 (但在重新检索步骤 2) 中的数据之前,如果一行或多行受操作影响,GridView 会更新其 PageIndex 属性。 但是, AffectedRows 属性不是由 ObjectDataSource 设置的,因此省略此步骤。 执行此步骤的一种方法是在删除操作成功完成时手动设置 AffectedRows 属性。 可以使用如下所示的代码来实现此目的:

protected void ObjectDataSource1_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    // If we get back a Boolean value from the DeleteProduct method and it's true,
    // then we successfully deleted the product. Set AffectedRows to 1
    if (e.ReturnValue is bool && ((bool)e.ReturnValue) == true)
        e.AffectedRows = 1;
}

这两个事件处理程序的代码可以在示例的代码隐藏类 EfficientPaging.aspx 中找到。

比较默认分页和自定义分页的性能

由于自定义分页仅检索所需的记录,而默认分页返回查看的每个页面 的所有 记录,因此很明显,自定义分页比默认分页更高效。 但是自定义分页的效率如何呢? 通过从默认分页移动到自定义分页,可以看到什么样的性能提升?

不幸的是,这里没有一个大小适合所有答案。 性能提升取决于许多因素,其中最突出的两个因素是分页记录数,以及放置在数据库服务器上的负载以及 Web 服务器与数据库服务器之间的通信通道。 对于只有几十条记录的小表,性能差异可能微乎其微。 但是,对于具有数千到数十万行的大型表,性能差异非常严重。

我的一篇文章“ASP.NET 2.0 中的自定义分页与 SQL Server 2005”包含一些性能测试,我运行以展示这两种分页技术在分页 50,000 条记录的数据库表时的性能差异。 在这些测试中,我检查了在SQL Server级别执行查询的时间, (使用 SQL Profiler) ,以及使用 ASP.NET 跟踪功能在 ASP.NET 页执行查询的时间。 请记住,这些测试是在我的开发箱上运行的,有一个活动用户,因此是不科学的,不会模仿典型的网站加载模式。 不管怎样,结果都说明了处理足够大的数据时默认分页和自定义分页的执行时间的相对差异。

平均持续时间 (秒) Reads
默认分页 SQL 探查器 1.411 383
自定义分页 SQL 探查器 0.002 29
默认分页 ASP.NET 跟踪 2.379 不适用
自定义分页 ASP.NET 跟踪 0.029 不适用

如你所看到的,检索特定数据页平均需要少 354 次读取,并且只需一小部分时间即可完成。 在 ASP.NET 页,自定义页面的呈现时间接近使用默认分页时所花费时间的 1 /100。

总结

默认分页是在数据 Web 控件智能标记中实现检查启用分页复选框,但这种简单性以性能为代价。 使用默认分页时,当用户请求任何数据页时, 将返回所有 记录,即使只显示其中的一小部分。 为了应对此性能开销,ObjectDataSource 提供了一个备用分页选项自定义分页。

虽然自定义分页通过仅检索需要显示的记录来改进默认分页性能问题,但实现自定义分页更为复杂。 首先,必须编写正确 (且高效地) 访问所请求的特定记录子集的查询。 这可以通过多种方式实现:本教程中介绍的一个是使用 SQL Server 2005 的新ROW_NUMBER()函数对结果进行排名,然后仅返回排名在指定范围内的结果。 此外,我们需要添加一种方法来确定正在分页的记录总数。 创建这些 DAL 和 BLL 方法后,我们还需要配置 ObjectDataSource,以便它可以确定正在分页的记录总数,并可以正确地将起始行索引和最大行值传递给 BLL。

虽然实现自定义分页确实需要许多步骤,而且几乎不像默认分页那样简单,但当分页浏览足够大量的数据时,自定义分页是必需的。 如检查的结果所示,自定义分页可以减少 ASP.NET 页面呈现时间的秒数,并可以减轻数据库服务器上的负载一个数量级以上。

编程快乐!

关于作者

斯科特·米切尔是七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直在使用 Microsoft Web 技术。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上mitchell@4GuysFromRolla.com联系他,也可以通过他的博客(可在 中找到http://ScottOnWriting.NET)。