共用方式為


有效率地分頁大量資料 (VB)

演講者: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 個頁碼。

使用預設分頁時,起始行索引計算為頁面索引和頁面大小加一的乘積,而最大行數只是頁面大小。 由於預設分頁在呈現任何資料頁時都會從資料庫中檢索所有記錄,因此每行的索引是已知的,從而使移動到起始行索引行成為一項簡單的任務。 此外,總記錄計數很容易獲得,因為它只是資料表 (或用於保存資料庫結果的任何物件) 中的記錄數。

給定起始行索引和最大行數變數,自訂分頁實作必須僅傳回從起始行索引開始的記錄的精確子集,之後的記錄數最多為最大行數。 自訂分頁帶來了兩個挑戰:

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

在接下來的兩個步驟中,我們將檢查應對這兩個挑戰所需的 SQL 指令碼。 除了 SQL 指令碼之外,我們還需要在 DAL 和 BLL 中實作方法。

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

在我們研究如何檢索正在顯示的頁面的精確記錄子集之前,讓我們先看看如何傳回正在分頁的記錄總數。 為了正確設定尋呼使用者介面,需要此資訊。 可以使用 COUNT 彙總函式取得特定 SQL 查詢傳回的記錄總數。 例如,要確定 Products 表中的記錄總數,我們可以使用下列查詢:

SELECT COUNT(*)
FROM Products

讓我們為 DAL 新增一個傳回此資訊的方法。 特別是,我們將建立一個名為 TotalNumberOfProducts() 的 DAL 方法來執行上面所示的 SELECT 陳述式。

首先開啟 App_Code/DAL 資料夾中的 Northwind.xsd 類型化資料集檔案。 接下來,以滑鼠右鍵按一下設計器中的 ProductsTableAdapter,並選擇「新增查詢」。 正如我們在先前的教學課程中所看到的,這將允許我們向 DAL 新增一個新方法,該方法在呼叫時將執行特定的 SQL 陳述式或預存程序。 與先前教學課程中的 TableAdapter 方法一樣,此方法選擇使用即席 SQL 陳述式。

使用臨機操作 SQL 陳述式

圖 1:使用臨機操作 SQL 陳述式

在下一個畫面上,我們可以指定要建立的查詢類型。 由於這個查詢將傳回一個單一的標量值,即 Products 表中的總記錄數,因此選擇傳回單一值的 SELECT 陳述式。

設定查詢以使用傳回單一值的 SELECT 陳述式

圖 2:設定查詢以使用傳回單一值的 SELECT 陳述式

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

使用 SELECT COUNT(*) FROM Products 查詢

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

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

將 DAL 方法命名為 TotalNumberOfProducts

圖 4:將 DAL 方法命名為 TotalNumberOfProducts

按一下「完成」後,精靈會將 TotalNumberOfProducts 方法新增至 DAL。 如果 SQL 查詢的結果是 NULL,則 DAL 中的標量傳回方法傳回可為 Null 的類型。 然而,我們的 COUNT 查詢將傳回一個非 NULL 的值;無論如何,DAL 方法都會傳回一個可為空的整數。

除了DAL方法之外,我們還需要BLL中的方法。 開啟 ProductsBLL 類別檔案並新增一個簡單呼叫 DAL TotalNumberOfProducts 方法的 TotalNumberOfProducts 方法:

Public Function TotalNumberOfProducts() As Integer
    Return Adapter.TotalNumberOfProducts().GetValueOrDefault()
End Function

DAL 的 TotalNumberOfProducts 方法會傳回一個可為空的整數;但是,我們建立了 ProductsBLL 類別的 TotalNumberOfProducts 方法,以便它傳回一個標準整數。 因此,我們需要讓 ProductsBLL 類別的 TotalNumberOfProducts 方法傳回 DAL 的 TotalNumberOfProducts 方法傳回的可空整數的值部分。 呼叫 GetValueOrDefault() 傳回可空整數的值 (如果存在) ;但是,如果可以為 Null 的整數為 null,則它會傳回預設整數值 0。

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

我們的下一個任務是在 DAL 和 BLL 中建立方法,接受前面討論的起始行索引和最大行變數並傳回適當的記錄。 在此之前,我們先來看看所需的 SQL 指令碼。 我們面臨的挑戰是必須能夠有效地為整個結果集中的每一行分配索引,以便我們能夠只傳回從起始行索引開始 (以及最多的記錄數) 的那些記錄。

如果資料庫表中已有一個作為行索引的列,那麼這就不成問題。 乍一看,我們可能認為 Products 表格的 ProductID 欄位就足夠了,因為第一個產品的 ProductID 是 1,第二個產品是 2,依此類推。 然而,刪除產品會在序列中留下間隙,使這種方法失效。

有兩種通用技術可用於有效地將行索引與要分頁的資料關聯起來,從而能夠檢索精確的記錄子集:

  • 使用 SQL Server 2005 中新增的 SQL Server 2005 的 ROW_NUMBER() 關鍵字 是 SQL Server 2005 中的新功能,ROW_NUMBER() 關鍵字會根據某種排序方式為每個傳回的記錄分配一個排名。 此排名可以用作每行的行索引。

  • 使用資料表變數和 SET ROWCOUNT SQL 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() 及其他排名函式的更詳細討論,請參閱 ROW_NUMBER 文件

當按照 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>)

注意

正如我們稍後將在本教學課程中看到的,ObjectDataSource 提供的StartRowIndex 是從零開始編號的,而 SQL Server 2005 傳回的 ROW_NUMBER() 值則從 1 開始編號的。 因此,WHERE 子句會傳回那些 PriceRank 大於 StartRowIndex 且小於或等於 StartRowIndex + MaximumRows 的記錄。

現在我們已經討論了如何使用 ROW_NUMBER() 來根據開始行索引和最大行數來檢索特定的資料頁面,我們現在需要在資料存取層 (DAL) 和商務邏輯層 (BLL) 中實現這個邏輯作為方法。

在建立此查詢時,我們必須決定結果的排序順序;讓我們按產品名稱的字母順序對產品進行排序。 這代表,透過本教學課程中的自訂分頁實現,我們將無法建立也可以排序的自訂分頁報告。 不過,在下一個教學課程中,我們將了解如何提供此類功能。

在上一節中,我們將 DAL 方法建立為臨時 SQL 陳述式。 不幸的是,Visual Studio 中的 T-SQL 解析器 (由 TableAdapter 向導使用) 不喜歡 ROW_NUMBER() 函式中使用的 OVER 語法。 因此,我們必須將此 DAL 方法建立為預存程序。 從「檢視」功能表中選擇「伺服器總管」(或按 Ctrl+Alt+S),然後展開 NORTHWND.MDF 節點。 若要新增新的預存程序,請以滑鼠右鍵按一下「預存程序」節點並選擇「新增新的預存程序」 (請參閱圖 6)。

新增新的預存程序以對產品進行分頁

圖 6:新增新的預存程序以對產品進行分頁

這個預存程式應該會接受兩個整數輸入參數,@startRowIndex@maximumRows,並使用 ROW_NUMBER() 函式,根據 ProductName 欄位進行排序,僅返回那些大於指定的 @startRowIndex 並且小於或等於 @startRowIndex + @maximumRow 的行數。 在新的預存程序中輸入以下指令碼,然後按一下「儲存」圖示將預存程序新增至資料庫。

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

建立預存程序後,花點時間對其進行測試。以滑鼠右鍵按一下伺服器總管中的 GetProductsPaged 預存程序名稱,然後選擇「執行」選項。 然後,Visual Studio 會提示您輸入參數 @startRowIndex@maximumRow (請參閱圖 7)。 嘗試不同的值並檢查結果。

輸入以下參數的值:<span class=@startRowIndex 和 @maximumRows 參數" />

圖 7:輸入 @startRowIndex 和 @maximumRows 參數的值

選擇這些輸入參數值後,輸出視窗將顯示結果。 圖 8 顯示了為 @startRowIndex@maximumRows 參數傳遞 10 時的結果。

傳回第二頁資料中出現的記錄

圖 8:傳回第二頁資料中出現的記錄 (點擊查看完整圖片)

建立此預存程序後,我們就可以建立 ProductsTableAdapter 方法了。 開啟 Northwind.xsd 類型化資料集,以滑鼠右鍵按一下 ProductsTableAdapter,然後選擇「新增查詢」選項。 不使用臨時 SQL 陳述式建立查詢,而是使用現有預存程序建立查詢。

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

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

接下來,系統會提示我們選擇要呼叫的預存程序。 從下拉式清單中選擇 GetProductsPaged 預存程序。

從下拉式清單中選擇 GetProductsPaged 預存程序

圖 10:從下拉式清單中選擇 GetProductsPaged 預存程序

然後,下一個畫面會詢問您預存程序傳回哪種類型的資料:表格資料、單一值或無值。 由於 GetProductsPaged 預存程序可以傳回多個記錄,因此指示它傳回表格資料。

指示預存程序傳回表格資料

圖 11:指示預存程序傳回表格資料

最後,指出您想要建立的方法的名稱。 與我們之前的教學課程一樣,繼續使用「填充資料表」和「返回資料表」建立方法。 命名第一個方法 FillPaged 和第二個方法 GetProductsPaged

將方法命名為 FillPaged 和 GetProductsPaged

圖 12:將方法命名為 FillPaged 和 GetProductsPaged

除了建立一個DAL方法來傳回特定的產品頁面之外,我們還需要在BLL中提供這樣的功能。 與 DAL 方法一樣,BLL 的 GetProductsPaged 方法必須接受兩個整數輸入來指定起始行索引和最大行數,並且必須只傳回位於指定範圍內的記錄。 在 ProductsBLL 類別中建立這樣一個 BLL 方法,該方法只會向下呼叫 DAL 的 GetProductsPaged 方法,如下所示:

<System.ComponentModel.DataObjectMethodAttribute( _
    System.ComponentModel.DataObjectMethodType.Select, False)> _
Public Function GetProductsPaged(startRowIndex As Integer, maximumRows As Integer) _
    As Northwind.ProductsDataTable
    Return Adapter.GetProductsPaged(startRowIndex, maximumRows)
End Function

您可以為 BLL 方法的輸入參數使用任何名稱,但是,正如我們稍後將看到的,選擇使用 startRowIndexmaximumRows 可以讓我們在設定 ObjectDataSource 以使用此方法時免於額外的工作。

步驟 4:設定 ObjectDataSource 以使用自訂分頁

完成用於存取特定記錄子集的 BLL 和 DAL 方法後,我們就可以建立一個 GridView 控制項,該控制項使用自訂分頁對其基礎記錄進行分頁。 首先開啟 PagingAndSorting 資料夾中的 EfficientPaging.aspx 頁面,將 GridView 新增至頁面,並將其設定為使用新的 ObjectDataSource 控制項。 在我們過去的教學課程中,我們經常將 ObjectDataSource 設定為使用 ProductsBLL 類別的 GetProducts 方法。 然而,這一次,我們想改用 GetProductsPaged 方法,因為 GetProducts 方法會傳回資料庫中的所有產品,而 GetProductsPaged 僅傳回記錄的特定子集。

設定 ObjectDataSource 以使用 ProductsBLL 類別 GetProductsPaged 方法

圖 13:設定 ObjectDataSource 以使用 ProductsBLL 類別 GetProductsPaged 方法

由於我們要建立唯讀 GridView,因此請花一些時間將 INSERT、UPDATE 和 DELETE 標籤中的方法下拉式清單設為 (無)。

接下來,ObjectDataSource 精靈提示我們輸入 GetProductsPaged 方法的 startRowIndex 來源和 maximumRows 輸入參數值。 這些輸入參數實際上將由 GridView 自動設定,因此只需將來源設定保留為「無」並按一下「完成」即可。

將輸入參數來源保留為 None

圖 14:將輸入參數來源保留為 None

完成 ObjectDataSource 精靈後,GridView 將包含每個產品資料欄位的 BoundField 或 CheckBoxField。 您可以根據需要隨意自訂 GridView 的外觀。 我選擇僅顯示 ProductNameCategoryNameSupplierNameQuantityPerUnitUnitPrice 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 作為 GetProductsPaged startRowIndexmaximumRows 輸入參數的值。 因此,產生的 SQL 查詢不會傳回任何記錄,因此不會顯示 GridView。

為了解決這個問題,我們需要將 ObjectDataSource 設定為使用自訂分頁。 這可以透過以下步驟完成:

  1. 將 ObjectDataSource 的 EnablePaging 屬性設定為 true,這表示 ObjectDataSource 必須傳遞兩個額外的參數給 SelectMethod,一個用來指定起始行索引 (StartRowIndexParameterName),另一個用來指定最大行數 (MaximumRowsParameterName)。
  2. 設定 ObjectDataSource 的 StartRowIndexParameterNameMaximumRowsParameterName 屬性StartRowIndexParameterNameMaximumRowsParameterName 屬性指示出於自訂分頁目的而傳遞到 SelectMethod 的輸入參數的名稱。 預設情況下,這些參數名稱是 startIndexRowmaximumRows,這就是為什麼在 BLL 中建立 GetProductsPaged 方法時,我會使用這些值作為輸入參數。 例如,如果您選擇為 BLL 的 GetProductsPaged 方法使用不同的參數名稱 (例如 startIndexmaxRows),則您需要相應地設定 ObjectDataSource 的 StartRowIndexParameterNameMaximumRowsParameterName 屬性 (例如將 StartRowIndexParameterName 設定為 startIndex,將 MaximumRowsParameterName 設定為 maxRows)。
  3. 將 ObjectDataSource 的 SelectCountMethod 屬性設定為傳回正在分頁的記錄總數的方法的名稱 (TotalNumberOfProducts) 回想一下,ProductsBLL 類別的 TotalNumberOfProducts 方法使用執行 SELECT COUNT(*) FROM Products 查詢的 DAL 方法傳回正在分頁的記錄總數。 ObjectDataSource 需要此資訊才能正確呈現分頁介面。
  4. 從 ObjectDataSource 宣告性標記中移除 startRowIndexmaximumRows<asp:Parameter> 元素透過精靈設定 ObjectDataSource 時,Visual Studio 會自動為 GetProductsPaged 方法的輸入參數新增兩個 <asp:Parameter> 元素。 透過將 EnablePaging 設為 true,這些參數將自動傳遞;如果它們也出現在宣告性語法中,則 ObjectDataSource 將嘗試將四個參數傳遞給 GetProductsPaged 方法,並將兩個參數傳遞給 TotalNumberOfProducts 方法。 如果您忘記刪除這些 <asp:Parameter> 元素,則在透過瀏覽器存取頁面時,您將收到錯誤訊息,例如:ObjectDataSource 'ObjectDataSource1' 找不到具有參數:startRowIndex、maximumRows 的非泛型方法 'TotalNumberOfProducts'

進行這些變更後,ObjectDataSource 的宣告式語法應如下所示:

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

請注意,EnablePagingSelectCountMethod 屬性已設定,且 <asp:Parameter> 元素已刪除。 圖 16 顯示了進行這些變更後「屬性」視窗的螢幕擷取畫面。

若要使用自訂分頁,請設定 ObjectDataSource 控制項

圖 16:若要使用自訂分頁,請設定 ObjectDataSource 控制項

進行這些變更後,透過瀏覽器造訪此頁面。 您應該會看到列出了 10 個產品,按字母順序排列。 花點時間一次一頁地瀏覽資料。 雖然從終端使用者的角度來看,預設分頁和自訂分頁之間沒有視覺差異,但自訂分頁可以更有效地對大量資料進行分頁,因為它只檢索需要為給定頁面顯示的那些記錄。

按產品名稱排序的資料使用自訂分頁進行分頁

圖 17:依產品名稱排序的資料使用自訂分頁進行分頁 (點擊查看完整圖片)

注意

透過自訂分頁,ObjectDataSource 的 SelectCountMethod 傳回的頁計數值會儲存在 GridView 的檢視狀態中。 其他 GridView 變數 PageIndexEditIndexSelectedIndexDataKeys 集合等儲存在 控制項狀態中,無論 GridView 的 EnableViewState 屬性的值為何,該狀態都會保留。 由於 PageCount 值使用檢視狀態在回傳過程中保持不變,因此當使用包含指向最後一頁的連結的分頁介面時,必須啟用 GridView 的檢視狀態。 (如果您的分頁介面不包含到最後一頁的直接連結,那麼您可以停用檢視狀態。)

點擊最後一頁連結會導致回傳並指示 GridView 更新其 PageIndex 屬性。 如果點擊最後一頁連結,GridView 會將其 PageIndex 屬性指派給比其 PageCount 屬性小一的值。 停用檢視狀態後,PageCount 值會在回傳過程中遺失,並且 PageIndex 會被指派最大整數值。 接下來,GridView 嘗試透過將 PageSizePageCount 屬性相乘來確定起始行索引。 由於乘積超出了允許的最大整數大小,因此會產生 OverflowException

實作自訂分頁和排序

我們目前的自訂分頁實作要求在建立 GetProductsPaged 預存程序時靜態指定資料分頁的順序。 但是,您可能已經注意到,除了「啟用分頁」選項之外,GridView 的智慧標籤還包含「啟用排序」核取方塊。 不幸的是,使用我們目前的自訂分頁實作來向 GridView 新增排序支援只會對目前查看的資料頁面上的記錄進行排序。 例如,如果您將 GridView 設定為也支援分頁,然後在查看第一頁資料時,按產品名稱降序排序,則會將第一頁上的產品順序顛倒過來。 如圖 18 所示,按字母順序反向排序時,Carnarvon Tigers 顯示為第一個產品,忽略了按字母順序排在 Carnarvon Tigers 之後的 71 個其他產品;排序時僅考慮第一頁的記錄。

僅對目前頁面顯示的資料進行排序

圖 18:僅對目前頁面顯示的資料進行排序 (點擊查看完整圖片)

排序僅適用於目前資料頁,因為排序是在從 BLL 的 GetProductsPaged 方法檢索資料之後發生的,且該方法僅傳回特定頁的記錄。 為了正確實現排序,我們需要將排序運算式傳遞給 GetProductsPaged 方法,以便在返回特定資料頁之前對資料進行適當的排序。 我們將在下一個教學課程中了解如何實現這一點。

實作自訂分頁和刪除

如果您在使用自訂分頁技術對資料進行分頁的 GridView 中啟用刪除功能,您會發現當最後一頁刪除最後一筆記錄時,GridView 會消失,而不是適當減少 GridView 的 PageIndex。 若要重現此錯誤,請啟用我們剛剛建立的教學課程的刪除功能。 移至最後一頁 (第 9 頁),您應該在其中看到單一產品,因為我們一次分頁瀏覽 81 個產品,一次 10 個產品。 刪除該產品。

刪除最後一個產品後,GridView 應該會自動移至第八頁,並且透過預設分頁展示此功能。 但是,使用自訂分頁時,刪除最後一頁上的最後一個產品後,GridView 就會從螢幕上完全消失。 發生這種情況的確切原因有點超出了本教學課程的範圍;有關此問題根源的詳細資訊,請參閱「使用自訂分頁從 GridView 中刪除最後一頁上的最後一筆記錄」。 總之,這是由於按一下「刪除」按鈕時 GridView 執行的以下步驟順序造成的:

  1. 刪除記錄
  2. 取得適當的記錄以顯示指定的 PageIndexPageSize
  3. 檢查以確保 PageIndex 不超過資料來源中資料的頁數;如果是,則自動減少 GridView 的 PageIndex 屬性
  4. 使用步驟 2 中獲得的記錄將適當的資料頁繫結到 GridView

問題出在第二步中,用於抓取要顯示的記錄的 PageIndex 仍然是最後一頁的 PageIndex,而該頁面中的唯一記錄剛剛被刪除。 因此,在步驟 2 中,不會傳回任何記錄,因為最後一頁資料不再包含任何記錄。 然後,在步驟 3 中,GridView 意識到其 PageIndex 屬性大於資料來源中的總頁數 (因為我們已經刪除了最後一頁的最後一筆記錄),因此會遞減其 PageIndex 屬性。 在步驟 4 中,GridView 會嘗試將自己繫結到第二步中檢索到的資料;然而,在第二步中沒有返回任何記錄,因此會導致 GridView 為空。 使用預設分頁時,不會出現此問題,因為在步驟 2 中,所有記錄都是從資料來源擷取的。

為了解決這個問題,我們有兩個選擇。 第一個是為 GridView 的 RowDeleted 事件處理常式建立事件處理常式,該處理常式確定剛剛刪除的頁面中顯示了多少筆記錄。 如果只有一筆記錄,那麼剛剛刪除的記錄一定是最後一筆記錄,我們需要遞減 GridView 的 PageIndex。 當然,我們只想在刪除操作實際成功時才更新 PageIndex,這可以透過確保 e.Exception 屬性為 null 來確定。

這種方法有效,因為它在步驟 1 之後但在步驟 2 之前更新了 PageIndex。 因此,在步驟 2 中,傳回適當的記錄集。 要實現此目的,請使用以下程式碼:

Protected Sub GridView1_RowDeleted(sender As Object, e As GridViewDeletedEventArgs) _
    Handles GridView1.RowDeleted
    ' If we just deleted the last row in the GridView, decrement the PageIndex
    If e.Exception Is Nothing AndAlso GridView1.Rows.Count = 1 Then
        ' we just deleted the last row
        GridView1.PageIndex = Math.Max(0, GridView1.PageIndex - 1)
    End If
End Sub

另一種解決方法是為 ObjectDataSource 的 RowDeleted 事件建立事件處理常式並將 AffectedRows 屬性設為值 1。 在步驟 1 中刪除記錄後 (但在步驟 2 中重新檢索資料之前),如果一行或多行受到該操作的影響,GridView 會更新其 PageIndex 屬性。 但是,該 AffectedRows 屬性不是由 ObjectDataSource 設定的,因此省略了此步驟。 執行此步驟的一種方法是,如果刪除操作成功完成,則手動設定 AffectedRows 屬性。 這可以使用以下程式碼來完成:

Protected Sub ObjectDataSource1_Deleted( _
    sender As Object, e As ObjectDataSourceStatusEventArgs) _
    Handles ObjectDataSource1.Deleted
    ' 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 TypeOf e.ReturnValue Is Boolean AndAlso CType(e.ReturnValue, Boolean) = True Then
        e.AffectedRows = 1
    End If
End Sub

這兩個事件處理常式的程式碼可以在 EfficientPaging.aspx 範例的程式碼隱藏類別中找到。

比較預設分頁和自訂分頁的效能

由於自訂分頁僅擷取所需的記錄,而預設分頁會傳回正在查看的每個頁面的所有記錄,因此很明顯自訂分頁比預設分頁更有效。 但自訂分頁的效率到底有多高呢? 從預設分頁轉移到自訂分頁可以看到什麼樣的效能提升?

不幸的是,這裡沒有一個適合所有情況的答案。 效能增益取決於許多因素,其中最重要的兩個因素是分頁的記錄數量、資料庫伺服器上的負載以及 Web 伺服器和資料庫伺服器之間的通訊通道。 對於只有幾十筆記錄的小型表,效能差異可能可以忽略不計。 然而,對於具有數千到數十萬行的大型表,效能差異非常嚴重。

我寫的一篇文章「使用 SQL Server 2005 在 ASP.NET 2.0 中進行自訂分頁」中包含了一些效能測試,展示了在通過包含 50,000 筆記錄的資料庫表進行分頁時,這兩種分頁技術的效能差異。 在這些測試中,我檢查了在 SQL Server 層級 (使用 SQL Profiler) 和使用 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

摘要

預設分頁的實作非常簡單,只需在資料 Web 控制項的智慧標籤中勾選「啟用分頁」選項,但這種簡單性是以效能為代價的。 使用預設分頁時,當使用者要求任何資料頁時,所有記錄都會傳回,即使只顯示其中的一小部分。 為了因應這種效能負荷,ObjectDataSource 提供了另一個分頁選項自訂分頁。

雖然自訂分頁透過僅檢索那些需要顯示的記錄來改善預設分頁的效能問題,但實現自訂分頁需要更多的工作。 首先,必須編寫一個查詢來正確 (且有效) 地存取所要求的特定記錄子集。 這可以透過多種方式實現;我們在本教學課程中研究的是使用 SQL Server 2005 的新 ROW_NUMBER() 函式對結果進行排名,然後僅傳回排名在指定範圍內的結果。 此外,我們需要新增一種方法來確定分頁的記錄總數。 建立這些 DAL 和 BLL 方法後,我們還需要設定 ObjectDataSource,以便它可以確定正在分頁的總記錄數,並且可以正確地將起始行索引和最大行值傳遞給 BLL。

雖然實作自訂分頁確實需要許多步驟,而且不像預設分頁那麼簡單,但當分頁足夠大量的資料時,自訂分頁是必要的。 如檢查結果所示,自訂分頁可以將 ASP.NET 頁面渲染時間縮短幾秒鐘,並且可以將資料庫伺服器上的負載減輕一個或多個數量級。

快樂程式!

關於作者

Scott Mitchell 是七本 ASP/ASP.NET 書籍的作者,也是 4GuysFromRolla.com 的創辦人,自 1998 年以來一直在使用 Microsoft 網路技術。 Scott 是一位獨立顧問、培訓師兼作家。 他的最新著作是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 您可以透過電子郵件聯繫他:mitchell@4GuysFromRolla.com。 或者透過他的部落格與他聯繫,網址為 http://ScottOnWriting.NET