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找到。

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