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

作者 :斯科特·米切尔

下载 PDF

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

介绍

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

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

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

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

注意

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

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

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

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

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

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

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

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

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

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

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

SELECT COUNT(*)
FROM Products

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

首先,在 Northwind.xsd 文件夹中打开类型化数据集文件 App_Code/DAL 。 接下来,右键单击 ProductsTableAdapter 设计器中的“添加查询”。 正如我们在前面的教程中看到的那样,这将使我们能够向 DAL 添加新方法,在调用时,该方法将执行特定的 SQL 语句或存储过程。 与前面的教程中的 TableAdapter 方法一样,对于此教程,选择使用即席 SQL 语句。

使用即席 SQL 语句

图 1:使用即席 SQL 语句

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

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

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

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

使用 SELECT COUNT\ FROM 产品查询

图 3:使用 SELECT COUNT} FROM 产品查询

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

将 DAL 方法命名为 TotalNumberOfProducts

图 4:命名 DAL 方法 TotalNumberOfProducts

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

除了 DAL 方法,我们还需要在 BLL 中使用方法。 ProductsBLL打开类文件并添加一个TotalNumberOfProducts仅调用 DAL 方法TotalNumberOfProducts的方法:

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

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

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

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

如果数据库表中已有用作行索引的列,则这不是一个挑战。 首先,我们可能认为表字段ProductID足以满足Products,因为第一个产品有 ProductID 1,第二个 2,等等。 但是,删除产品会在序列中留下空白,使此方法失效。

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

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

  • 使用表变量和 SET ROWCOUNT SQL Server s 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() 关键字实现自定义分页。 有关使用表变量和技术 SET ROWCOUNT 的详细信息,请参阅 “有效分页浏览大量数据”。

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

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

不能直接在子句中使用WHEREROW_NUMBER()返回的排名信息。 但是,派生表可用于返回 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>)

注意

如本教程稍后所述, StartRowIndex ObjectDataSource 提供的索引从零开始编制索引,而 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 的行。ProductName ROW_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 s(请参阅图 7)。 尝试不同的值并检查结果。

输入 <span 类的值=@startRowIndex和@maximumRows参数“/>

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

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

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

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

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

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

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

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

从下拉列表中选择 GetProductsPaged 存储过程

图 10:从下拉列表中选择 GetProductsPaged 存储过程

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

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

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

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

将方法命名为 FillPaged 和 GetProductsPaged

图 12:命名方法 FillPaged 和 GetProductsPaged

除了创建 DAL 方法以返回产品的特定页面外,我们还需要在 BLL 中提供此类功能。 与 DAL 方法一样,BLL s 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.aspx 文件夹中的页面 PagingAndSorting ,将 GridView 添加到页面,并将其配置为使用新的 ObjectDataSource 控件。 在过去的教程中,我们经常将 ObjectDataSource 配置为使用 ProductsBLL 类的方法 GetProducts 。 但是,这一次,我们希望改用GetProductsPaged该方法,因为GetProducts该方法返回数据库中的所有产品,而GetProductsPaged只返回特定记录子集。

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

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

由于我们重新创建只读 GridView,因此请花点时间将 INSERT、UPDATE 和 DELETE 选项卡中的方法下拉列表设置为“无”。

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

将输入参数源保留为 None

图 14:将输入参数源保留为 None

完成 ObjectDataSource 向导后,GridView 将包含每个产品数据字段的 BoundField 或 CheckBoxField。 随意定制 GridView 的外观,如你所见。 我选择只ProductName显示、CategoryNameSupplierNameQuantityPerUnitUnitPrice BoundFields。 此外,通过选中智能标记中的“启用分页”复选框,将 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 作为输入GetProductsPagedstartRowIndex参数和maximumRows输入参数的值。 因此,生成的 SQL 查询不返回任何记录,因此不显示 GridView。

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

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

进行这些更改后,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 变量、PageIndexEditIndex集合SelectedIndexDataKeys等都存储在控件状态中,无论 GridView EnableViewState 的属性的值如何,这些变量都会保留。 由于该值 PageCount 使用视图状态在回发之间持久化,因此在使用包含指向最后一页的链接的分页接口时,必须启用 GridView 的视图状态。 (如果分页接口不包含指向最后一页的直接链接,则可以禁用视图状态。

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

实现自定义分页和排序

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

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

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

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

实现自定义分页和删除

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

删除最后一个产品后,GridView 自动转到第八页,并且此类功能与默认分页一起显示。 但是,通过自定义分页,在最后一页上删除该最后一个产品后,GridView 只会完全从屏幕中消失。 发生此情况的确切原因略超出本教程的范围;请参阅使用自定义分页从 GridView 中删除最后一条记录,了解此问题的来源的自定义分页。 总之,这是因为单击“删除”按钮时 GridView 执行的以下步骤序列:

  1. 删除记录
  2. 获取要为指定 PageIndexPageSize
  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删除操作是否实际成功,这可以通过确保属性是null确定的e.Exception

此方法的工作原理是因为它更新了 PageIndex 步骤 1 之后,但在步骤 2 之前。 因此,在步骤 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 服务器和数据库服务器之间的通信通道。 对于只有几十条记录的小表,性能差异可能微不足道。 但是,对于大型表,有数千到数十万行,但性能差异非常严重。

我的一篇文章“使用 SQL Server 2005 在 ASP.NET 2.0 中的自定义分页”包含一些性能测试,在通过包含 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 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0。 他可以通过他的博客联系到mitchell@4GuysFromRolla.com他,可以在该博客中找到http://ScottOnWriting.NET