導言
分頁和排序是顯示資料時經常用到的功能。比如,在一個線上書店裡搜尋關於ASP.NET 的書的時候,可能結果會是成百上千,而每頁只列出十條。而且結果可以根據title(書名),price(價格),page count(頁數),author name(作者)等來排序。我們在分頁和排序報表資料 裡已經討論過, GridView, DetailsView, 和FormView 都有內建的分頁功能,僅僅只需要勾一個checkbox就可以開啟。GridView 還支援內建的排序。
不幸的是,DataList 和Repeater 都沒有提供內建的分頁和排序功能。本章我們將學習如何在DataList 和Repeater 裡添加分頁和排序的支援。我們需要建立分頁介面,顯示正確的頁的記錄,並在postback過程中記下瀏覽的頁。雖然這會比GridView, DetailsView, 和FormView裡花費更多的時間和寫更多的代碼,但是也提供了更多的可擴充性。
注意:本章集中精力討論分頁,下章我們將學習排序。
第一步: 添加分頁和排序的教程頁
首先添加本章和下一章需要的頁。建立一個名為PagingSortingDataListRepeater的檔案夾,然後添加下面的5個頁,記得全部選擇Site.master。
Default.aspx
Paging.aspx
Sorting.aspx
SortingWithDefaultPaging.aspx
SortingWithCustomPaging.aspx
圖 1: 建立頁
然後開啟Default.aspx頁,從UserControls檔案夾裡拖一個SectionLevelTutorialListing.ascx使用者控制項進來。這個使用者控制項我們已經用了很多次了。見母板頁和網站導覽 。
圖 2: 添加使用者控制項
為了將排序和分頁的教程列出來,我們需要將他們添加到site map(網站地圖)裡。開啟Web.sitemap檔案,將下面的標記語言添加到“Editing and Deleting with the DataList”()的節點後面:
<siteMapNode url="~/PagingSortingDataListRepeater/Default.aspx" title="Paging and Sorting with the DataList and Repeater" description="Paging and Sorting the Data in the DataList and Repeater Controls"> <siteMapNode url="~/PagingSortingDataListRepeater/Paging.aspx" title="Paging" description="Learn how to page through the data shown in the DataList and Repeater controls." /> <siteMapNode url="~/PagingSortingDataListRepeater/Sorting.aspx" title="Sorting" description="Sort the data displayed in a DataList or Repeater control." /> <siteMapNode url="~/PagingSortingDataListRepeater/SortingWithDefaultPaging.aspx" title="Sorting with Default Paging" description="Create a DataList or Repeater control that is paged using default paging and can be sorted." /> <siteMapNode url="~/PagingSortingDataListRepeater/SortingWithCustomPaging.aspx" title="Sorting with Custom Paging" description="Learn how to sort the data displayed in a DataList or Repeater control that uses custom paging." /></siteMapNode>
圖 3: 更新 Site Map
回顧一下分頁
在前面我們學習了如何使用GridView, DetailsView, FormView 來分頁。這三個控制項都提供了一種稱為預設分頁的功能,僅僅只需要從智能標籤裡勾上“Enable Paging”(開啟分頁)即可。在使用預設分頁時,每次請求資料 – 無論是第一頁還是其它頁–GridView, DetailsView, 和FormView 都會重新請求所有的資料。然後根據請求的頁索引和每頁顯示的記錄數來顯示特定頁的資料,而忽略其它資料(即雖然被請求但未顯示的資料)。我們在分頁和排序報表資料 裡已經詳細的討論過預設分頁了。
預設分頁由於每次都請求所有的資料,因此在大資料量的情況下並不合適。例如,想象一下每頁顯示10條資料,總共有有50,000條。每次使用者瀏覽一頁時,都要從資料庫請求50,000條資料,而其中只有10條會被顯示。
自訂分頁使用每次只返回請求的資料,從而解決了預設分頁的效能問題。當使用自訂分頁時,我們需要寫有效返回正確的記錄的SQL語句。我們在裡學習了用SQL Server2005的ROW_NUMBER() keyword 來建立這樣的語句。
在DataList或Repeater裡使用預設分頁,我們可以使用PagedDataSource class來封裝ProductsDataTable裡需要分頁的內容。PagedDataSource類有一個可以賦給任何枚舉類型對象的DataSource屬性,和PageSize (每頁顯示的記錄數)and CurrentPageIndex (當前頁的索引)。一旦設定了這些屬性,PagedDataSource就可以作為任何資料控制項的資料來源。PagedDataSource根據PageSize和CurrentPageIndex來返回合適的記錄。圖4描述了PagedDataSource類的功能。
圖 4: PagedDataSource使用可分頁的介面封裝枚舉對象
PagedDataSource對象可以在BLL裡直接建立和配置,並通過ObjectDataSource綁定到DataList或Repeater。或者也可以在ASP.NET 頁的後台代碼裡直接做這些。如果使用後一種方法,我們就不能使用ObjectDataSource而應該直接編程將分頁資料繫結到DataList或Repeater。
PagedDataSource對象也有支援自訂分頁的屬性。但是在這裡我們將不討論它,因為我們在ProductsBLL類裡已經有一個可以精確的返回需要顯示的記錄的方法。本章我們將學習如何通過在ProductsBLL類裡添加一個返回合適的PagedDataSource對象的方法來實現預設分頁。下章我們再討論自訂分頁。
第二步: 在BLL裡添加預設的分頁方法
ProductsBLL類裡現在有一個返回所有product的方法–GetProducts()–和一個返回特定子集的方法–GetProductsPaged(startRowIndex,maximumRows)。當使用預設分頁時,GridView, DetailsView, FormView 使用GetProducts()方法擷取所有的product,但是在內部使用PagedDataSource來顯示正確的記錄子集。在DataList和Repeater裡實現同樣的功能,我們可以在BLL裡建立一個類比這種行為的方法。
在ProductsBLL裡添加一個帶兩個整型參數的方法,名為GetProductsAsPagedDataSource:
pageIndex – 顯示的頁的索引,從0開始
pageSize – 每頁顯示的記錄數.
GetProductsAsPagedDataSource首先從GetProducts()裡擷取所有的記錄。然後建立一個PagedDataSource對象,將CurrentPageIndex和PageSize屬性設定為傳進來的參數,pageIndex和pageSize。方法的最後返回這個配置過的PagedDataSource。
[System.ComponentModel.DataObjectMethodAttribute (System.ComponentModel.DataObjectMethodType.Select, false)]public PagedDataSource GetProductsAsPagedDataSource(int pageIndex, int pageSize){ // Get ALL of the products Northwind.ProductsDataTable products = GetProducts(); // Limit the results through a PagedDataSource PagedDataSource pagedData = new PagedDataSource(); pagedData.DataSource = products.Rows; pagedData.AllowPaging = true; pagedData.CurrentPageIndex = pageIndex; pagedData.PageSize = pageSize; return pagedData;}
第三步: 在DataList裡使用預設分頁顯示Product
完成GetProductsAsPagedDataSource方法後,我們現在來建立一個提供預設分頁的DataList或Repeater。開啟PagingSortingDataListRepeater檔案夾下的Paging.aspx頁,拖一個DataList進來,將ID設為ProductsDefaultPaging。通過智能標籤建立一個名為ProductsDefaultPagingDataSource的ObjectDataSource並用GetProductsAsPagedDataSource方法配置它。
圖 5: 建立並配置ObjectDataSource
在UPDATE, INSERT, DELETE 標籤的下拉式清單裡都選擇“(None)”.
圖 6: 在UPDATE, INSERT, DELETE 標籤的下拉裡選擇“(None)”
因為GetProductsAsPagedDataSource方法需要兩個參數,因此嚮導會提示我們選擇參數源。page index和page size的值必須在postback過程中記下來。它們可以存在view state,querystring,session裡或用其它技術來記錄。本章我們使用querystring。
分別使用querystring欄位“pageIndex” 和“pageSize”來配置pageIndex和pageSize。見圖7。由於使用者第一次瀏覽頁的時候沒有querystring,因此還需要設定這兩個參數的預設值。將pageIndex的預設值設為0(表示顯示第一頁資料),將pageSize的預設值設為4。
圖 7: 配置參數
配置完ObjectDataSource後,Visual Studio自動為DataList建立一個ItemTemplate。修改它讓它只顯示product的name,category和supplier。將DataList的RepeatColumns屬性設為2,Width設為“100%”, ItemStyle的Width設為 “50%”. 這樣的設定會為兩列提供相同的間距。完成這些後DataList和ObjectDataSource的標記語言看起來應該如下:
<asp:DataList ID="ProductsDefaultPaging" runat="server" Width="100%" DataKeyField="ProductID" DataSourceID="ProductsDefaultPagingDataSource" RepeatColumns="2" EnableViewState="False"> <ItemTemplate> <h4><asp:Label ID="ProductNameLabel" runat="server" Text='<%# Eval("ProductName") %>'></asp:Label></h4> Category: <asp:Label ID="CategoryNameLabel" runat="server" Text='<%# Eval("CategoryName") %>'></asp:Label><br /> Supplier: <asp:Label ID="SupplierNameLabel" runat="server" Text='<%# Eval("SupplierName") %>'></asp:Label><br /> <br /> <br /> </ItemTemplate> <ItemStyle Width="50%" /></asp:DataList><asp:ObjectDataSource ID="ProductsDefaultPagingDataSource" runat="server" OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL" SelectMethod="GetProductsAsPagedDataSource"> <SelectParameters> <asp:QueryStringParameter DefaultValue="0" Name="pageIndex" QueryStringField="pageIndex" Type="Int32" /> <asp:QueryStringParameter DefaultValue="4" Name="pageSize" QueryStringField="pageSize" Type="Int32" /> </SelectParameters></asp:ObjectDataSource>
注意:由於這裡我們不實現任何更新或刪除的功能,你可以禁用DataList的view state來減少頁面的大小。
第一次瀏覽頁的時候,querystring裡沒有提供pageIndex 和pageSize的值,因此將使用預設的0和4。見圖8。DataList將顯示4條product記錄。
圖 8: 顯示4條Product
由於還沒有分頁的介面,因此使用者現在還不能直接導航到第二頁。我們將在第四步裡建立分頁介面。現在我們只能直接在querystring裡指定分頁的參數來進行分頁。例如,我們可以將地址從Paging.aspx改為Paging.aspx?pageIndex=2,然後斷行符號,來瀏覽第二頁。這樣就可以看到第二頁的資料了,見圖9。
圖 9: 顯示第二頁資料
第四步: 建立分頁介面
有很多不同的完成分頁介面的方法。GridView, DetailsView, FormView 提供了4種不同的介面:
Next, Previous(後一頁,前一頁) – 使用者可以瀏覽上一頁或下一頁.
Next, Previous, First(第一頁), Last (最後一頁)– 除了上面的功能,這個還包含第一頁和最後一頁。
Numeric (數字)–在分頁介面上列出頁數,使用者可以隨意的選擇一個頁 .
Numeric, First, Last – 在上一個功能的基礎上增加了第一頁和最後一頁.
對DataList 和Repeater而言,我們需要決定它的分頁介面並實現它。這其中包含了需要建立web控制項和當特定頁的button被點時顯示請求的頁。另外某些分頁介面的控制項可能需要禁用。例如,當使用Next, Previous, First, Last這個模式來顯示時,如果瀏覽第一頁資料,那麼第一頁和前一頁的button應該被禁用。
本章我們使用 Next, Previous, First, Last介面。添加4個button,並將ID分別設為FirstPage,PrevPage,NextPage和LastPage。將Text設為“<< First”, “< Prev”, “Next >”, “Last >>”.
<asp:Button runat="server" ID="FirstPage" Text="<< First" /><asp:Button runat="server" ID="PrevPage" Text="< Prev" /><asp:Button runat="server" ID="NextPage" Text="Next >" /><asp:Button runat="server" ID="LastPage" Text="Last >>" />
然後為每個button建立一個Click事件處理。呆會我們將添加代碼來顯示請求的頁。
記下分頁的總記錄數
不管選擇哪種分頁介面,我們都需要計算和記下分頁的總記錄數。總的行數(和page size)來決定總的頁數,它決定了那些分頁介面的控制項需要增加或啟用。在我們建立的Next, Previous, First, Last 介面裡,page count(頁數)在兩種情況下需要被用到:
判斷我們是否在瀏覽最後一頁,這種情況下Next 和Last buttons 需要禁用。
如果使用者點了Last button我們需要將它轉到最後一頁,它的索引等於page count減1。
page count通過總行數除以page size(頁數)來計算得到。例如我們要分頁79條記錄,每頁顯示4條,那麼page count為20(79/4)。如果我們使用數字分頁介面,就可以通過這個資訊來決定要顯示多少個數字頁的button。如果分頁介面只包含Next 和Last buttons,可以通過page count來什麼時候禁用Next 和Last buttons。
如果分頁介面包含Last button(最後一頁),我們需要在postback過程中記下分頁的總記錄數,這樣在點Last button的時候我們可以獲得最後一頁的索引。為了方便實現這個,我們在ASP.NET頁的後台代碼裡建立一個TotalRowCount屬性來將這個值儲存到view state裡。
private int TotalRowCount{ get { object o = ViewState["TotalRowCount"]; if (o == null) return -1; else return (int)o; } set { ViewState["TotalRowCount"] = value; }}
除了TotalRowCount外,還需要為page index,page size和page count建立頁面級的唯讀屬性來方便讀取。
private int PageIndex{ get { if (!string.IsNullOrEmpty(Request.QueryString["pageIndex"])) return Convert.ToInt32(Request.QueryString["pageIndex"]); else return 0; }}private int PageSize{ get { if (!string.IsNullOrEmpty(Request.QueryString["pageSize"])) return Convert.ToInt32(Request.QueryString["pageSize"]); else return 4; }}private int PageCount{ get { if (TotalRowCount <= 0 || PageSize <= 0) return 1; else return ((TotalRowCount + PageSize) - 1) / PageSize; }}
擷取分頁的總記錄數
從ObjectDataSource的Select()方法返回一個PagedDataSource對象包含所有的product記錄,即使只有一部分會在DataList裡顯示。PagedDataSource的Count property 返回將在DataList裡顯示的項的數目。DataSourceCount property 返回PagedDataSource裡的所有項的的總數目。因此我們需要將ASP.NET頁的TotalRowCount屬性賦值為PagedDataSource的DataSourceCount。
我們為ObjectDataSource的Selectd事件建立一個event handler來完成這些。在Selectd的event handler裡我們擷取ObjectDataSource的Select()方法的傳回值–在這種情況下是PagedDataSource。
protected void ProductsDefaultPagingDataSource_Selected (object sender, ObjectDataSourceStatusEventArgs e){ // Reference the PagedDataSource bound to the DataList PagedDataSource pagedData = (PagedDataSource)e.ReturnValue; // Remember the total number of records being paged through // across postbacks TotalRowCount = pagedData.DataSourceCount;}
顯示請求的頁的資料
當使用者點分頁介面上的button時,我們需要顯示請求的頁的資料。由於分頁的參數在querystring裡指定,因此使用Response.Redirect(url)來讓使用者重新請求帶合適分頁參數的Paging.aspx頁。例如,顯示第二頁的資料,我們將使用者重新導向到Paging.aspx?pageIndex=1。
建立一個RedirectUser(sendUserToPageIndex)方法來重新導向使用者到Paging.aspx?pageIndex=sendUserToPageIndex。然後在四個按鈕的Click事件處理裡調用這個方法。在FirstPageClick裡調用RedirectUser(0),在PrevPageClick裡調用RedirectUser(PageIndex-1)。
protected void FirstPage_Click(object sender, EventArgs e){ // Send the user to the first page RedirectUser(0);}protected void PrevPage_Click(object sender, EventArgs e){ // Send the user to the previous page RedirectUser(PageIndex - 1);}protected void NextPage_Click(object sender, EventArgs e){ // Send the user to the next page RedirectUser(PageIndex + 1);}protected void LastPage_Click(object sender, EventArgs e){ // Send the user to the last page RedirectUser(PageCount - 1);}private void RedirectUser(int sendUserToPageIndex){ // Send the user to the requested page Response.Redirect(string.Format("Paging.aspx?pageIndex={0}&pageSize={1}", sendUserToPageIndex, PageSize));}
完成Click事件處理後,DataList的記錄現在可以通過button來分頁了,你可以測試一下。
禁用分頁控制項
現在無論瀏覽哪頁四個按鈕都是可用的。然而我們在瀏覽第一頁時需要禁用 First 和Previous buttons ,在瀏覽最後一頁時需要禁用Next 和Last buttons。通過ObjectDataSource的Select()方法返回的PagedDataSource對象有幾個屬性– IsFirstPage 和 IsLastPage –通過它們可以判斷使用者瀏覽的是否是第一或最後一頁資料。添加下面的代碼到ObjectDataSource的Selected事件處理裡:
// Configure the paging interface based on the data in the PagedDataSourceFirstPage.Enabled = !pagedData.IsFirstPage;PrevPage.Enabled = !pagedData.IsFirstPage;NextPage.Enabled = !pagedData.IsLastPage;LastPage.Enabled = !pagedData.IsLastPage;
添加完後,當瀏覽第一頁時,First 和Previous buttons 將被禁用。當瀏覽最後一頁時,Next 和 Last buttons 將被禁用。
我們最後來實現在分頁介面裡通知使用者他們當前是瀏覽的哪頁和一共有多少頁。添加一個Label控制項並將ID設為CurrentPageNumber。在ObjectDataSource的Selected事件處理中設定它的Text屬性,讓它顯示當前瀏覽的頁(PageIndex+1)和總頁數(PageCount)。
// Display the current page being viewed...CurrentPageNumber.Text = string.Format("You are viewing page {0} of {1}...", PageIndex + 1, PageCount);
圖10是第一次瀏覽Paging.aspx頁的樣子。由於querystring是空的,因此DataList預設顯示最開始的4條product。First 和Previous buttons 被禁用。點Next 會顯示下面的4條記錄(見圖11),而First 和Previous buttons 同時被啟用了。
圖 10: 第一頁資料
圖 11: 第二頁資料
注意:分頁介面可以進一步改善,比如增加允許使用者來指定每頁顯示多少記錄。例如添加一個DropDownList列出page size的選項,比如5, 10, 25, 50, 和ALL。使用者選擇了page size後會重新導向到Paging.aspx?pageIndex=0&pageSize=selectedPageSize。我將這個作為練習留給讀者自己完成。
使用自訂分頁
DataList使用沒有效率的預設分頁技術。當大資料量時,我們需要使用自訂分頁。雖然實現的細節有所不同,但是分頁裡的概念和預設分頁是一樣的。預設分頁時,使用ProductsBLL類的GetProductsPaged方法(而不是GetProductsAsPagedDataSource)。正如在大資料量時提高分頁的效率 裡討論的那樣,GetProductsPaged需要傳入開始行的索引和行的最大數目。這些參數可以通過預設分頁裡使用的querystring裡的pageIndex和pageSize參數來儲存。
由於自訂分頁裡沒有PagedDataSource,所以需要其它技術來決定總記錄數和判斷我們是否顯示第一或最後一頁資料。ProductsBLL類的TotalNumberOfProducts()方法返回roduct的總記錄數。而為了判斷是否瀏覽的是第一頁資料,我們需要檢查開始行的索引–如果是0,則表示在瀏覽第一頁。如果開始行的索引加上最大的行數大於或等於總記錄數則表示在最後一頁.我們將在下章詳細的討論如何?自訂分頁。
總結
DataList和Repeater都沒有提供象GridView, DetailsView, FormView 那樣的分頁的支援,這樣的功能需要我們來實現。最簡單的實現方法是使用預設分頁,將所有的product都封裝到PagedDataSource裡,然後綁定PagedDataSource到DataList或Repeater。本章我們在ProductsBLL類裡添加GetProductsAsPagedDataSource方法,它返回PagedDataSource。ProductsBLL類已經包含了自訂分頁需要的方法– GetProductsPaged和TotalNumberOfProducts。
不管是自訂方法裡擷取精確的記錄還是預設方法裡擷取所有記錄,我們都需要手動添加分頁介面。本章我們建立的是包含4個button控制項的Next, Previous, First, Last interface 。當然還添加了一個顯示當前頁和總頁數的Label控制項。