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

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

作者 :Scott Mitchell

下载 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 总数,可以使用以下查询:

SQL
SELECT COUNT(*)
FROM Products

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

首先,在 Northwind.xsd 文件夹中打开类型化 DataSet 文件 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 的类型。 但是,我们的 COUNT 查询将始终返回一个非NULL 值;不管怎样,DAL 方法都会返回可为 null 的整数。

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

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

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

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

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

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

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

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

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

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

    此方法的效率基于所请求的页码,因为为 SET ROWCOUNT 值分配了起始行索引的值加上最大行数。 当分页到低编号页面(例如前几个数据页)时,此方法非常高效。 但是,在接近末尾检索页面时,它表现出默认的分页式性能。

本教程使用 ROW_NUMBER() 关键字实现自定义分页。 有关使用表变量和技术 SET ROWCOUNT 的详细信息,请参阅 一种更高效的方法,用于通过大型结果集分页

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

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

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

SQL
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 返回排名结果

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

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

SQL
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

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

HTML
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:为通过产品分页添加新存储过程

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

SQL
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 类=@startRowIndex 和 @maximumRows 参数的值“/>

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

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

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

图 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 s GetProductsPaged 方法必须接受两个整数输入来指定起始行索引和最大行,并且必须只返回属于指定范围内的记录。 在 ProductsBLL 类中创建这样的 BLL 方法,只需调用 DAL s GetProductsPaged 方法,如下所示:

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

可以将任何名称用于 BLL 方法的输入参数,但是,在配置 ObjectDataSource 以使用此方法时,我们将选择使用 startRowIndex 并 maximumRows 保存额外的工作位。

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

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

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

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

由于我们重新创建只读 GridView,因此请花点时间在 INSERT、UPDATE 和 DELETE 选项卡中设置方法下拉列表,以 (None) 。

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

将输入参数源保留为 None

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

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

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

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

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

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

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

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

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

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

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

 备注

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

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

实现自定义分页和排序

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

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

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

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

实现自定义分页和删除

如果在使用自定义分页技术对数据进行分页的 GridView 中启用删除功能,则当从最后一页删除最后一条记录时,GridView 将消失,而不是适当递减 GridView。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删除操作是否实际成功,这可以通过确保属性是null确定的e.Exception

此方法的工作原理是因为它在步骤 1 之后更新 PageIndex ,但在步骤 2 之前。 因此,在步骤 2 中,将返回相应的记录集。 若要完成此操作,请使用如下所示的代码:

C#
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 属性。 可以使用如下所示的代码来实现此目的:

C#
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 探查器 () 在SQL Server级别执行查询的时间,以及使用 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 s 的新ROW_NUMBER()函数对结果进行排名,然后仅返回排名在指定范围内的结果。 此外,我们需要添加一种方法来确定正在分页的记录总数。 创建这些 DAL 和 BLL 方法后,我们还需要配置 ObjectDataSource,以便它可以确定要分页的总记录数,并可以正确地将起始行索引和最大行值传递给 BLL。

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

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍和 4GuysFromRolla.com 创始人,自1998年以来一直在与 Microsoft Web 技术合作。 斯科特是一名独立顾问、教练员和作家。 他的最新书是 山姆斯教自己在24小时内 ASP.NET 2.0。 他可以通过他的博客访问[email protected],也可以通过他的博客找到http://ScottOnWriting.NET

排序自定义分页数据 (C#)

作者 :Scott Mitchell

下载 PDF

在上一教程中,我们学习了如何在网页上呈现数据时实现自定义分页。 本教程介绍如何扩展前面的示例,以包括对自定义分页进行排序的支持。

介绍

与默认分页相比,自定义分页可以通过多个数量级提高分页的性能,使自定义分页在分页时选择事实上的分页实现选择。 但是,实现自定义分页比实现默认分页更为复杂,尤其是在向混合中添加排序时。 在本教程中,我们将扩展前面的示例,以包括对排序  自定义分页的支持。

 备注

由于本教程基于前面的网页,因此在开始之前需要一些时间从前面的教程网页中复制元素中的声明性语法<asp:Content>, (EfficientPaging.aspx) 并将其粘贴到页面中的<asp:Content>SortParameter.aspx元素之间。 有关将一个 ASP.NET 页面的功能复制到另一个页面的更详细讨论,请参阅 “将验证控件添加到编辑和插入接口 ”教程的步骤 1。

步骤 1:重新验证自定义分页技术

若要使自定义分页正常工作,我们必须实现一些技术,这些技术可以有效地获取给定起始行索引和最大行参数的特定记录子集。 有一些技术可用于实现此目标。 在前面的教程中,我们介绍了如何使用 Microsoft SQL Server 2005 s 的新ROW_NUMBER()排名函数来完成此操作。 简言之, ROW_NUMBER() 排名函数将行号分配给由按指定排序顺序排名的查询返回的每一行。 然后,通过返回编号结果的特定部分来获取相应的记录子集。 以下查询演示了如何使用此方法返回按字母顺序 ProductName排序的结果时编号为 11 到 20 的产品:

SQL
SELECT ProductID, ProductName, ...
FROM
   (SELECT ProductID, ProductName, ..., ROW_NUMBER() OVER
        (ORDER BY ProductName) AS RowRank
    FROM Products) AS ProductsWithRowNumbers
WHERE RowRank > 10 AND RowRank <= 20

此方法适用于使用按字母顺序排序的特定排序顺序 (分页,在本例中) ProductName ,但需要修改查询以显示按其他排序表达式排序的结果。 理想情况下,可以重写上述查询以在子句中使用 OVER 参数,如下所示:

SQL
SELECT ProductID, ProductName, ...
FROM
   (SELECT ProductID, ProductName, ..., ROW_NUMBER() OVER
        (ORDER BY @sortExpression) AS RowRank
    FROM Products) AS ProductsWithRowNumbers
WHERE RowRank > 10 AND RowRank <= 20

遗憾的是,不允许参数化 ORDER BY 子句。 相反,我们必须创建接受输入参数的 @sortExpression 存储过程,但使用以下解决方法之一:

  • 为可能使用的每种排序表达式编写硬编码查询;然后,使用 IF/ELSE T-SQL 语句确定要执行的查询。
  • CASE使用语句基于 @sortExpressio n 输入参数提供动态ORDER BY表达式;有关详细信息,请参阅 SQL 语句的 Power of SQL CASE 语句中的“用于动态排序查询结果”部分。
  • 在存储过程中将适当的查询创建为字符串,然后使用sp_executesql系统存储过程执行动态查询。

其中每个解决方法都有一些缺点。 第一个选项不像其他两个选项一样可维护,因为它要求为每个可能的排序表达式创建查询。 因此,如果以后决定向 GridView 添加新的可排序字段,则还需要返回并更新存储过程。 第二种方法有一些微妙之处,在按非字符串数据库列进行排序时引入性能问题,并且与第一种方法具有相同的可维护性问题。 使用动态 SQL 的第三个选项会引入 SQL 注入攻击的风险,如果攻击者能够执行传入所选输入参数值的存储过程。

虽然这些方法都不是完美的,但我认为第三个选项是三个选项中最好的。 使用动态 SQL 时,它提供了其他两个不具有灵活性的级别。 此外,只有在攻击者能够执行传入所选输入参数的存储过程时,才能利用 SQL 注入攻击。 由于 DAL 使用参数化查询,因此 ADO.NET 将保护通过体系结构发送到数据库的这些参数,这意味着只有在攻击者可以直接执行存储过程时才存在 SQL 注入攻击漏洞。

若要实现此功能,请在名为 GetProductsPagedAndSortedNorthwind 数据库中创建新的存储过程。 此存储过程应接受三个输入参数:@sortExpression一个类型nvarchar(100为) 的输入参数,用于指定结果的排序方式,并在子句中的OVER文本之后ORDER BY直接注入;以及@startRowIndex@maximumRows上一教程中检查的存储过程中的相同两个整数输入参数GetProductsPaged。 GetProductsPagedAndSorted使用以下脚本创建存储过程:

SQL
CREATE PROCEDURE dbo.GetProductsPagedAndSorted
(
    @sortExpression nvarchar(100),
    @startRowIndex int,
    @maximumRows int
)
AS
-- Make sure a @sortExpression is specified
IF LEN(@sortExpression) = 0
    SET @sortExpression = 'ProductID'
-- Issue query
DECLARE @sql nvarchar(4000)
SET @sql = 'SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
            UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
            CategoryName, SupplierName
            FROM (SELECT ProductID, ProductName, p.SupplierID, p.CategoryID,
                    QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
                    ReorderLevel, Discontinued,
                  c.CategoryName, s.CompanyName AS SupplierName,
                   ROW_NUMBER() OVER (ORDER BY ' + @sortExpression + ') AS RowRank
            FROM Products AS p
                    INNER JOIN Categories AS c ON
                        c.CategoryID = p.CategoryID
                    INNER JOIN Suppliers AS s ON
                        s.SupplierID = p.SupplierID) AS ProductsWithRowNumbers
            WHERE     RowRank > ' + CONVERT(nvarchar(10), @startRowIndex) +
                ' AND RowRank <= (' + CONVERT(nvarchar(10), @startRowIndex) + ' + '
                + CONVERT(nvarchar(10), @maximumRows) + ')'
-- Execute the SQL query
EXEC sp_executesql @sql

存储过程首先确保指定了参数的值 @sortExpression 。 如果缺少,则结果按 ProductID排名。 接下来,将构造动态 SQL 查询。 请注意,此处的动态 SQL 查询与以前用于从 Products 表中检索所有行的查询略有不同。 在前面的示例中,我们使用子查询获取了每个产品关联的类别和供应商名称。 此决定是在 “创建数据访问层” 教程中做出的,但未使用 JOIN S,因为 TableAdapter 无法自动为此类查询创建关联的插入、更新和删除方法。 GetProductsPagedAndSorted但是,存储过程必须用于JOIN按类别或供应商名称排序的结果。

此动态查询是通过连接静态查询部分和参数@sortExpression@startRowIndex@maximumRows构建的。 由于 @startRowIndex 并且 @maximumRows 是整数参数,因此必须将其转换为 nvarchar,以便正确连接。 构造此动态 SQL 查询后,将通过 sp_executesql该查询执行。

请花点时间测试此存储过程,其中包含不同值@sortExpression@startRowIndex以及@maximumRows参数。 在服务器资源管理器中,右键单击存储过程名称,然后选择“执行”。 这将启动“运行存储过程”对话框,你可以在其中输入输入参数, (请参阅图 1) 。 若要按类别名称对结果进行排序,请使用 CategoryName 获取 @sortExpression 参数值;若要按供应商的公司名称排序,请使用 CompanyName。 提供参数值后,单击“确定”。 结果显示在“输出”窗口中。 图 2 显示了返回产品时按降序排序 UnitPrice 时排名 11 到 20 的结果。

尝试存储过程的三个输入参数的不同值

图 1:尝试存储过程的三个输入参数的不同值

存储过程的结果显示在输出窗口中

图 2:存储过程的结果显示在输出窗口中 (单击以查看全尺寸图像)

 备注

在子句中OVER按指定ORDER BY列对结果进行排名时,SQL Server必须对结果进行排序。 如果列上存在聚集索引 () 结果按或有覆盖索引排序,但成本可能更高,则这是一种快速操作。 若要提高足够大的查询的性能,请考虑为按其排序结果的列添加非聚集索引。 有关更多详细信息,请参阅 2005 SQL Server中的排名函数和性能

步骤 2:扩充数据访问和业务逻辑层

创建存储过程后 GetProductsPagedAndSorted ,下一步是提供通过应用程序体系结构执行该存储过程的方法。 这需要向 DAL 和 BLL 添加适当的方法。 首先,将方法添加到 DAL。 Northwind.xsd打开类型化数据集,右键单击ProductsTableAdapter,并从上下文菜单中选择“添加查询”选项。 如前一教程所示,我们希望将此新的 DAL 方法配置为使用现有的存储过程, GetProductsPagedAndSorted在本例中。 首先指示希望新的 TableAdapter 方法使用现有的存储过程。

选择使用现有存储过程

图 3:选择使用现有存储过程

若要指定要使用的存储过程, GetProductsPagedAndSorted 请从下一屏幕的下拉列表中选择存储过程。

使用 GetProductsPagedAndSorted 存储过程

图 4:使用 GetProductsPagedAndSorted 存储过程

此存储过程将返回一组记录作为其结果,因此在下一个屏幕中,指示它返回表格数据。

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

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

最后,创建使用 Fill a DataTable 和 Return a DataTable 模式的 DAL 方法,分别命名方法和FillPagedAndSortedGetProductsPagedAndSorted方法。

选择方法名称

图 6:选择方法名称

现在,我们已经扩展了 DAL,我们准备转向 BLL。 ProductsBLL打开类文件并添加新方法GetProductsPagedAndSorted。 此方法需要接受三个输入参数 sortExpression, startRowIndex并且 maximumRows 应该直接调用 DAL s GetProductsPagedAndSorted 方法,如下所示:

C#
[System.ComponentModel.DataObjectMethodAttribute(
    System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsPagedAndSorted(
    string sortExpression, int startRowIndex, int maximumRows)
{
    return Adapter.GetProductsPagedAndSorted
        (sortExpression, startRowIndex, maximumRows);
}

步骤 3:配置 ObjectDataSource 以传入 SortExpression 参数

通过扩充 DAL 和 BLL 以包含使用 GetProductsPagedAndSorted 存储过程的方法,剩余的所有操作都是在页面中将 ObjectDataSource SortParameter.aspx 配置为使用新的 BLL 方法,并根据用户请求对结果排序依据的列传入 SortExpression 参数。

首先将 ObjectDataSource 更改为 SelectMethodGetProductsPagedGetProductsPagedAndSorted. 这可以通过“配置数据源”向导、从okno Vlastnosti或通过声明性语法直接完成。 接下来,我们需要提供 ObjectDataSource 属性SortParameterName的值。 如果设置了此属性,ObjectDataSource 会尝试将 GridView 属性 SortExpression 传递到该 SelectMethod属性。 具体而言,ObjectDataSource 会查找名称等于属性值的 SortParameterName 输入参数。 由于 BLL s GetProductsPagedAndSorted 方法具有命名 sortExpression的排序表达式输入参数,请将 ObjectDataSource s SortExpression 属性设置为 sortExpression。

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

ASP.NET
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL"
    SelectMethod="GetProductsPagedAndSorted" EnablePaging="True"
    SelectCountMethod="TotalNumberOfProducts" SortParameterName="sortExpression">
</asp:ObjectDataSource>

 备注

与前面的教程一样,请确保 ObjectDataSource 在其 SelectParameters 集合中 不包含 sortExpression、startRowIndex 或 maximumRows 输入参数。

若要在 GridView 中启用排序,只需选中 GridView 智能标记中的“启用排序”复选框,该复选框会将 GridView 属性AllowSortingtrue设置为 ,并导致每个列的标题文本呈现为 LinkButton。 当最终用户单击其中一个标头 LinkButton 时,随后会出现回发并执行以下步骤:

  1. GridView 将其SortExpression属性更新为单击其标头链接的字段的值SortExpression
  2. ObjectDataSource 调用 BLL s GetProductsPagedAndSorted 方法,将 GridView 属性 SortExpression 作为方法 sortExpression 输入参数的值传入 (以及) 的相应 startRowIndex 输入 maximumRows 参数值
  3. BLL 调用 DAL 方法GetProductsPagedAndSorted
  4. DAL 执行GetProductsPagedAndSorted存储过程,@sortExpression传入参数 (以及@startRowIndex@maximumRows输入参数值)
  5. 存储过程将适当的数据子集返回到 BLL,该子集将其返回到 ObjectDataSource;然后,此数据绑定到 GridView,呈现为 HTML,并发送到最终用户

图 7 显示按升序排序 UnitPrice 时结果的第一页。

结果按 UnitPrice 排序

图 7:结果按 UnitPrice 排序 (单击以查看全尺寸图像)

虽然当前实现可以按产品名称、类别名称、单位数量和单价正确对结果进行排序,但尝试按供应商名称对结果进行排序会导致运行时异常, (请参阅图 8) 。

尝试在以下运行时异常中按供应商结果对结果进行排序

图 8:尝试按供应商结果在以下运行时异常中对结果进行排序

发生此异常的原因是 SortExpression GridView s SupplierName BoundField 设置为 SupplierName。 但是,表中的供应商名称实际上称为CompanyName此列名。SuppliersSupplierName 但是, OVER 函数使用的 ROW_NUMBER() 子句不能使用别名,并且必须使用实际的列名。 因此,将 SupplierName BoundField s SortExpression 从 SupplierName 更改为 CompanyName (请参阅图 9) 。 如图 10 所示,在此更改后,结果可由供应商排序。

将 SupplierName BoundField s SortExpression 更改为 CompanyName

图 9:将 SupplierName BoundField s SortExpression 更改为 CompanyName

结果现在可以按供应商排序

图 10:结果现在可以按供应商排序 (单击以查看全尺寸图像)

摘要

我们在前面的教程中检查的自定义分页实现要求在设计时按顺序对结果进行排序。 简言之,这意味着我们实现的自定义分页实现无法同时提供排序功能。 在本教程中,我们将存储过程从第一个 @sortExpression 扩展为包含可排序结果的输入参数来克服此限制。

在 DAL 和 BLL 中创建此存储过程并创建新方法后,我们可以通过配置 ObjectDataSource 将 GridView 的当前 SortExpression 属性传递到 BLL SelectMethod来实现一个 GridView,该视图提供排序和自定义分页。

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍和 4GuysFromRolla.com 创始人,自1998年以来一直在与 Microsoft Web 技术合作。 斯科特是一名独立顾问、教练员和作家。 他的最新书是 山姆斯教自己在24小时内 ASP.NET 2.0。 他可以通过他的博客访问[email protected],也可以通过他的博客找到http://ScottOnWriting.NET


 

创建自定义的排序用户界面 (C#)

作者 :Scott Mitchell

下载 PDF

显示排序数据长列表时,通过引入分隔符行对相关数据进行分组非常有用。 本教程介绍如何创建此类排序用户界面。

介绍

在显示排序列中只有少量不同值的已排序数据的长列表时,最终用户可能会发现很难辨别出差异边界的位置(确切地说)。 例如,数据库中有 81 个产品,但只有 9 个不同的类别选项 (8 个唯一类别加上 NULL 选项) 。 请考虑有兴趣检查属于“海鲜”类别的产品的用户的情况。 从列出单个 GridView 中的所有 产品的页面中,用户可能会决定她的最佳选择是按类别对结果进行排序,这将将所有海鲜产品组合在一起。 按类别排序后,用户需要搜索列表,查找海鲜分组产品开始和结束的位置。 由于结果按类别名称按类别名称按字母顺序排序,发现海鲜产品并不困难,但它仍然需要仔细扫描网格中的项目列表。

为了帮助突出显示排序组之间的边界,许多网站使用用户界面来添加此类组之间的分隔符。 与图 1 中显示的分隔符一样,用户能够更快地找到特定组并识别其边界,并确定数据中是否存在不同的组。

已明确标识每个类别组

图 1:已明确标识每个类别组 (单击以查看全尺寸图像)

本教程介绍如何创建此类排序用户界面。

步骤 1:创建标准、可排序的 GridView

在了解如何扩充 GridView 以提供增强的排序接口之前,让我们先创建列出产品的标准可排序 GridView。 首先,在 CustomSortingUI.aspx 文件夹中打开页面 PagingAndSorting 。 将 GridView 添加到页面,将其 ID 属性 ProductList设置为 ,并将其绑定到新的 ObjectDataSource。 将 ObjectDataSource 配置为使用 ProductsBLL 类 s GetProducts() 方法来选择记录。

接下来,配置 GridView,使其仅包含 ProductNameCategoryNameSupplierName、和 BoundFields 和UnitPrice已停止的 CheckBoxField。 最后,通过选中 GridView 智能标记 (中的“启用排序”复选框或将其 AllowSorting 属性设置为 true) ,将 GridView 配置为支持排序。 向 CustomSortingUI.aspx 页面添加这些内容后,声明性标记应如下所示:

ASP.NET
<asp:GridView ID="ProductList" runat="server" AllowSorting="True"
    AutoGenerateColumns="False" DataKeyNames="ProductID"
    DataSourceID="ObjectDataSource1" EnableViewState="False">
    <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"
            ReadOnly="True" SortExpression="SupplierName" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:C}"
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts"
    TypeName="ProductsBLL"></asp:ObjectDataSource>

花点时间查看到目前为止在浏览器中的进度。 图 2 显示按按字母顺序按类别排序时可排序的 GridView。

可排序的 GridView 数据按类别排序

图 2:按类别排序的可排序 GridView 数据 (单击以查看全尺寸图像)

步骤 2:探索添加分隔符行的技术

使用通用的可排序 GridView 完成时,所有保留项都是能够在每个唯一排序组之前在 GridView 中添加分隔符行。 但是,如何将此类行注入到 GridView 中? 实质上,我们需要循环访问 GridView 的行,确定排序列中的值之间的差异,然后添加相应的分隔符行。 在考虑此问题时,解决方案似乎很自然,位于 GridView 事件处理程序 RowDataBound 的某个位置。 如我们在 “基于数据自定义格式 设置”教程中所述,根据行数据应用行级别格式时,通常会使用此事件处理程序。 但是, RowDataBound 事件处理程序不是此处的解决方案,因为无法以编程方式将此事件处理程序中的行添加到 GridView。 事实上,GridView 的 Rows 集合是只读的。

若要向 GridView 添加其他行,有三种选择:

  • 将这些元数据分隔符行添加到绑定到 GridView 的实际数据
  • 将 GridView 绑定到数据后,将其他 TableRow 实例添加到 GridView 控件集合
  • 创建自定义服务器控件,该控件扩展 GridView 控件并重写负责构造 GridView 结构的方法

如果许多网页或多个网站都需要此功能,则创建自定义服务器控件是最佳方法。 但是,这需要相当多的代码和对 GridView 内部工作的深度进行彻底探索。 因此,我们不会考虑本教程的选项。

另外两个选项将分隔符行添加到绑定到 GridView 的实际数据,并在其绑定后操作 GridView 控件集合 - 以不同的方式攻击问题,值得讨论。

向绑定到 GridView 的数据添加行

当 GridView 绑定到数据源时,它会为数据源返回的每个记录创建一个 GridViewRow 。 因此,我们可以通过在将分隔符记录绑定到 GridView 之前将分隔符记录添加到数据源来注入所需的分隔符行。 图 3 说明了此概念。

一种技术涉及向数据源添加分隔符行

图 3:一种技术涉及向数据源添加分隔符行

我在引号中使用术语分隔符记录,因为没有特殊的分隔符记录;相反,我们必须以某种方式标记数据源中的特定记录作为分隔符,而不是普通数据行。 对于我们的示例,我们将实例 ProductsDataTable 绑定到由其构成 ProductRows的 GridView。 通过将其 CategoryID 属性设置为 -1 (,我们可以将记录标记为分隔符行,因为此类值通常无法) 存在。

若要利用此方法,我们需要执行以下步骤:

  1. 以编程方式检索要绑定到 GridView 的数据, (实例 ProductsDataTable)
  2. 根据 GridView 和SortExpressionSortDirection属性对数据进行排序
  3. 循环访问 ProductsRows 中 ProductsDataTable,查找排序列的差异所在位置
  4. 在每个组边界处,将分隔符记录 ProductsRow 实例注入 DataTable,该 CategoryID 实例设置为 -1 (或决定将记录标记为分隔符记录的任何指定)
  5. 注入分隔符行后,以编程方式将数据绑定到 GridView

除了这五个步骤,我们还需要为 GridView 事件 RowDataBound 提供事件处理程序。 在这里,我们将检查每个 DataRow 行,并确定它是分隔符行,一个是其 CategoryID 设置 -1。 如果是这样,我们可能需要调整其格式或单元格中显示的文本 () 。

使用此方法注入排序组边界需要比上面概述的工作多一点,因为还需要为 GridView 事件 Sorting 提供事件处理程序,并跟踪 SortExpression 和 SortDirection 值。

在数据绑定后操作 GridView 控件集合

在将数据绑定到 GridView 之前,我们可以在数据绑定到 GridView 之后 添加分隔符行, 数据绑定的过程构建 GridView 的控制层次结构,实际上只是由 Table 行集合组成的实例,每个实例由单元格集合组成。 具体而言,GridView 控件集合在其根目录中包含一个对象,一个 TableGridViewRow 派生自绑定到 TableRow GridView 的每个记录 DataSource 的类) 的 (,以及 TableCell 每个实例中每个 GridViewRow 数据字段的对象 DataSource

若要在每个排序组之间添加分隔符行,我们可以在创建此控件层次结构后直接操作此控件层次结构。 我们可以确信,在呈现页面时,GridView 控件层次结构是上次创建的。 因此,此方法重写 Page 类 s Render 方法,此时 GridView 的最终控件层次结构将更新为包含所需的分隔符行。 图 4 说明了此过程。

替代技术操作 GridView 控件层次结构

图 4:备用技术操作 GridView 控件层次结构 (单击以查看全尺寸图像)

在本教程中,我们将使用此后一种方法来自定义排序用户体验。

 备注

在本教程中演示的代码基于 Teemu Keiski 博客文章中提供的示例, 使用 GridView 排序分组播放位

步骤 3:将分隔符行添加到 GridView 控件层次结构

由于我们只想在其控件层次结构创建并创建该页面访问的最后一次之后,将分隔符行添加到 GridView 控件层次结构中,因此我们希望在页面生命周期结束时执行此添加,但在实际 GridView 控件层次结构呈现到 HTML 之前。 我们可实现此目的的最新可能点是 Page 类事件 Render ,可以使用以下方法签名在代码隐藏类中重写该事件:

C#
protected override void Render(HtmlTextWriter writer)
{
    // Add code to manipulate the GridView control hierarchy
    base.Render(writer);
}

Page当调用base.Render(writer)类的原始Render方法时,页面中的每个控件都将呈现,并基于其控件层次结构生成标记。 因此,必须调用这两 base.Render(writer)个页面,以便呈现页面,并且我们在调用 base.Render(writer)之前操作 GridView 控件层次结构,以便在呈现该控件层次结构之前,分隔符行已添加到 GridView 控件层次结构中。

若要注入排序组标头,我们首先需要确保用户已请求对数据进行排序。 默认情况下,GridView 的内容不会排序,因此我们不需要输入任何组排序标头。

 备注

如果希望在首次加载页面时按特定列对 GridView 进行排序,请调用第一页上的 GridView 方法 Sort , (但不在后续回发) 。 为此,请在条件内的Page_Loadif (!Page.IsPostBack)事件处理程序中添加此调用。 有关方法的详细信息,请参阅 分页和排序报表数据 教程信息 Sort 。

假设数据已排序,下一个任务是确定数据排序依据的列,然后扫描该列的值是否存在差异的行。 以下代码确保已对数据进行排序,并查找已对数据进行排序的列:

C#
protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // Determine the index and HeaderText of the column that
        //the data is sorted by
        int sortColumnIndex = -1;
        string sortColumnHeaderText = string.Empty;
        for (int i = 0; i < ProductList.Columns.Count; i++)
        {
            if (ProductList.Columns[i].SortExpression.CompareTo(ProductList.SortExpression)
                == 0)
            {
                sortColumnIndex = i;
                sortColumnHeaderText = ProductList.Columns[i].HeaderText;
                break;
            }
        }
        // TODO: Scan the rows for differences in the sorted column�s values
}

如果 GridView 尚未排序,则 GridView 的属性 SortExpression 将尚未设置。 因此,仅当此属性具有一些值时,我们才希望添加分隔符行。 如果这样做,接下来需要确定对数据进行排序的列的索引。 这是通过循环访问 GridView 集合 Columns 来实现的,搜索其 SortExpression 属性等于 GridView 属性 SortExpression 的列。 除了列索引之外,我们还会获取显示 HeaderText 分隔符行时使用的属性。

使用对数据进行排序的列的索引时,最后一步是枚举 GridView 的行。 对于每一行,我们需要确定排序列的值是否与上一行排序列的值不同。 如果是这样,我们需要将新 GridViewRow 实例注入到控件层次结构中。 此操作通过以下代码完成:

C#
protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // ... Code for finding the sorted column index removed for brevity ...
        // Reference the Table the GridView has been rendered into
        Table gridTable = (Table)ProductList.Controls[0];
        // Enumerate each TableRow, adding a sorting UI header if
        // the sorted value has changed
        string lastValue = string.Empty;
        foreach (GridViewRow gvr in ProductList.Rows)
        {
            string currentValue = gvr.Cells[sortColumnIndex].Text;
            if (lastValue.CompareTo(currentValue) != 0)
            {
                // there's been a change in value in the sorted column
                int rowIndex = gridTable.Rows.GetRowIndex(gvr);
                // Add a new sort header row
                GridViewRow sortRow = new GridViewRow(rowIndex, rowIndex,
                    DataControlRowType.DataRow, DataControlRowState.Normal);
                TableCell sortCell = new TableCell();
                sortCell.ColumnSpan = ProductList.Columns.Count;
                sortCell.Text = string.Format("{0}: {1}",
                    sortColumnHeaderText, currentValue);
                sortCell.CssClass = "SortHeaderRowStyle";
                // Add sortCell to sortRow, and sortRow to gridTable
                sortRow.Cells.Add(sortCell);
                gridTable.Controls.AddAt(rowIndex, sortRow);
                // Update lastValue
                lastValue = currentValue;
            }
        }
    }
    base.Render(writer);
}

此代码首先以编程方式引用 Table GridView 控件层次结构的根目录中找到的对象,并创建名为 lastValue 的字符串变量。 lastValue 用于将当前行的排序列值与上一行的值进行比较。 接下来,将枚举 GridView 的 Rows 集合,并且对于每一行,排序列的值将存储在变量中 currentValue 。

 备注

若要确定特定行排序列的值,请使用单元格 s Text 属性。 这适用于 BoundFields,但对 TemplateFields、CheckBoxFields 等将不起作用。 我们将很快了解如何考虑备用 GridView 字段。

currentValue然后比较和lastValue变量。 如果它们不同,我们需要向控件层次结构添加新分隔符行。 这可以通过确定对象Rows集合中的Table索引GridViewRow、创建新GridViewRow实例和TableCell实例,然后将控件GridViewRow层次结构添加到TableCell控件层次结构来实现。

请注意,分隔符行的孤独 TableCell 格式,以便它跨越 GridView 的整个宽度,使用 SortHeaderRowStyle CSS 类进行格式化,并具有其 Text 属性,以便它同时显示排序组名称 ((如 Category ) )和组值 ((如饮料) )。 最后, lastValue 更新为值 currentValue

需要在文件中指定Styles.css用于设置排序组标题行SortHeaderRowStyle格式的 CSS 类。 随意使用任何样式设置吸引你;我使用了以下内容:

css
.SortHeaderRowStyle
{
    background-color: #c00;
    text-align: left;
    font-weight: bold;
    color: White;
}

使用当前代码,排序接口在按任何 BoundField 排序时添加排序组标头 (请参阅图 5,其中显示了供应商排序时) 的屏幕截图。 但是,如果按任何其他字段类型 ((如 CheckBoxField 或 TemplateField) )进行排序,则找不到排序组标头, (请参阅图 6) 。

排序接口包括按 BoundFields 排序时的排序组标头

图 5:按 BoundFields 排序时,排序接口包括排序组标题 (单击以查看全尺寸图像)

对 CheckBoxField 进行排序时缺少排序组标头

图 6:对 CheckBoxField 进行排序时缺少排序组标题 (单击以查看全尺寸图像)

按 CheckBoxField 排序时缺少排序组标头的原因是代码当前只 TableCell 使用 s Text 属性来确定每行排序列的值。 对于 CheckBoxFields,TableCellText 属性为空字符串;相反,该值可通过驻留在集合Controls中的 TableCell CheckBox Web 控件获得。

若要处理 BoundFields 以外的字段类型,我们需要扩充分配变量以 currentValue 检查集合中 TableCell 是否存在 CheckBox 的代码 Controls 。 不要使用 currentValue = gvr.Cells[sortColumnIndex].Text,请将此代码替换为以下内容:

C#
string currentValue = string.Empty;
if (gvr.Cells[sortColumnIndex].Controls.Count > 0)
{
    if (gvr.Cells[sortColumnIndex].Controls[0] is CheckBox)
    {
        if (((CheckBox)gvr.Cells[sortColumnIndex].Controls[0]).Checked)
            currentValue = "Yes";
        else
            currentValue = "No";
    }
    // ... Add other checks here if using columns with other
    //      Web controls in them (Calendars, DropDownLists, etc.) ...
}
else
    currentValue = gvr.Cells[sortColumnIndex].Text;

此代码检查当前行的排序列 TableCell ,以确定集合中 Controls 是否有任何控件。 如果有,第一个控件是 CheckBox,则 currentValue 变量设置为“是”或“否”,具体取决于 CheckBox 的属性 Checked 。 否则,该值取自 TableCell s Text 属性。 可以复制此逻辑以处理 GridView 中可能存在的任何 TemplateFields 的排序。

添加上述代码后,“已停止的 CheckBoxField”排序时,排序组标头现在存在, (请参阅图 7) 。

对 CheckBoxField 进行排序时,排序组标题现在存在

图 7:对 CheckBoxField 进行排序时,“排序组标题” (单击以查看全尺寸图像)

 备注

如果产品具有NULL数据库值CategoryIDSupplierID(或UnitPrice字段),则默认情况下,这些值将在 GridView 中显示为空字符串,这意味着具有值的这些产品NULL的分隔符行文本将类似于 Category: (,类别后没有名称:如类别:饮料) 。 如果希望在此处显示一个值,可以将 BoundFields NullDisplayText 属性设置为所显示的文本,或者在向分隔符行的属性Text分配currentValue时在 Render 方法中添加条件语句。

摘要

GridView 不包含许多用于自定义排序接口的内置选项。 但是,使用一些低级别代码,可以调整 GridView 控件层次结构以创建更自定义的接口。 本教程介绍了如何为可排序 GridView 添加排序组分隔符行,以便更轻松地标识不同的组和这些组边界。 有关自定义排序接口的其他示例,请查看 Scott Guthrie s A Few ASP.NET 2.0 GridView Sorting Tips and Tricks 博客文章。

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍和 4GuysFromRolla.com 的创始人,自1998年以来一直在与 Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆斯教自己在24小时内 ASP.NET 2.0。 他可以到达 [email protected] 或通过他的博客,可以在其中 http://ScottOnWriting.NET找到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章