通過大量數據有效分頁 (C#)
處理大量數據時,數據呈現控件的默認分頁選項是不合適的,因爲它的基礎數據源控件檢索所有記錄,即使只顯示一部分數據。 在這種情況下,我們必須轉向自定義分頁。
介紹
如前面的教程中所述,分頁可以通過以下兩種方式之一實現:
- 只需在數據 Web 控件智能標記中檢查“啓用分頁”選項即可實現默認分頁;但是,每當查看數據頁時,ObjectDataSource 都會檢索所有記錄,即使只有一部分記錄顯示在頁面中
- 自定義分頁 通過僅從數據庫中檢索需要爲用戶請求的特定數據頁顯示的記錄來提高默認分頁的性能:但是,自定義分頁比默認分頁更努力實現
由於實現簡單,只需選中一個複選框即可完成! 默認分頁是一個有吸引力的選項。 不過,檢索所有記錄的天真方法使得在分頁足夠大的數據或具有許多併發用戶的站點時,這是一個不可冒犯的選擇。 在這種情況下,我們必須轉向自定義分頁,以提供響應式系統。
自定義分頁的挑戰是能夠編寫一個查詢,該查詢返回特定數據頁所需的精確記錄集。 幸運的是,Microsoft SQL Server 2005 提供了一個新的關鍵字來排名結果,這使我們可以編寫一個查詢,該查詢可以有效地檢索正確的記錄子集。 本教程介紹如何使用此新的 SQL Server 2005 關鍵字在 GridView 控件中實現自定義分頁。 雖然自定義分頁的用戶界面與默認分頁的用戶界面相同,但使用自定義分頁從一個頁面單步到下一頁的速度可能比默認分頁快幾倍。
備註
自定義分頁所表現出的確切性能取決於正在分頁的記錄總數以及要放置在數據庫服務器上的負載。 在本教程結束時,我們將介紹一些粗略的指標,這些指標展示了通過自定義分頁獲得的性能優勢。
步驟 1:瞭解自定義分頁過程
分頁數據時,頁面上顯示的精確記錄取決於所請求的數據頁以及每頁顯示的記錄數。 例如,假設我們想要瀏覽 81 個產品,每頁顯示 10 個產品。 查看第一頁時,我們希望產品 1 到 10;查看第二頁時,我們對產品 11 到 20 感興趣,等等。
有三個變量指示需要檢索哪些記錄以及如何呈現分頁接口:
- 開始行索引 要顯示的數據頁中第一行的索引;可以通過將頁面索引乘以每頁顯示並添加一個記錄來計算此索引。 例如,當一次分頁記錄 10 時,對於第一頁 (其頁面索引爲 0) ,起始行索引爲 0 * 10 + 1 或 1;對於第二頁 (其頁面索引爲 1) ,起始行索引爲 1 * 10 + 1 或 11。
- 最大行 數是每頁要顯示的最大記錄數。 此變量稱爲自上一頁以來的最大行數,返回的記錄數可能少於頁面大小。 例如,每頁分頁 81 個產品 10 條記錄時,第九頁和最後一頁只包含一條記錄。 不過,沒有頁面顯示的記錄數超過“最大行數”值。
- 總記錄計數 正在分頁的記錄總數。 雖然此變量不需要確定要檢索給定頁面的記錄,但它確實決定了分頁接口。 例如,如果有 81 個正在分頁的產品,則分頁界面知道在分頁 UI 中顯示 9 個頁碼。
使用默認分頁時,起始行索引將計算爲頁面索引的乘積和頁面大小加上一個,而最大行只是頁面大小。 由於默認分頁在呈現任何數據頁時從數據庫檢索所有記錄,因此已知每行的索引,從而使移動到“開始行索引”行是一個微不足道的任務。 此外,“總記錄計數”隨時可用,因爲它只是 DataTable (中的記錄數或用於保存數據庫結果的任何對象) 。
給定起始行索引和最大行變量,自定義分頁實現只能返回從起始行索引開始的記錄的精確子集,之後最多返回最大行數的記錄數。 自定義分頁提供兩個難題:
- 我們必須能夠有效地將行索引與正在分頁的整個數據中的每個行相關聯,以便我們可以開始在指定的起始行索引處返回記錄
- 我們需要提供正在分頁的記錄總數
在接下來的兩個步驟中,我們將檢查響應這兩個挑戰所需的 SQL 腳本。 除了 SQL 腳本,我們還需要在 DAL 和 BLL 中實現方法。
步驟 2:返回正在分頁的記錄總數
在檢查如何檢索所顯示頁面的記錄的精確子集之前,讓我們先看看如何返回正在分頁的記錄總數。 需要此信息才能正確配置分頁用戶界面。 可以使用聚合函數獲取COUNT
特定 SQL 查詢返回的記錄總數。 例如,若要確定表中的記錄 Products
總數,可以使用以下查詢:
SELECT COUNT(*)
FROM Products
讓我們向 DAL 添加一個返回此信息的方法。 具體而言,我們將創建一個調用 TotalNumberOfProducts()
的 DAL 方法,用於執行 SELECT
上面所示的語句。
首先,在 Northwind.xsd
文件夾中打開類型化 DataSet 文件 App_Code/DAL
。 接下來,右鍵單擊設計器中的“ ProductsTableAdapter
添加查詢”。 正如我們在前面的教程中看到的那樣,這將使我們能夠向 DAL 添加新方法,在調用時,將執行特定的 SQL 語句或存儲過程。 與前面的教程中的 TableAdapter 方法一樣,爲此,選擇使用即席 SQL 語句。
圖 1:使用即席 SQL 語句
在下一個屏幕上,我們可以指定要創建的查詢類型。 由於此查詢將返回單個記錄,因此表中記錄 Products
總數的標量值選擇 SELECT
返回一個單一值選項。
圖 2:將查詢配置爲使用返回單個值的 SELECT 語句
指示要使用的查詢類型後,接下來必須指定查詢。
圖 3:使用 SELECT COUNT (*) FROM 產品查詢
最後,指定方法的名稱。 如前所述,讓我們使用 TotalNumberOfProducts
。
圖 4:命名 DAL 方法 TotalNumberOfProducts
單擊“完成”後,嚮導會將該方法添加到 TotalNumberOfProducts
DAL。 如果 SQL 查詢的結果是 NULL
,DAL 中的標量返回方法返回可爲 null 的類型。 但是,我們的 COUNT
查詢將始終返回一個非NULL
值;不管怎樣,DAL 方法都會返回可爲 null 的整數。
除了 DAL 方法外,我們還需要 BLL 中的方法。 ProductsBLL
打開類文件並添加一個TotalNumberOfProducts
僅調用 DAL 方法TotalNumberOfProducts
的方法:
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 ROWCOUNT
SQL Server sSET 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()
使用以下語法將排名與通過特定排序返回的每個記錄相關聯:
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 返回排名結果。
在子句UnitPrice
(中OVER
按指定ORDER BY
列對結果進行排名時,在上述示例中) ,SQL Server必須對結果進行排序。 如果列上有聚集索引, () 結果按順序排序,或者有覆蓋索引,但成本可能更高,則這是一種快速操作。 爲了幫助提高足夠大型查詢的性能,請考慮爲按其排序結果的列添加非聚集索引。 有關性能注意事項的詳細信息,請參閱 2005 SQL Server中的排名函數和性能。
不能直接在子句中使用由其 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>)
備註
如本教程稍後所述,StartRowIndex
ObjectDataSource 提供的索引從零開始,而 ROW_NUMBER()
SQL Server 2005 返回的值從 1 開始編制索引。 因此,子WHERE
句將返回嚴格大於StartRowIndex
和小於或等於StartRowIndex
+ MaximumRows
的記錄。PriceRank
現在,我們已經討論瞭如何在 ROW_NUMBER()
給定起始行索引和最大值行值的情況下檢索特定數據頁,現在我們需要在 DAL 和 BLL 中將此邏輯實現爲方法。
創建此查詢時,必須確定結果的排名順序;讓我們按其名稱按字母順序對產品進行排序。 這意味着,使用本教程中的自定義分頁實現,我們將無法創建自定義分頁報表,也不能對報表進行排序。 不過,在下一教程中,我們將瞭解如何提供此類功能。
在上一部分中,我們創建了 DAL 方法作爲即席 SQL 語句。 遺憾的是,TableAdapter 嚮導使用的 Visual Studio 中的 T-SQL 分析程序不喜歡 OVER
函數使用的 ROW_NUMBER()
語法。 因此,我們必須將此 DAL 方法創建爲存儲過程。 從“視圖”菜單中選擇“服務器資源管理器”, (或按 Ctrl+Alt+S) 並展開 NORTHWND.MDF
節點。 若要添加新存儲過程,請右鍵單擊“存儲過程”節點,然後選擇“添加新存儲過程” (請參閱圖 6) 。
圖 6:爲通過產品分頁添加新存儲過程
此存儲過程應接受兩個整數輸入參數,@startRowIndex
並使用@maximumRows
ROW_NUMBER()
按字段排序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
(請參閱圖 7) 。 嘗試不同的值並檢查結果。
@startRowIndex 和 @maximumRows 參數的值“/>
圖 7:輸入 @startRowIndex 值和 @maximumRows 參數
選擇這些輸入參數值後,“輸出”窗口將顯示結果。 圖 8 顯示了傳入 10 個參數 @startRowIndex
和 @maximumRows
參數時的結果。
圖 8:返回第二頁數據中顯示的記錄 (單擊以查看全尺寸圖像)
創建此存儲過程後,我們便可以創建 ProductsTableAdapter
該方法。 Northwind.xsd
打開類型化數據集,右鍵單擊,ProductsTableAdapter
然後選擇“添加查詢”選項。 不使用即席 SQL 語句創建查詢,而是使用現有的存儲過程創建查詢。
圖 9:使用現有存儲過程創建 DAL 方法
接下來,系統會提示選擇要調用的存儲過程。 GetProductsPaged
從下拉列表中選擇存儲過程。
圖 10:從Drop-Down列表中選擇 GetProductsPaged 存儲過程
然後,下一個屏幕會詢問存儲過程返回的數據類型:表格數據、單個值或無值。 GetProductsPaged
由於存儲過程可以返回多個記錄,因此指示它返回表格數據。
圖 11:指示存儲過程返回表格數據
最後,指示要創建的方法的名稱。 與前面的教程一樣,請繼續使用“填充 DataTable”和“返回 DataTable”創建方法。 將第一個方法命名爲第一個方法和 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 以使用此方法時,我們將選擇使用 startRowIndex
並 maximumRows
保存額外的工作位。
步驟 4:將 ObjectDataSource 配置爲使用自定義分頁
通過 BLL 和 DAL 方法來訪問特定子集的記錄完成,我們準備使用自定義分頁創建一個 GridView 控件,該控件通過其基礎記錄進行分頁。 首先 EfficientPaging.aspx
打開 PagingAndSorting
文件夾中的頁面,將 GridView 添加到頁面,並將其配置爲使用新的 ObjectDataSource 控件。 在過去教程中,我們通常已將 ObjectDataSource 配置爲使用 ProductsBLL
類 s GetProducts
方法。 但是,這一次,我們希望改用該方法 GetProductsPaged
,因爲該方法 GetProducts
返回數據庫中 的所有 產品,而 GetProductsPaged
只返回特定記錄子集。
圖 13:將 ObjectDataSource 配置爲使用 ProductsBLL 類 s GetProductsPaged 方法
由於我們重新創建只讀 GridView,因此請花點時間在 INSERT、UPDATE 和 DELETE 選項卡中設置方法下拉列表,以 (None) 。
接下來,ObjectDataSource 嚮導會提示我們輸入方法startRowIndex
的GetProductsPaged
源和maximumRows
輸入參數值。 這些輸入參數實際上將由 GridView 自動設置,因此只需將源設置爲“無”並單擊“完成”。
圖 14:將輸入參數源保留爲 None
完成 ObjectDataSource 嚮導後,GridView 將包含每個產品數據字段的 BoundField 或 CheckBoxField。 隨時根據需要定製 GridView 的外觀。 我選擇僅ProductName
顯示、CategoryName
SupplierName
、和 QuantityPerUnit
UnitPrice
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 找不到任何位置。
圖 15:未顯示 GridView
GridView 缺失,因爲 ObjectDataSource 當前使用 0 作爲輸入 GetProductsPaged
startRowIndex
參數和 maximumRows
輸入參數的值。 因此,生成的 SQL 查詢不返回任何記錄,因此不顯示 GridView。
若要解決此問題,我們需要將 ObjectDataSource 配置爲使用自定義分頁。 可通過以下步驟完成此操作:
- 將 ObjectDataSource 屬性
EnablePaging
設置爲true
這表示必須傳遞給SelectMethod
ObjectDataSource 的兩個附加參數:一個用於指定起始行索引 (StartRowIndexParameterName) ,另一個用於指定最大行數 (MaximumRowsParameterName) 。 - 相應地設置 ObjectDataSource 和
StartRowIndexParameterName
MaximumRowsParameterName
Properties,以及MaximumRowsParameterName
屬性指示傳入的SelectMethod
輸入參數的名稱,以便進行自定義分頁。StartRowIndexParameterName
默認情況下,這些參數名稱是startIndexRow
,maximumRows
這就是爲什麼在 BLL 中創建GetProductsPaged
方法時,我爲輸入參數使用了這些值。 如果選擇對 BLL 方法GetProductsPaged
startIndex
maxRows
使用不同的參數名稱,例如,需要相應地設置 ObjectDataSource sStartRowIndexParameterName
和MaximumRowsParameterName
屬性 (,如 startIndex forStartRowIndexParameterName
和 maxRows forMaximumRowsParameterName
) 。 - 將 ObjectDataSource s
SelectCountMethod
屬性 設置爲返回通過 (TotalNumberOfProducts
分頁記錄總數的方法的名稱) 回想一下,ProductsBLL
類方法TotalNumberOfProducts
返回使用執行查詢的 DAL 方法SELECT COUNT(*) FROM Products
分頁記錄總數。 ObjectDataSource 需要此信息才能正確呈現分頁接口。 - 通過嚮導配置 ObjectDataSource 時,從 ObjectDataSource 聲明性標記中刪除和
maximumRows
<asp:Parameter>
元素,Visual Studio 會自動爲方法的輸入參數添加兩個元素。startRowIndex
<asp:Parameter>
GetProductsPaged
通過設置爲EnablePaging
true
,這些參數將自動傳遞;如果這些參數也出現在聲明性語法中,ObjectDataSource 將嘗試將 四 個參數傳遞給該方法,並將兩個參數GetProductsPaged
傳遞給TotalNumberOfProducts
該方法。 如果忘記刪除這些<asp:Parameter>
元素,在通過瀏覽器訪問頁面時,將收到如下錯誤消息: ObjectDataSource 'ObjectDataSource1' 找不到參數爲 startRowIndex、maximumRows 的非泛型方法“TotalNumberOfProducts”。
進行這些更改後,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 顯示了這些更改後okno Vlastnosti的屏幕截圖。
圖 16:若要使用自定義分頁,請配置 ObjectDataSource 控件
進行這些更改後,通過瀏覽器訪問此頁面。 應會看到 10 個按字母順序列出的產品。 花點時間一次逐頁瀏覽數據。 雖然與最終用戶在默認分頁和自定義分頁之間沒有視覺差異,但自定義分頁通過大量數據更有效地頁面,因爲它只檢索需要爲給定頁面顯示這些記錄。
圖 17:按產品名稱排序的數據使用自定義分頁 (單擊以查看全尺寸圖像)
備註
使用自定義分頁時,ObjectDataSource 返回的 SelectCountMethod
頁計數值存儲在 GridView 的視圖狀態中。 其他 GridView 變量、PageIndex
EditIndex
集合SelectedIndex
DataKeys
等存儲在控件狀態中,無論 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 執行的以下步驟序列造成的:
- 刪除記錄
- 獲取要爲指定
PageIndex
和PageSize
- 檢查以確保
PageIndex
數據源中的數據頁數不超過;如果這樣做,則自動遞減 GridView 的屬性PageIndex
- 使用步驟 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 中,將返回相應的記錄集。 若要完成此操作,請使用如下所示的代碼:
protected void GridView1_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
// If we just deleted the last row in the GridView, decrement the PageIndex
if (e.Exception == null && GridView1.Rows.Count == 1)
// we just deleted the last row
GridView1.PageIndex = Math.Max(0, GridView1.PageIndex - 1);
}
另一種解決方法是爲 ObjectDataSource 事件 RowDeleted
創建事件處理程序,並將屬性設置爲 AffectedRows
值 1。 刪除步驟 1 (中的記錄,但在重新檢索步驟 2) 中的數據之前,如果一行或多行受操作影響,GridView 會更新其 PageIndex
屬性。 但是, AffectedRows
ObjectDataSource 不會設置該屬性,因此省略此步驟。 執行此步驟的一種方法是,如果刪除操作成功完成,則手動設置 AffectedRows
屬性。 可以使用如下所示的代碼來實現此目的:
protected void ObjectDataSource1_Deleted(
object sender, ObjectDataSourceStatusEventArgs e)
{
// If we get back a Boolean value from the DeleteProduct method and it's true,
// then we successfully deleted the product. Set AffectedRows to 1
if (e.ReturnValue is bool && ((bool)e.ReturnValue) == true)
e.AffectedRows = 1;
}
這兩個事件處理程序的代碼都可以在示例的代碼隱藏類 EfficientPaging.aspx
中找到。
比較默認分頁和自定義分頁的性能
由於自定義分頁僅檢索所需的記錄,而默認分頁返回正在查看的每個頁面 的所有 記錄,因此很明顯,自定義分頁比默認分頁更高效。 但是自定義分頁的效率要高多少? 通過從默認分頁移動到自定義分頁,可以看到哪種性能提升?
不幸的是,這裏沒有一個大小適合所有答案。 性能提升取決於多種因素,最突出的兩個因素是正在分頁的記錄數,以及放置在數據庫服務器上的負載以及 Web 服務器和數據庫服務器之間的通信通道。 對於只有幾十條記錄的小表,性能差異可能微不足道。 但是,對於大型表,有數千到數十萬行,但性能差異非常嚴重。
我的一篇文章“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#)
在上一教程中,我們學習瞭如何在網頁上呈現數據時實現自定義分頁。 本教程介紹如何擴展前面的示例,以包括對自定義分頁進行排序的支持。
介紹
與默認分頁相比,自定義分頁可以通過多個數量級提高分頁的性能,使自定義分頁在分頁時選擇事實上的分頁實現選擇。 但是,實現自定義分頁比實現默認分頁更爲複雜,尤其是在向混合中添加排序時。 在本教程中,我們將擴展前面的示例,以包括對排序 和 自定義分頁的支持。
備註
由於本教程基於前面的網頁,因此在開始之前需要一些時間從前面的教程網頁中複製元素中的聲明性語法<asp:Content>
, (EfficientPaging.aspx
) 並將其粘貼到頁面中的<asp:Content>
SortParameter.aspx
元素之間。 有關將一個 ASP.NET 頁面的功能複製到另一個頁面的更詳細討論,請參閱 “將驗證控件添加到編輯和插入接口 ”教程的步驟 1。
步驟 1:重新驗證自定義分頁技術
若要使自定義分頁正常工作,我們必須實現一些技術,這些技術可以有效地獲取給定起始行索引和最大行參數的特定記錄子集。 有一些技術可用於實現此目標。 在前面的教程中,我們介紹瞭如何使用 Microsoft SQL Server 2005 s 的新ROW_NUMBER()
排名函數來完成此操作。 簡言之, ROW_NUMBER()
排名函數將行號分配給由按指定排序順序排名的查詢返回的每一行。 然後,通過返回編號結果的特定部分來獲取相應的記錄子集。 以下查詢演示瞭如何使用此方法返回按字母順序 ProductName
排序的結果時編號爲 11 到 20 的產品:
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
參數,如下所示:
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 SQLCASE
語句中的“用於動態排序查詢結果”部分。- 在存儲過程中將適當的查詢創建爲字符串,然後使用
sp_executesql
系統存儲過程執行動態查詢。
其中每個解決方法都有一些缺點。 第一個選項不像其他兩個選項一樣可維護,因爲它要求爲每個可能的排序表達式創建查詢。 因此,如果以後決定向 GridView 添加新的可排序字段,則還需要返回並更新存儲過程。 第二種方法有一些微妙之處,在按非字符串數據庫列進行排序時引入性能問題,並且與第一種方法具有相同的可維護性問題。 使用動態 SQL 的第三個選項會引入 SQL 注入攻擊的風險,如果攻擊者能夠執行傳入所選輸入參數值的存儲過程。
雖然這些方法都不是完美的,但我認爲第三個選項是三個選項中最好的。 使用動態 SQL 時,它提供了其他兩個不具有靈活性的級別。 此外,只有在攻擊者能夠執行傳入所選輸入參數的存儲過程時,才能利用 SQL 注入攻擊。 由於 DAL 使用參數化查詢,因此 ADO.NET 將保護通過體系結構發送到數據庫的這些參數,這意味着只有在攻擊者可以直接執行存儲過程時才存在 SQL 注入攻擊漏洞。
若要實現此功能,請在名爲 GetProductsPagedAndSorted
Northwind 數據庫中創建新的存儲過程。 此存儲過程應接受三個輸入參數:@sortExpression
一個類型nvarchar(100
爲) 的輸入參數,用於指定結果的排序方式,並在子句中的OVER
文本之後ORDER BY
直接注入;以及@startRowIndex
@maximumRows
上一教程中檢查的存儲過程中的相同兩個整數輸入參數GetProductsPaged
。 GetProductsPagedAndSorted
使用以下腳本創建存儲過程:
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
請從下一屏幕的下拉列表中選擇存儲過程。
圖 4:使用 GetProductsPagedAndSorted 存儲過程
此存儲過程將返回一組記錄作爲其結果,因此在下一個屏幕中,指示它返回表格數據。
圖 5:指示存儲過程返回表格數據
最後,創建使用 Fill a DataTable 和 Return a DataTable 模式的 DAL 方法,分別命名方法和FillPagedAndSorted
GetProductsPagedAndSorted
方法。
圖 6:選擇方法名稱
現在,我們已經擴展了 DAL,我們準備轉向 BLL。 ProductsBLL
打開類文件並添加新方法GetProductsPagedAndSorted
。 此方法需要接受三個輸入參數 sortExpression
, startRowIndex
並且 maximumRows
應該直接調用 DAL s GetProductsPagedAndSorted
方法,如下所示:
[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 更改爲 SelectMethod
GetProductsPaged
GetProductsPagedAndSorted
. 這可以通過“配置數據源”嚮導、從okno Vlastnosti或通過聲明性語法直接完成。 接下來,我們需要提供 ObjectDataSource 屬性SortParameterName
的值。 如果設置了此屬性,ObjectDataSource 會嘗試將 GridView 屬性 SortExpression
傳遞到該 SelectMethod
屬性。 具體而言,ObjectDataSource 會查找名稱等於屬性值的 SortParameterName
輸入參數。 由於 BLL s GetProductsPagedAndSorted
方法具有命名 sortExpression
的排序表達式輸入參數,請將 ObjectDataSource s SortExpression
屬性設置爲 sortExpression。
進行這兩項更改後,ObjectDataSource 的聲明性語法應如下所示:
<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 屬性AllowSorting
true
設置爲 ,並導致每個列的標題文本呈現爲 LinkButton。 當最終用戶單擊其中一個標頭 LinkButton 時,隨後會出現回發並執行以下步驟:
- GridView 將其
SortExpression
屬性更新爲單擊其標頭鏈接的字段的值SortExpression
- ObjectDataSource 調用 BLL s
GetProductsPagedAndSorted
方法,將 GridView 屬性SortExpression
作爲方法sortExpression
輸入參數的值傳入 (以及) 的相應startRowIndex
輸入maximumRows
參數值 - BLL 調用 DAL 方法
GetProductsPagedAndSorted
- DAL 執行
GetProductsPagedAndSorted
存儲過程,@sortExpression
傳入參數 (以及@startRowIndex
@maximumRows
輸入參數值) - 存儲過程將適當的數據子集返回到 BLL,該子集將其返回到 ObjectDataSource;然後,此數據綁定到 GridView,呈現爲 HTML,併發送到最終用戶
圖 7 顯示按升序排序 UnitPrice
時結果的第一頁。
圖 7:結果按 UnitPrice 排序 (單擊以查看全尺寸圖像)
雖然當前實現可以按產品名稱、類別名稱、單位數量和單價正確對結果進行排序,但嘗試按供應商名稱對結果進行排序會導致運行時異常, (請參閱圖 8) 。
圖 8:嘗試按供應商結果在以下運行時異常中對結果進行排序
發生此異常的原因是 SortExpression
GridView s SupplierName
BoundField 設置爲 SupplierName
。 但是,表中的供應商名稱實際上稱爲CompanyName
此列名。Suppliers
SupplierName
但是, OVER
函數使用的 ROW_NUMBER()
子句不能使用別名,並且必須使用實際的列名。 因此,將 SupplierName
BoundField s SortExpression
從 SupplierName 更改爲 CompanyName (請參閱圖 9) 。 如圖 10 所示,在此更改後,結果可由供應商排序。
圖 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#)
顯示排序數據長列表時,通過引入分隔符行對相關數據進行分組非常有用。 本教程介紹如何創建此類排序用戶界面。
介紹
在顯示排序列中只有少量不同值的已排序數據的長列表時,最終用戶可能會發現很難辨別出差異邊界的位置(確切地說)。 例如,數據庫中有 81 個產品,但只有 9 個不同的類別選項 (8 個唯一類別加上 NULL
選項) 。 請考慮有興趣檢查屬於“海鮮”類別的產品的用戶的情況。 從列出單個 GridView 中的所有 產品的頁面中,用戶可能會決定她的最佳選擇是按類別對結果進行排序,這將將所有海鮮產品組合在一起。 按類別排序後,用戶需要搜索列表,查找海鮮分組產品開始和結束的位置。 由於結果按類別名稱按類別名稱按字母順序排序,發現海鮮產品並不困難,但它仍然需要仔細掃描網格中的項目列表。
爲了幫助突出顯示排序組之間的邊界,許多網站使用用戶界面來添加此類組之間的分隔符。 與圖 1 中顯示的分隔符一樣,用戶能夠更快地找到特定組並識別其邊界,並確定數據中是否存在不同的組。
圖 1:已明確標識每個類別組 (單擊以查看全尺寸圖像)
本教程介紹如何創建此類排序用戶界面。
步驟 1:創建標準、可排序的 GridView
在瞭解如何擴充 GridView 以提供增強的排序接口之前,讓我們先創建列出產品的標準可排序 GridView。 首先,在 CustomSortingUI.aspx
文件夾中打開頁面 PagingAndSorting
。 將 GridView 添加到頁面,將其 ID
屬性 ProductList
設置爲 ,並將其綁定到新的 ObjectDataSource。 將 ObjectDataSource 配置爲使用 ProductsBLL
類 s GetProducts()
方法來選擇記錄。
接下來,配置 GridView,使其僅包含 ProductName
、CategoryName
SupplierName
、和 BoundFields 和UnitPrice
已停止的 CheckBoxField。 最後,通過選中 GridView 智能標記 (中的“啓用排序”複選框或將其 AllowSorting
屬性設置爲 true
) ,將 GridView 配置爲支持排序。 向 CustomSortingUI.aspx
頁面添加這些內容後,聲明性標記應如下所示:
<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。
圖 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
(,我們可以將記錄標記爲分隔符行,因爲此類值通常無法) 存在。
若要利用此方法,我們需要執行以下步驟:
- 以編程方式檢索要綁定到 GridView 的數據, (實例
ProductsDataTable
) - 根據 GridView 和
SortExpression
SortDirection
屬性對數據進行排序 - 循環訪問
ProductsRows
中ProductsDataTable
,查找排序列的差異所在位置 - 在每個組邊界處,將分隔符記錄
ProductsRow
實例注入 DataTable,該CategoryID
實例設置爲-1
(或決定將記錄標記爲分隔符記錄的任何指定) - 注入分隔符行後,以編程方式將數據綁定到 GridView
除了這五個步驟,我們還需要爲 GridView 事件 RowDataBound
提供事件處理程序。 在這裏,我們將檢查每個 DataRow
行,並確定它是分隔符行,一個是其 CategoryID
設置 -1
。 如果是這樣,我們可能需要調整其格式或單元格中顯示的文本 () 。
使用此方法注入排序組邊界需要比上面概述的工作多一點,因爲還需要爲 GridView 事件 Sorting
提供事件處理程序,並跟蹤 SortExpression
和 SortDirection
值。
在數據綁定後操作 GridView 控件集合
在將數據綁定到 GridView 之前,我們可以在數據綁定到 GridView 之後 添加分隔符行, 數據綁定的過程構建 GridView 的控制層次結構,實際上只是由 Table
行集合組成的實例,每個實例由單元格集合組成。 具體而言,GridView 控件集合在其根目錄中包含一個對象,一個 Table
GridViewRow
派生自綁定到 TableRow
GridView 的每個記錄 DataSource
的類) 的 (,以及 TableCell
每個實例中每個 GridViewRow
數據字段的對象 DataSource
。
若要在每個排序組之間添加分隔符行,我們可以在創建此控件層次結構後直接操作此控件層次結構。 我們可以確信,在呈現頁面時,GridView 控件層次結構是上次創建的。 因此,此方法重寫 Page
類 s Render
方法,此時 GridView 的最終控件層次結構將更新爲包含所需的分隔符行。 圖 4 說明了此過程。
圖 4:備用技術操作 GridView 控件層次結構 (單擊以查看全尺寸圖像)
在本教程中,我們將使用此後一種方法來自定義排序用戶體驗。
備註
在本教程中演示的代碼基於 Teemu Keiski 博客文章中提供的示例, 使用 GridView 排序分組播放位。
步驟 3:將分隔符行添加到 GridView 控件層次結構
由於我們只想在其控件層次結構創建並創建該頁面訪問的最後一次之後,將分隔符行添加到 GridView 控件層次結構中,因此我們希望在頁面生命週期結束時執行此添加,但在實際 GridView 控件層次結構呈現到 HTML 之前。 我們可實現此目的的最新可能點是 Page
類事件 Render
,可以使用以下方法簽名在代碼隱藏類中重寫該事件:
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_Load
if (!Page.IsPostBack)
事件處理程序中添加此調用。 有關方法的詳細信息,請參閱 分頁和排序報表數據 教程信息 Sort
。
假設數據已排序,下一個任務是確定數據排序依據的列,然後掃描該列的值是否存在差異的行。 以下代碼確保已對數據進行排序,並查找已對數據進行排序的列:
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
實例注入到控件層次結構中。 此操作通過以下代碼完成:
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 類。 隨意使用任何樣式設置吸引你;我使用了以下內容:
.SortHeaderRowStyle
{
background-color: #c00;
text-align: left;
font-weight: bold;
color: White;
}
使用當前代碼,排序接口在按任何 BoundField 排序時添加排序組標頭 (請參閱圖 5,其中顯示了供應商排序時) 的屏幕截圖。 但是,如果按任何其他字段類型 ((如 CheckBoxField 或 TemplateField) )進行排序,則找不到排序組標頭, (請參閱圖 6) 。
圖 5:按 BoundFields 排序時,排序接口包括排序組標題 (單擊以查看全尺寸圖像)
圖 6:對 CheckBoxField 進行排序時缺少排序組標題 (單擊以查看全尺寸圖像)
按 CheckBoxField 排序時缺少排序組標頭的原因是代碼當前只 TableCell
使用 s Text
屬性來確定每行排序列的值。 對於 CheckBoxFields,TableCell
s Text
屬性爲空字符串;相反,該值可通過駐留在集合Controls
中的 TableCell
CheckBox Web 控件獲得。
若要處理 BoundFields 以外的字段類型,我們需要擴充分配變量以 currentValue
檢查集合中 TableCell
是否存在 CheckBox 的代碼 Controls
。 不要使用 currentValue = gvr.Cells[sortColumnIndex].Text
,請將此代碼替換爲以下內容:
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) 。
圖 7:對 CheckBoxField 進行排序時,“排序組標題” (單擊以查看全尺寸圖像)
備註
如果產品具有NULL
數據庫值CategoryID
SupplierID
(或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找到。