有效率地分頁大量資料 (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 筆記錄時,第九個和最後一頁只會有一筆記錄。 不過,沒有頁面會顯示超過 [最大數據列] 值的記錄。
  • Total Record Count the total number of records being paged through. 雖然不需要此變數來判斷要擷取給指定頁面的記錄,但它確實會決定分頁介面。 例如,如果有 81 個正在分頁的產品,分頁介面就會知道在分頁 UI 中顯示九個頁碼。

使用預設分頁時,起始數據列索引會計算為頁面索引的乘積和頁面大小加上一個,而 [最大數據列] 只是頁面大小。 由於預設分頁會在轉譯任何數據頁時從資料庫擷取所有記錄,因此已知每個數據列的索引,因而讓移至 [開始數據列索引] 數據列成為一項簡單的工作。 此外,[總記錄計數] 已立即可用,因為它只是 DataTable (中的記錄數目,或任何用來保存資料庫結果的物件) 。

假設有起始數據列索引和最大數據列變數,自定義分頁實作必須只傳回從起始數據列索引開始的精確記錄子集,以及之後最多傳回最大數據列數目的記錄子集。 自訂分頁提供兩項挑戰:

  • 我們必須能夠有效率地將數據列索引與整個數據中的每一個數據列產生關聯,以便開始在指定的起始數據列索引處傳回記錄
  • 我們需要提供正在分頁的記錄總數

在接下來的兩個步驟中,我們將檢查回應這兩項挑戰所需的 SQL 腳本。 除了 SQL 腳本之外,我們也需要在 DAL 和 BLL 中實作方法。

步驟 2:傳回正在分頁的記錄總數

在檢查如何擷取所顯示頁面的精確記錄子集之前,讓我們先看看如何傳回已分頁的記錄總數。 需要此資訊才能正確設定分頁用戶介面。 您可以使用聚合函數來取得COUNT特定 SQL 查詢所傳回的記錄總數。 例如,若要判斷數據表中的 Products 記錄總數,我們可以使用下列查詢:

SELECT COUNT(*)
FROM Products

讓我們將方法新增至 DAL,以傳回這項資訊。 特別是,我們將建立稱為 TotalNumberOfProducts() 的 DAL 方法,以執行 SELECT 上述語句。

從開啟 Northwind.xsd 資料夾中的具類型資料集檔案 App_Code/DAL 開始。 接下來,以滑鼠右鍵按兩下 ProductsTableAdapter Designer中的 ,然後選擇 [新增查詢]。 如先前教學課程中所見,這可讓我們將新的方法新增至 DAL,叫用時,將會執行特定的 SQL 語句或預存程式。 如同上一個教學課程中的 TableAdapter 方法,針對此教學課程,選擇使用臨機操作 SQL 語句。

使用臨機操作 SQL 語句

圖 1:使用臨機操作 SQL 語句

在下一個畫面上,我們可以指定要建立的查詢類型。 由於此查詢會傳回單一的純量值,因此資料表中的 Products 記錄總數會選擇 SELECT 傳回 Singe 值選項的 。

將查詢設定為使用傳回單一值的SELECT語句

圖 2:將查詢設定為使用傳回單一值的 SELECT 語句

指示要使用的查詢類型之後,接下來必須指定查詢。

使用 SELECT COUNT (*) FROM 產品查詢

圖 3:使用 SELECT COUNT (*) FROM Products 查詢

最後,指定方法的名稱。 如前所述,讓我們使用 TotalNumberOfProducts

將 DAL 方法命名為 TotalNumberOfProducts

圖 4:將 DAL 方法命名為 TotalNumberOfProducts

按兩下 [完成] 之後,精靈會將 方法新增 TotalNumberOfProducts 至 DAL。 如果 SQL 查詢的結果為 NULL,DAL 中的純量傳回方法會傳回可為 Null 的類型。 不過,我們的 COUNT 查詢一律會傳回非NULL 值;不論 DAL 方法會傳回可為 Null 的整數。

除了 DAL 方法之外,我們也需要在 BLL 中使用 方法。 ProductsBLL開啟類別檔案,並新增TotalNumberOfProducts直接呼叫 DAL s TotalNumberOfProducts 方法的方法:

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

DAL s TotalNumberOfProducts 方法會傳回可為 Null 的整數;不過,我們已建立 ProductsBLL 類別 s TotalNumberOfProducts 方法,以便傳回標準整數。 因此,我們必須讓 ProductsBLL 類別 s TotalNumberOfProducts 方法傳回 DAL s 方法所傳回之可為 Null 整數的值 TotalNumberOfProducts 部分。 的呼叫 GetValueOrDefault() 會傳回可為 Null 整數的值,如果存在,則傳回可為 Null 的整數;不過,如果可為 Null 的整數是 null,則會傳回預設整數值 0。

步驟 3:傳回精確的記錄子集

下一個工作是在 DAL 和 BLL 中建立方法,以接受稍早討論的起始數據列索引和最大數據列變數,並傳回適當的記錄。 在這麼做之前,讓我們先看看所需的 SQL 腳本。 我們遇到的挑戰是,我們必須能夠有效率地將索引指派給整個結果中的每一個數據列,以便只傳回從起始數據列索引 (開始的記錄,以及最多到記錄數目上限) 。

如果資料庫數據表中已經有數據行做為數據列索引,則這不是一項挑戰。 第一眼,我們可能會認為 Products 數據表字段 ProductID 就已足夠,因為第一個產品有 ProductID 1 個、第二個產品是 2 等等。 不過,刪除產品會讓序列有間距,使此方法失效。

有兩種一般技術可用來有效率地將數據列索引與要逐頁瀏覽的數據產生關聯,進而讓擷取的精確記錄子集:

  • 使用 SQL Server 2005 s ROW_NUMBER() 關鍵詞 new SQL Server 2005 時,關鍵詞會ROW_NUMBER()根據某些順序,將排名與每個傳回的記錄產生關聯。 此排名可作為每個數據列的數據列索引。

  • 使用數據表變數和SET ROWCOUNTSQL Server 語句SET ROWCOUNT可用來指定查詢在終止前應該處理的記錄總數;數據表變數是本機 T-SQL 變數,可保存表格式數據,類似於臨時表。 此方法同樣適用於 Microsoft SQL Server 2005 和 SQL Server 2000 (,而ROW_NUMBER()此方法只適用於 SQL Server 2005) 。

    這裡的概念是建立數據表變數,該變數具有 IDENTITY 分頁數據之數據表主鍵的數據行和數據行。 接下來,正在分頁的數據表內容會傾印到數據表變數中,藉此透過數據表中每個記錄的數據行) ,將循序數據列索引 IDENTITY (產生關聯。 一旦填入數據表變數, SELECT 就可以執行數據表變數上的語句,並聯結基礎表來提取特定記錄。 語句 SET ROWCOUNT 用來以智慧方式限制需要傾印到數據表變數的記錄數目。

    此方法的效率是以所要求的頁碼為基礎,因為值 SET ROWCOUNT 會指派開始數據列索引的值加上最大數據列。 分頁到低編號的頁面時,例如前幾個數據頁,此方法非常有效率。 不過,它會在擷取接近結尾的頁面時,展現預設的分頁類似效能。

本教學課程使用 ROW_NUMBER() 關鍵詞實作自定義分頁。 如需使用數據表變數和技術 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 中的排名函式和效能

ROW_NUMBER() 傳回的排名資訊不能直接用於 WHERE 子句中。 不過,衍生數據表可用來傳回 ROW_NUMBER() 結果,然後會出現在 子句中 WHERE 。 例如,下列查詢會使用衍生數據表來傳回 ProductName 和 UnitPrice 數據行以及 ROW_NUMBER() 結果,然後使用 WHERE 子句只傳回價格排名介於 11 到 20 之間的產品:

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

進一步擴充此概念,我們可以利用此方法,擷取所需的起始數據列索引和最大數據列值的特定數據頁面:

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

注意

如本教學課程稍後所見,StartRowIndexObjectDataSource 所提供的 索引是從零開始,而 ROW_NUMBER() 2005 SQL Server 傳回的值是從 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並使用ROW_NUMBER()@maximumRows依欄位排序的ProductName函式,只傳回大於指定@startRowIndex且小於或等於@maximumRow@startRowIndex + s 的數據列。 在新的預存程式中輸入下列腳本,然後按下 [儲存] 圖示,將預存程式新增至資料庫。

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:輸入 和 @maximumRows 參數的值@startRowIndex

選擇這些輸入參數值之後,[輸出] 視窗會顯示結果。 圖 8 顯示針對 和 @maximumRows 參數傳入 10 @startRowIndex 時的結果。

數據的第二頁中顯示的記錄會傳回

圖 8:數據的第二頁中顯示的記錄會傳回 (按兩下即可檢視大小完整的影像)

建立此預存程式之後,我們就可以開始建立 ProductsTableAdapter 方法。 Northwind.xsd開啟 [具類型的數據集],在 中ProductsTableAdapter按兩下滑鼠右鍵,然後選擇 [新增查詢] 選項。 不要使用臨機操作 SQL 語句建立查詢,而是使用現有的預存程式加以建立。

使用現有的預存程式建立 DAL 方法

圖 9:使用現有的預存程式建立 DAL 方法

接下來,系統會提示您選取要叫用的預存程式。 GetProductsPaged從下拉式清單中挑選預存程式。

從 Drop-Down 清單中選擇 GetProductsPaged 預存程式

圖 10:從 Drop-Down 列表中選擇 GetProductsPaged 預存程式

下一個畫面接著會詢問預存程式傳回何種數據:表格式數據、單一值或無值。 GetProductsPaged由於預存程式可以傳回多個記錄,因此表示它會傳回表格式數據。

指出預存程式會傳回表格式數據

圖 11:指出預存程式會傳回表格式數據

最後,指出您想要建立的方法名稱。 如同先前的教學課程,請繼續使用 Fill a DataTable 和 Return a DataTable 來建立方法。 將第一個方法與 FillPaged 第二 GetProductsPaged個命名為 。

將方法命名為 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 類別 s GetProducts 方法。 不過,這次我們想要改用 GetProductsPaged 方法,因為 GetProducts 方法會傳回資料庫中 的所有 產品,而 GetProductsPaged 只會傳回特定記錄子集。

將 ObjectDataSource 設定為使用 ProductsBLL 類別 s GetProductsPaged 方法

圖 13:將 ObjectDataSource 設定為使用 ProductsBLL 類別 s GetProductsPaged 方法

因為我們要建立只讀 GridView,所以請花點時間在 INSERT、UPDATE 和 DELETE 索引標籤中設定方法下拉式清單,以 (None) 。

接下來,ObjectDataSource 精靈會提示我們輸入方法startRowIndex的來源GetProductsPagedmaximumRows輸入參數值。 這些輸入參數實際上是由 GridView 自動設定,因此只要將來源設定為 [無],然後按兩下 [完成]。

將輸入參數來源保留為無

圖 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 做為 和 maximumRows 輸入參數的值。GetProductsPagedstartRowIndex 因此,產生的 SQL 查詢不會傳回任何記錄,因此不會顯示 GridView。

若要解決此問題,我們必須將 ObjectDataSource 設定為使用自定義分頁。 這可以在下列步驟中完成:

  1. 將 ObjectDataSource 的 EnablePaging 屬性設定為 true ,表示它必須傳遞至 SelectMethod 兩個額外參數的 ObjectDataSource:一個指定起始數據列索引 (StartRowIndexParameterName) ,另一個指定 [最大 MaximumRowsParameterName 數據列 () 。
  2. 設定 ObjectDataSource s StartRowIndexParameterNameMaximumRowsParameterName Properties 根據StartRowIndexParameterName 和 屬性,指出 MaximumRowsParameterName 為了自定義分頁目的傳遞至 SelectMethod 的輸入參數名稱。 根據預設,這些參數名稱為 startIndexRowmaximumRows,這就是為什麼在 BLL 中建立 GetProductsPaged 方法時,我使用這些值作為輸入參數。 如果您選擇針對 BLL s GetProductsPaged 方法使用不同的參數名稱,例如 startIndexmaxRows,例如,您必須 (針對) 設定 ObjectDataSource s StartRowIndexParameterNameMaximumRowsParameterName 屬性,例如 startIndex for StartRowIndexParameterName 和 maxRows MaximumRowsParameterName
  3. 將 ObjectDataSource s SelectCountMethod 屬性 設定為方法的名稱,這個方法會傳回透過分頁 TotalNumberOfProducts 記錄的總數 () 重新叫 ProductsBLL 用類別 TotalNumberOfProducts 方法會傳回使用執行查詢的 DAL 方法 SELECT COUNT(*) FROM Products 所分頁的記錄總數。 ObjectDataSource 需要這項資訊,才能正確轉譯分頁介面。
  4. 透過精靈設定 ObjectDataSource 時,從 ObjectDataSource 宣告式標記中移除 和 maximumRows<asp:Parameter> Elements,Visual Studio 會自動為方法的輸入參數新增兩個元素。startRowIndex<asp:Parameter>GetProductsPaged 藉由將 設定EnablePagingtrue為 ,這些參數會自動傳遞;如果這些參數也出現在宣告式語法中,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 變數、PageIndexEditIndexSelectedIndexDataKeys、集合等等會儲存在控件狀態中,不論 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 方法 GetProductsPaged 擷取數據之後發生,而這個方法只會傳回特定頁面的那些記錄。 若要正確實作排序,我們需要將排序表達式傳遞至 GetProductsPaged 方法,以便在傳回特定數據頁面之前適當地排序數據。 我們將在下一個教學課程中瞭解如何完成此作業。

實作自定義分頁和刪除

如果您在使用自定義分頁技術分頁的 GridView 中啟用刪除功能,您會發現從最後一頁刪除最後一筆記錄時,GridView 會消失,而不是適當遞減 GridView s PageIndex。 若要重現這個 Bug,請只針對我們剛建立的教學課程啟用刪除。 移至最後一頁 (第 9 頁) ,您應該會看到單一產品,因為我們一次分頁 81 個產品,10 個產品。 刪除此產品。

刪除最後一個產品時,GridView 應該 會自動移至第八頁,而這類功能會以預設分頁呈現。 不過,透過自定義分頁,在刪除最後一個頁面上的最後一個產品之後,GridView 只會從畫面完全消失。 發生此情況的確切原因比本教學課程的範圍還多一點;請參閱從具有自定義分頁的 GridView 刪除最後一筆記錄,以取得此問題來源的低階詳細數據。 總而言之,這是因為按兩下 [刪除] 按鈕時,GridView 所執行的步驟順序如下:

  1. 刪除記錄
  2. 取得要針對指定 PageIndex 和 顯示的適當記錄 PageSize
  3. 檢查以確定 PageIndex 不會超過數據源中的數據頁數;如果是,則會自動遞減 GridView 的 PageIndex 屬性
  4. 使用步驟 2 中取得的記錄,將數據的適當頁面系結至 GridView

問題源自於在步驟 2 PageIndex 中,擷取要顯示的記錄時所使用的 是最後 PageIndex 一個頁面的唯一記錄剛刪除的 。 因此,在步驟 2 中,自最後一頁的數據不再包含任何記錄之後, 就不會 傳回任何記錄。 然後,在步驟 3 中,GridView 發現其 PageIndex 屬性大於數據源中的總頁數 (,因為我們刪除了最後一頁的最後一筆記錄) ,因此會遞減其 PageIndex 屬性。 在步驟 4 中,GridView 會嘗試將本身系結至步驟 2 中擷取的數據;不過,在步驟 2 中未傳回任何記錄,因此會產生空的 GridView。 使用預設分頁時,此問題不會呈現,因為在步驟 2 中 ,會 從數據源擷取所有記錄。

若要修正此問題,我們有兩個選項。 第一個是建立 GridView 事件處理程式的 RowDeleted 事件處理程式,以決定剛刪除的頁面中顯示多少筆記錄。 如果只有一筆記錄,則剛刪除的記錄必須是最後一筆記錄,而我們需要遞減 GridView s PageIndex。 當然,我們只想要在刪除作業實際成功時更新 PageIndex ,這可藉由確保 e.Exception 屬性為 null來判斷。

此方法的運作方式是因為它會在 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 代碼後置類別中找到。

比較預設和自定義分頁的效能

由於自定義分頁只會擷取所需的記錄,而預設分頁會針對要檢視 的每個 頁面傳回所有記錄,因此清楚表示自定義分頁比預設分頁更有效率。 但自定義分頁更有效率? 從預設分頁移至自定義分頁,即可看到何種效能提升?

可惜的是,這裡沒有一個大小符合所有答案。 效能提升取決於許多因素,最明顯的兩個因素是分頁的記錄數目,以及放置於資料庫伺服器上的負載,以及網頁伺服器與資料庫伺服器之間的通道。 對於只有數十筆記錄的小型數據表,效能差異可能會微不足道。 對於大型數據表,有數千到數百萬個數據列,但效能差異相當嚴重。

我的文章「ASP.NET 2.0 中的自定義分頁與 SQL Server 2005」包含一些效能測試,我執行了一些效能測試,以在分頁至具有 50,000 筆記錄的資料庫數據表時,展示這兩個分頁技術之間的效能差異。 在這些測試中,我檢查了使用 SQL Profiler) 在 SQL Server (層級執行查詢的時間,以及使用 ASP.NET 追蹤功能的 ASP.NET 頁面執行查詢。 請記住,這些測試是在我的開發方塊中搭配單一作用中用戶執行,因此並不重要,而且不會模擬一般網站負載模式。 不論為何,結果都會說明使用足夠大量數據時,預設和自定義分頁運行時間的相對差異。

平均持續時間 (秒) Reads
默認分頁 SQL Profiler 1.411 383
自定義分頁 SQL Profiler 0.002 29
默認分頁 ASP.NET 追蹤 2.379 N/A
自定義分頁 ASP.NET 追蹤 0.029 N/A

如您所見,擷取需要 354 個數據的特定頁面平均讀取,並在一小部分時間內完成。 在 [ASP.NET] 頁面上,自定義頁面能夠在使用預設分頁時,以接近 1/100 的時間 轉譯。

摘要

默認分頁是實作的一個 Cinch,只要核取數據 Web 控件智慧標記中的 [啟用分頁] 複選框,但這種簡單性會以效能成本計算。 使用預設分頁時,當使用者要求任何數據頁面時,會傳回 所有 記錄,即使只顯示其中一小部分也一樣。 為了對抗此效能額外負荷,ObjectDataSource 提供替代的分頁選項自定義分頁。

雖然自定義分頁只會擷取需要顯示的記錄,以改善預設分頁效能問題,但實作自定義分頁會更相關。 首先,必須撰寫正確 (且有效率的查詢,) 存取所要求的特定記錄子集。 這可以透過數種方式來完成;我們在本教學課程中檢查的是使用 SQL Server 2005 的新ROW_NUMBER()函式來排名結果,然後只傳回排名落在指定範圍內的結果。 此外,我們需要新增方法來判斷要分頁的記錄總數。 建立這些 DAL 和 BLL 方法之後,我們也需要設定 ObjectDataSource,以便判斷要分頁的記錄總數,並正確地將起始數據列索引和最大數據列值傳遞至 BLL。

雖然實作自定義分頁需要一些步驟,而且不像預設分頁一樣簡單,但自定義分頁在分頁到足夠大量數據時是必要的。 如所檢查的結果所示,自定義分頁可以捨棄 ASP.NET 頁面轉譯時間的秒數,並可讓資料庫伺服器上的負載減少一或多個大小順序。

快樂的程序設計!

關於作者

Scott Mitchell 是七份 ASP/ASP.NET 書籍的作者,以及 1998 年以來與 Microsoft Web 技術合作的 4GuysFromRolla.com 作者。 Scott 是獨立顧問、訓練員和作者。 他的最新書籍是 Sams 在 24 小時內自行 ASP.NET 2.0。 您可以透過mitchell@4GuysFromRolla.com部落格來連線到 ,您可以在 找到http://ScottOnWriting.NET