摘要:解決向任何 asp.net 控件添加分頁功能的問題。還為開發(fā)復(fù)合 ASP.NET 控件提供了很多有用的提示和技巧。 下載本文的源代碼(英文)。(請注重,在示例文件中,程序員的注釋使用的是英文,本文中將其譯為中文是為了便于讀者理解。)從程序員的角度來看,Microsoft® SQL Server™ 查詢的最大缺陷之一就是返回的行數(shù)通常比應(yīng)用程序的用戶界面實際可以容納的行數(shù)要多得多。這種尷尬情形經(jīng)常將開發(fā)人員陷于困境。開發(fā)人員是應(yīng)該創(chuàng)建一個非常長的頁面,讓用戶花時間去滾動瀏覽,還是應(yīng)該通過設(shè)置一個手動分頁機(jī)制來更好地解決這個問題? 哪種解決方案更好,在很大程度上取決于要檢索的數(shù)據(jù)的特性。由多個項目(如搜索結(jié)果)組成的較長列表,最好通過各頁大小相等、每頁相對較短的多個頁面顯示。由單個項目(如文章的文本)組成的較長列表,假如整個插入在一個頁面中,使用起來會更方便。最后得出的分析結(jié)果是,應(yīng)該根據(jù)應(yīng)用程序的總體用途來做決定。那么,Microsoft® ASP.NET 是如何解決數(shù)據(jù)分頁問題的呢?ASP.NET 提供了功能強(qiáng)大的數(shù)據(jù)綁定控件,以便將查詢結(jié)果格式化為 Html 標(biāo)記。但是,這些數(shù)據(jù)綁定控件中只有一種控件(即 DataGrid 控件)本來就支持分頁。其他控件(如 DataList、Repeater 或 CheckBoxList)則不支持分頁。這些控件及其他列表控件不支持分頁,不是因為它們在結(jié)構(gòu)上不支持分頁,而是因為它們與 DataGrid 不同,不包含任何處理分頁的特定代碼。但是,處理分頁的代碼是相當(dāng)樣板化的,可以添加到所有這些控件中。 Scott Mitchell 在最近的一篇題目為“Creating a Pageable, Sortable DataGrid”(英文)的文章中,介紹了 DataGrid 分頁。該文還引用了 Web 上的其他有用信息,為您提供了有關(guān)網(wǎng)格分頁基礎(chǔ)知識和其他信息。假如想查看如何使 DataList 控件可以進(jìn)行分頁的示例,可以查看此文章(英文)。該文演示了如何創(chuàng)建一個自定義的 DataList 控件,該控件具有當(dāng)前索引和頁面大小屬性,并可以啟動頁面更改事件。 同樣的代碼也可以用于滿足其他列表控件(如 ListBox 和 CheckBoxList)的分頁需要。不過,向各個控件添加分頁功能實際上并不是一種非常好的做法,因為,如上所述,分頁代碼是相當(dāng)樣板化的。因此,對于聰明的程序員來說,有什么方法比使用一種新的通用分頁程序控件來實現(xiàn)所有這些控件的分頁功能更好的呢?本文中將建立一個分頁程序控件,它將使合作者列表控件能夠?qū)?SQL Server 的查詢結(jié)果進(jìn)行分頁。該控件名為 SqlPager,它支持兩種類型的合作者控件 - 列表控件和基礎(chǔ)數(shù)據(jù)列表控件。SqlPager 控件的顯著特點SqlPager 控件是一個 ASP.NET 復(fù)合控件,包含一個單行表格。該行又包含兩個單元格 - 導(dǎo)航條和頁面描述符。該控件的用戶界面呈條形,理想情況下,其寬度與合作者控件的寬度相同。導(dǎo)航條部分提供了可單擊的元素,以便在頁面之間移動;頁面描述符部分為用戶提供了有關(guān)當(dāng)前顯示的頁面的一些反饋信息。
圖 1:Visual Studio .NET 網(wǎng)頁設(shè)計器中顯示的 SqlPager 控件與 DataGrid 控件的嵌入式分頁程序一樣,SqlPager 控件具有兩種導(dǎo)航模式,即下一頁/上一頁和數(shù)字頁面。此外,其非凡屬性 PagerStyle 使您能夠選擇更方便的樣式。該控件與列表控件協(xié)同工作。您可以通過 ControlToPaginate 字符串屬性為分頁程序指定一個這樣的合作者控件。 SqlPager1.ControlToPaginate = "ListBox1"; 一般情況下,分頁程序首先獲取 SQL Server 的查詢結(jié)果,預(yù)備一個適當(dāng)?shù)挠涗涰撁妫缓笸ㄟ^合作者控件的 DataSource 屬性顯示該頁面。當(dāng)用戶單擊以查看新頁面時,分頁程序?qū)z索請求的數(shù)據(jù)并再次通過合作者控件來顯示數(shù)據(jù)。分頁機(jī)制對于列表控件是完全透明的。列表控件的數(shù)據(jù)源是通過編程方式進(jìn)行更新的,任何時候都只包含適合當(dāng)前頁面的記錄。 控件的分頁引擎具有多個 public 屬性,如 CurrentPageIndex、ItemsPerPage 和 PageCount,通過這些屬性來獲取并設(shè)置當(dāng)前頁面的索引、每個頁面的大小以及要顯示的頁面的總數(shù)。分頁程序治理數(shù)據(jù)檢索和分頁所需的任何邏輯。 SelectCommand 屬性設(shè)置獲取數(shù)據(jù)所用的命令文本。ConnectionString 屬性定義數(shù)據(jù)庫的名稱和位置以及連接憑據(jù)。執(zhí)行查詢時采用的方式取決于 PagingMode 屬性的值。該屬性的可能值為與其同名的 PagingMode 枚舉的值 - Cached 和 NonCached。假如選擇 Cached 選項,則將使用數(shù)據(jù)適配器和 DataTable 對象檢索整個結(jié)果集。可以選擇將結(jié)果集放置在 ASP.NET 的 Cache 對象中,該結(jié)果集可以重復(fù)使用直到過期。假如選擇 NonCached 選項,則查詢只檢索適合當(dāng)前頁面的記錄。這時,ASP.NET 的 Cache 中不放置任何數(shù)據(jù)。NonCached 模式與 DataGrid 控件的自定義分頁模式幾乎相同。
下表列出 SqlPager 控件的全部編程接口。表 1:SqlPager 控件的編程接口名稱 | 類型 | 說明 | CacheDuration屬性指示數(shù)據(jù)在 ASP.NET 的緩存中保留的秒數(shù)。只用于 Cached 模式。默認(rèn)值為 60 秒。ConnectionString屬性用來訪問所選擇的 SQL Server 數(shù)據(jù)庫的連接字符串。ControlToPaginate屬性同一個 .aspx 頁面中的控件 ID,它將顯示分頁程序檢索的記錄頁面。這是合作者控件。CurrentPageIndex屬性獲取并設(shè)置基于 0 的頁面索引。 ItemsPerPage屬性獲取并設(shè)置每頁要顯示的記錄數(shù)量。默認(rèn)值為每頁顯示 10 個項目。PagerStyle屬性該值指示分頁程序用戶界面的樣式。它可以為 PagerStyle 枚舉值:Next
PRev 和 NumericPages 之一。在 NextPrev 模式中,將顯示 VCR 式的按鈕,來轉(zhuǎn)到第一頁、上一頁、下一頁和最后一頁。而在 NumericPages 模式中,將顯示一個下拉列表,列出所有可用頁面的索引。PagingMode屬性該值指示檢索數(shù)據(jù)的方式。它可以為 PagingMode 枚舉值:Cached 和 NonCached 之一。假如為 Cached,則將使用數(shù)據(jù)適配器,且整個結(jié)果集將臨時放置在 ASP.NET 緩存中。假如為 NonCached,則只檢索當(dāng)前頁面中的記錄。在這種情況下,不進(jìn)行緩存。SelectCommand屬性用來進(jìn)行查詢的命令文本。最好為 SELECT-FROM-WHERE 形式。不支持 ORDER BY 子句。排序是通過 SortField 屬性另外指定的。SortField屬性用來排序的字段的名稱。此字段用于為查詢提供動態(tài)的 ORDER BY 子句。排序是由 SQL Server 執(zhí)行的。 ClearCache方法刪除存儲在 ASP.NET 緩存中的任何數(shù)據(jù)。PageIndexChanged事件默認(rèn)事件,當(dāng)分頁程序移動到另一個頁面時發(fā)生。事件的數(shù)據(jù)結(jié)構(gòu)為 PageChangedEventArgs 類,包含舊頁面和新頁面的索引。由于 SqlPager 控件繼續(xù)了 WebControl,因此它也具有很多與 UI 相關(guān)的屬性來治理
字體、邊框和顏色。 生成 SqlPager 控件將作為復(fù)合控件來生成 SqlPager 控件并讓其繼續(xù) WebControl 類。復(fù)合控件是 ASP.NET 服務(wù)器控件所特有的,它是由一個或多個服務(wù)器控件組成。 public class SqlPager : WebControl, INamingContainer{ ... } 除非生成完全自定義的控件或擴(kuò)展現(xiàn)有的控件,否則,創(chuàng)建新控件時,大多數(shù)時間實際上是在生成復(fù)合控件。要創(chuàng)建 SqlPager,組合一個 Table 控件,并根據(jù)分頁程序的樣式,組合幾個 LinkButton 控件或者一個 DropDownList 控件。 生成復(fù)合控件時,需要注重幾條原則。首先,需要覆蓋 CreateChildControls protected 方法。CreateChildControls 方法是從 Control 繼續(xù)來的,當(dāng)服務(wù)器控件為了顯示而要創(chuàng)建子控件時或在返回后,將調(diào)用此方法。 protected override void CreateChildControls(){ // 清除現(xiàn)有的子控件及其 ViewState Controls.Clear(); ClearChildViewState(); // 生成控件樹 BuildControlHierarchy();} 覆蓋此方法時,需要執(zhí)行幾項重要的操作。創(chuàng)建并初始化任何所需的子控件實例并將它們添加到父控件的 Controls 集合中。但是,生成新控件樹之前,應(yīng)該刪除任何現(xiàn)有的子控件并清除子控件可能留下的任何 ViewState 信息。 復(fù)合
組件還需要實現(xiàn) INamingContainer 接口,以便 ASP.NET 運(yùn)行時可以為其創(chuàng)建一個新的命名范圍。這就確保了復(fù)合控件中的所有控件都具有唯一的名稱。這還將確保能夠自動處理子控件的返回數(shù)據(jù)。 對于 SqlPager 控件來說,成為命名容器非常重要。事實上,SqlPager 包含一些 LinkButton 控件,并且需要獲取并處理其單擊事件以便導(dǎo)航頁面。正如 ASP.NET 頁面中的任何其他控件一樣,LinkButton 也被賦予了一個 ID,用于標(biāo)識處理返回事件的控件。 處理返回事件時,ASP.NET 運(yùn)行時試圖查找事件的目標(biāo) ID 與主窗體的任何直接子控件之間是否存在匹配關(guān)系。LinkButton 是分頁程序的子控件,因此不能運(yùn)行其服務(wù)器端的代碼。這是否意味著只有窗體的直接子控件才能啟動并處理服務(wù)器事件?當(dāng)然不是,只要您使用命名容器。 通過使 SqlPager 控件實現(xiàn) INamingContainer 接口,可以將嵌入式鏈接按鈕的實際 ID 從“First”更改為“SqlPager1:First”。當(dāng)用戶單擊以查看新頁面時,返回事件將 SqlPager1:First 作為目標(biāo)控件。實際上,運(yùn)行時用來識別目標(biāo)控件的算法比上面介紹的要復(fù)雜一些。運(yùn)行時將事件目標(biāo)控件的名稱看作是用冒號分隔開的字符串。實際上,這種匹配是在窗體的子控件和用冒號分隔開的字符串(如 SqlPager1:First)的第一個標(biāo)記之間進(jìn)行的。由于分頁程序是窗體的子控件,因此匹配時會成功,分頁程序獲取單擊事件。假如您認(rèn)為這種解釋不夠充分或者令人費(fèi)解,只要下載 SqlPager 控件的源代碼,刪除 INamingContainer 標(biāo)記接口并進(jìn)行重新編譯即可。您將看到分頁程序能夠返回,但不能內(nèi)部處理單擊事件。
INamingContainer 接口是一個不具備方法的標(biāo)記接口,其實現(xiàn)只需在類聲明中指定名稱即可,無需進(jìn)行任何其他操作。復(fù)合控件的另一個重要方面是,它們通常不需要自定義邏輯來進(jìn)行顯示。復(fù)合控件的顯示遵循其組成控件的顯示。生成復(fù)合控件時,通常無需覆蓋 Render 方法。 控件的 SqlPager 樹由一個包含兩個單元格的單行表格組成。該表格繼續(xù)了分頁程序的大部分可視設(shè)置,如前景顏色和背景顏色、邊框、字體信息和寬度等。第一個單元格包含導(dǎo)航條,其結(jié)構(gòu)取決于 PagerStyle 屬性的值。假如分頁程序的樣式為 NextPrev,則導(dǎo)航條將由四個 VCR 式的按鈕組成。否則,它將由一個下拉列表組成。private void BuildControlHierarchy(){ // 生成環(huán)境表格(一行,兩個單元格) Table t = new Table(); t.Font.Name = this.Font.Name; t.Font.Size = this.Font.Size; t.BorderStyle = this.BorderStyle; t.BorderWidth = this.BorderWidth; t.BorderColor = this.BorderColor; t.Width = this.Width; t.Height = this.Height; t.BackColor = this.BackColor; t.ForeColor = this.ForeColor; // 生成表格中的行 TableRow row = new TableRow(); t.Rows.Add(row); // 生成帶有導(dǎo)航條的單元格 TableCell cellNavBar = new TableCell(); if (PagerStyle == this.PagerStyle.NextPrev) BuildNextPrevUI(cellNavBar); else BuildNumericPagesUI(cellNavBar); row.Cells.Add(cellNavBar); // 生成帶有頁面索引的單元格 TableCell cellPageDesc = new TableCell(); cellPageDesc.HorizontalAlign = HorizontalAlign.Right; BuildCurrentPage(cellPageDesc); row.Cells.Add(cellPageDesc); // 將表格添加到控件樹 this.Controls.Add(t);} 將各個控件添加到正確的 Controls 集合對于分頁程序的正確顯示極其重要。最外層的表格必須添加到分頁程序的 Controls 集合中。鏈接按鈕和下拉列表必須添加到相應(yīng)表格單元格的 Controls 集合中。下面給出了用來生成鏈接按鈕導(dǎo)航條的代碼。每個按鈕都顯示有一個 Webdings 字符,可以根據(jù)需要禁用,并被綁定到內(nèi)部的 Click 事件處理程序。 private void BuildNextPrevUI(TableCell cell){ bool isValidPage = ((CurrentPageIndex >=0) && (CurrentPageIndex <= TotalPages-1)); bool canMoveBack = (CurrentPageIndex>0); bool canMoveForward = (CurrentPageIndex<TotalPages-1); // 顯示 << 按鈕 LinkButton first = new LinkButton(); first.ID = "First"; first.Click += new EventHandler(first_Click); first.Font.Name = "webdings"; first.Font.Size = FontUnit.Medium; first.ForeColor = ForeColor; first.ToolT
ip = "第一頁"; first.Text = "7"; first.Enabled = isValidPage && canMoveBack; cell.Controls.Add(first); :} 分頁程序的另一種樣式(在下拉列表中列出數(shù)字頁面)的生成方法如下所示:private void BuildNumericPagesUI(TableCell cell){ // 顯示一個下拉列表 DropDownList pageList = new DropDownList(); pageList.ID = "PageList"; pageList.AutoPostBack = true; pageList.SelectedIndexChanged += new EventHandler(PageList_Click); pageList.Font.Name = this.Font.Name; pageList.Font.Size = Font.Size; pageList.ForeColor = ForeColor; if (TotalPages <=0 CurrentPageIndex == -1) { pageList.Items.Add("No pages"); pageList.Enabled = false; pageList.SelectedIndex = 0; } else // 填充列表 { for(int i=1; i<=TotalPages; i++) { ListItem item = new ListItem(i.ToString(), (i-1).ToString()); pageList.Items.Add(item); } pageList.SelectedIndex = CurrentPageIndex; }}
所有事件處理程序(Click 和 SelectedIndexChanged)最終都會更改當(dāng)前顯示的頁面。這兩種方法都會調(diào)用一個公用的 private 方法 GoToPage。private void first_Click(object sender, EventArgs e){ GoToPage(0);}private void PageList_Click(object sender, EventArgs e){ DropDownList pageList = (DropDownList) sender; int pageIndex = Convert.ToInt32(pageList.SelectedItem.Value); GoToPage(pageIndex);}private void GoToPage(int pageIndex){ // 預(yù)備事件數(shù)據(jù) PageChangedEventArgs e = new PageChangedEventArgs(); e.OldPageIndex = CurrentPageIndex; e.NewPageIndex = pageIndex; // 更新當(dāng)前的索引 CurrentPageIndex = pageIndex; // 啟動頁面更改事件 OnPageIndexChanged(e); // 綁定新數(shù)據(jù) DataBind();} 其他導(dǎo)航按鈕的處理程序與 first_Click 的區(qū)別僅在于它們傳遞給 GoToPage 方法的頁碼不同。GoToPage 方法負(fù)責(zé)處理 PageIndexChanged 事件,并負(fù)責(zé)啟動數(shù)據(jù)綁定過程。它預(yù)備事件數(shù)據(jù)(舊頁面和新頁面索引)并觸發(fā)事件。GoToPage 被定義為 private,但是可以使用 CurrentPageIndex 屬性通過編程的方式更改顯示的頁面。 public int CurrentPageIndex{ get {return Convert.ToInt32(ViewState["CurrentPageIndex"]);} set {ViewState["CurrentPageIndex"] = value;}} 與表 1 中所列的所有屬性一樣,CurrentPageIndex 屬性的實現(xiàn)方法也相當(dāng)簡單。它將其內(nèi)容保存到 ViewState 中并從中進(jìn)行還原。在數(shù)據(jù)綁定過程中,會驗證和使用頁面索引。數(shù)據(jù)綁定過程DataBind 方法是所有 ASP.NET 控件公用的,對于數(shù)據(jù)綁定控件來說,它將觸發(fā)用戶界面的刷新以反映新數(shù)據(jù)。SqlPager 控件根據(jù) SelectCommand 和 ConnectionString 屬性的值,使用此方法啟動數(shù)據(jù)檢索操作。不言而喻,假如這些屬性中的任何一個為空,該過程將終止。同樣,假如合作者控件不存在,數(shù)據(jù)綁定過程將被取消。要查找合作者控件,DataBind 方法使用 Page 類中的 FindControl 函數(shù)。由此可見,合作者控件必須為主窗體的直接子控件。進(jìn)行分頁顯示的控件不能為任意的 ASP.NET 服務(wù)器控件。它必須為列表控件或基本數(shù)據(jù)列表控件。更一般來說,合作者控件必須具有 DataSource 屬性并實現(xiàn) DataBind 方法。可能進(jìn)行分頁的控件實際上只需要滿足這些要求。Microsoft® .NET Framework 中所有繼續(xù) ListControl 或 BaseDataList 的控件都滿足第一個要求;而所有 Web 控件通過設(shè)計都滿足 DataBind 要求。使用當(dāng)前的實現(xiàn)方法,無法使用 SqlPager 控件來對 Repeater 進(jìn)行分頁。Repeater 與合作者控件 DataList 和 DataGrid 不同,不繼續(xù) BaseDataList,也不提供列表控件的功能。下表列出了可以使用 SqlPager 進(jìn)行分頁的控件。表 2:可以由 SqlPager 控件進(jìn)行分頁的數(shù)據(jù)綁定控件
控件 | 說明 | CheckBoxList從 ListControl 派生而來,顯示為復(fù)選框列表。 DropDownList從 ListControl 派生而來,顯示為字符串下拉列表。ListBox從 ListControl 派生而來,顯示為字符串可滾動列表。RadioButtonList從 ListControl 派生而來,顯示為單選按鈕列表。DataList從 BaseDataList 派生而來,顯示為
模板化數(shù)據(jù)項目列表。DataGrid從 BaseDataList 派生而來,顯示為數(shù)據(jù)項目的表格網(wǎng)格。DataGrid 是唯一一個內(nèi)置有功能強(qiáng)大的分頁引擎的 ASP.NET 控件。 以下代碼說明由 SqlPager 控件實現(xiàn)的數(shù)據(jù)綁定過程。 public override void DataBind(){ // 啟動數(shù)據(jù)綁定事件 base.DataBinding(); // 數(shù)據(jù)綁定后必須重新創(chuàng)建控件 ChildControlsCreated = false; // 確保控件存在且為列表控件 _controlToPaginate = Page.FindControl(ControlToPaginate); if (_controlToPaginate == null) return; if (!(_controlToPaginate is BaseDataList _controlToPaginate is ListControl)) return; // 確保具有足夠的連接信息并指定查詢 if (ConnectionString == "" SelectCommand == "") return; // 獲取數(shù)據(jù) if (PagingMode == PagingMode.Cached) FetchAllData(); else FetchPageData(); // 將數(shù)據(jù)綁定到合作者控件 BaseDataList baseDataListControl = null; ListControl listControl = null; if (_controlToPaginate is BaseDataList) { baseDataListControl = (BaseDataList) _controlToPaginate; baseDataListControl.DataSource = _dataSource; baseDataListControl.DataBind(); return; } if (_controlToPaginate is ListControl) { listControl = (ListControl) _controlToPaginate; listControl.Items.Clear(); listControl.DataSource = _dataSource; listControl.DataBind(); return; }}
根據(jù) PagingMode 屬性的值調(diào)用不同的獲取例程。在任何情況下,結(jié)果集都綁定到 PagedDataSource 類的實例上。此類提供了一些用來對數(shù)據(jù)進(jìn)行分頁的功能。非凡是,當(dāng)整個數(shù)據(jù)集都存儲在緩存中時,該類將自動檢索當(dāng)前頁面的記錄并返回布爾值,來提供有關(guān)第一頁和最后一頁的信息。稍后將回來介紹此類的內(nèi)部結(jié)構(gòu)。在上述列表中,幫助程序的 PagedDataSource 對象是由 _dataSource 變量表示的。然后,SqlPager 控件經(jīng)過計算得出合作者控件的類型,并將 PagedDataSource 對象的內(nèi)容綁定到合作者控件的 DataSource 屬性。有時,上述的 DataBind 方法還將 ChildControlsCreated 屬性重新設(shè)置為 false。那么,為什么要這樣做呢? 當(dāng)包含分頁程序的頁面返回時,所有控件都要重新創(chuàng)建;分頁程序也不例外。通常情況下,所有控件及其子控件都是在預(yù)備顯示頁面之前創(chuàng)建的。在每個控件接收到 OnPreRender 通知之前的一瞬間,protected EnsureChildControls 方法將被調(diào)用,這樣,每個控件都可以生成自己的控件樹。此事件發(fā)生后,數(shù)據(jù)綁定過程完成,新數(shù)據(jù)已存儲到緩存中。 但是,當(dāng)由于單擊分頁程序的一個組成控件而使頁面返回時(即用戶單擊以更改頁面),就會生成分頁程序的控件樹,這時遠(yuǎn)未達(dá)到顯示階段。非凡是,當(dāng)處理相關(guān)的服務(wù)器端事件時,就必須生成控件樹,因而是在數(shù)據(jù)綁定開始之前生成控件樹。困難在于,數(shù)據(jù)綁定將修改頁面索引,而這必須反映在用戶界面中。假如不采取某些對策的話,當(dāng)用戶切換到另一頁時,分頁程序中的頁面索引將不會被刷新。有各種方法可以解決此問題,但重要的是要弄清楚問題及其真正的原因。您可以避免生成控件樹,并在 Render 方法中生成所有輸出。另外,您還可以修改樹中受數(shù)據(jù)綁定更改影響的部分。本文選擇了第三種方法,這種方法需要較少的代碼,而且,不管控件的用戶界面的哪個部分受到數(shù)據(jù)綁定更改的影響,都能夠解決問題。通過將 ChildControlsCreated 屬性設(shè)置為 false,可以使以前創(chuàng)建的任何控件樹無效。這樣,在顯示之前將重新創(chuàng)建控件樹。分頁引擎SqlPager 控件支持兩種檢索數(shù)據(jù)的方法 - 緩存和非緩存。假如采用前者,選擇命令原樣執(zhí)行,整個結(jié)果集將綁定到在內(nèi)部進(jìn)行分頁的數(shù)據(jù)源對象上。PagedDataSource 對象將自動返回適合特定頁面的記錄。PagedDataSource 類也是在 DataGrid 默認(rèn)分頁機(jī)制背后運(yùn)行的系統(tǒng)組件。檢索所有記錄但只顯示適合頁面的幾個記錄,這通常不是一種明智的方法。由于 Web 應(yīng)用程序的無狀態(tài)特性,事實上,每次請求頁面時都可能運(yùn)行大量的查詢。要使操作有效,采用緩存的數(shù)據(jù)檢索方法必須依靠于某種緩存對象,ASP.NET 的 Cache 對象就是一個很好的候選對象。緩存技術(shù)的使用加快了應(yīng)用程序的運(yùn)行速度,但其提供的數(shù)據(jù)快照不能反映最新的更改。另外,它需要使用 Web 服務(wù)器上的較多內(nèi)存。而且荒謬的是,假如大量的數(shù)據(jù)按會話緩存的話,甚至可能造成很大的問題。Cache 容器對于應(yīng)用程序來說是全局的;假如數(shù)據(jù)按會話存儲在其中,還需生成會話特有的項目名稱。Cache 對象的上面是完全支持過期策略的。換句話說,存儲在緩存中的數(shù)據(jù)在一段時間后可以自動被釋放。以下代碼說明 SqlPager 類中用來獲取數(shù)據(jù)并將其存儲在緩存中的一個 private 方法。private void FetchAllData(){ // 在 ASP.NET Cache 中查找數(shù)據(jù) DataTable data; data = (DataTable) Page.Cache[CacheKeyName]; if (data == null) { // 使用 order-by 信息修改 SelectCommand AdjustSelectCommand(true); // 假如數(shù)據(jù)過期或從未被獲取,則轉(zhuǎn)到數(shù)據(jù)庫 SqlDataAdapter adapter = new SqlDataAdapter(SelectCommand, ConnectionString); data = new DataTable(); adapter.Fill(data); Page.Cache.Insert(CacheKeyName, data, null, DateTime.Now.AddSeconds(CacheDuration), System.Web.Caching.Cache.NoSlidingEXPiration); } // 配置分頁的數(shù)據(jù)源組件 if (_dataSource == null) _dataSource = new PagedDataSource(); _dataSource.DataSource = data.DefaultView; _dataSource.AllowPaging = true; _dataSource.PageSize = ItemsPerPage; TotalPages = _dataSource.PageCount; // 確保頁面索引有效 ValidatePageIndex(); if (CurrentPageIndex == -1) { _dataSource = null; return; } // 選擇要查看的頁面 _dataSource.CurrentPageIndex = CurrentPageIndex;} 控件和請求的緩存項目名稱是唯一的。它包括頁面的 URL 和控件的 ID。在指定的時間(以秒計算)內(nèi),數(shù)據(jù)被綁定到緩存。要使項目過期,必須使用 Cache.Insert 方法。以下較簡單的代碼將項目添加到緩存,但不包括任何過期策略。Page.Cache[CacheKeyName] = data;
PagedDataSource 對象通過其 DataSource 屬性獲取要進(jìn)行分頁的數(shù)據(jù)。值得注重的是,PagedDataSource 類的 DataSource 屬性只接受 IEnumerable 對象。DataTable 不滿足此要求,這就是為什么采取 DefaultView 屬性的原因。SelectCommand 屬性確定在 SQL Server 數(shù)據(jù)庫上運(yùn)行的查詢。此字符串最好為 SELECT-FROM-WHERE 形式。不支持 ORDER BY 子句,假如指定了該子句,將被刪除。這正是 AdjustSelectCommand 方法所做的。使用 SortField 屬性可以指定任何排序信息。AdjustSelectCommand 方法本身將根據(jù) SortField 的值添加一個正確的 ORDER BY 子句。這樣做有什么原因嗎?當(dāng)分頁程序以 NonCached 模式工作時,原始的查詢將被修改以確保只檢索當(dāng)前頁面的記錄。在 SQL Server 上執(zhí)行的實際查詢文本將采取以下形式。SELECT * FROM (SELECT TOP
ItemsPerPage
* FROM (SELECT TOP
ItemsPerPage*CurrentPageIndex
* FROM (
SelectCommand
) AS t0 ORDER BY
SortField
ASC) AS t1ORDER BY
SortField
DESC) AS t2 ORDER BY
SortField
該查詢彌補(bǔ)了 SQL Server 2000 中 ROWNUM 子句的缺陷,并且對記錄進(jìn)行重新排序,使得只有
x 項目的“第 n 個”塊經(jīng)過正確排序后返回。您可以指定基礎(chǔ)查詢,分頁程序?qū)⑺纸鉃槎鄠€較小的頁面。只有適合某個頁面的記錄被返回。正如您看到的那樣,除了查詢命令以外,上述查詢需要處理排序字段。這就是為什么另外添加了 SortField 屬性。此代碼的唯一缺陷是默認(rèn)情況為升序排序。通過使 ASC/DESC 要害字參數(shù)化,可以使此代碼真的非常完美:private void FetchPageData(){ // 需要經(jīng)過驗證的頁面索引來獲取數(shù)據(jù)。 // 還需要實際的頁數(shù)來驗證頁面索引。 AdjustSelectCommand(false); VirtualRecordCount countInfo = CalculateVirtualRecordCount(); TotalPages = countInfo.PageCount; // 驗證頁碼(確保 CurrentPageIndex 有效或為“-1”) ValidatePageIndex(); if (CurrentPageIndex == -1) return; // 預(yù)備并運(yùn)行命令 SqlCommand cmd = PrepareCommand(countInfo); if (cmd == null) return; SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable data = new DataTable(); adapter.Fill(data); // 配置分頁的數(shù)據(jù)源組件 if (_dataSource == null) _dataSource = new PagedDataSource(); _dataSource.AllowCustomPaging = true; _dataSource.AllowPaging = true; _dataSource.CurrentPageIndex = 0; _dataSource.PageSize = ItemsPerPage; _dataSource.VirtualCount = countInfo.RecordCount; _dataSource.DataSource = data.DefaultView; } 在 NonCached 模式中,PagedDataSource 對象并不提供整個數(shù)據(jù)源,因此不能計算出要進(jìn)行分頁的總頁數(shù)。進(jìn)而必須對 AllowCustomPaging 屬性進(jìn)行標(biāo)記,并提供數(shù)據(jù)源中的實際記錄數(shù)量。通常,實際數(shù)量是使用 SELECT COUNT(*) 查詢進(jìn)行檢索的。此模型與 DataGrid 的自定義分頁幾乎相同。此外,PagedDataSource 對象中選擇的當(dāng)前頁面索引通常為 0,因為實際上已經(jīng)存儲了一頁記錄。SqlPager 控件的實現(xiàn)方法就介紹到這里,下面我們介紹一下它的使用方法。使用 SqlPager 控件假設(shè)存在一個包含 ListBox 控件的示例頁面。要使用分頁程序,請確保 .aspx 頁面正確地注冊了該控件的程序集。 <%@ Register TagPrefix="expo" Namespace="DevCenter" Assembly="SqlPager" %> 控件的標(biāo)記取決于實際設(shè)置的屬性。以下標(biāo)記是一個合理的示例:<asp:listbox runat="server" id="ListBox1" Width="300px" Height="168px" DataTextField="companyname" /> <br><expo:SqlPager runat="server" id="SqlPager1" Width="300px" ControlToPaginate="ListBox1" SelectCommand="SELECT customerid, companyname FROM customers" ConnectionString="SERVER=localhost;DATABASE=northwind;UID=..." SortKeyField="companyname" /><br><asp:button runat="server" id="LoadFirst1" Text="加載第一頁" />
除了分頁程序以外,頁面還包含一個列表框和一個按鈕。列表框?qū)@示每個頁面的內(nèi)容;按鈕只用于首次填充列表框。該按鈕具有一個單擊事件處理程序,定義如下。private void LoadFirst1_Click(object sender, EventArgs e) { SqlPager1.CurrentPageIndex = 0; SqlPager1.DataBind(); } 圖 2 顯示操作中的頁面。
](http://www.knowsky.com/UploadFiles/20080308/20083814402678177802.gif)
圖 2:與 ListBox 控件協(xié)同工作的 SqlPager 控件。使用 DataList 控件可以生成一個更有意思的示例。目標(biāo)是使用分頁程序瀏覽每個 Northwind 職員的個人記錄。該 DataList 如以下列表所示。<asp:datalist runat="server" id="DataList1" Width="300px" Font-Names="Verdana" Font-Size="8pt"><ItemTemplate><table bgcolor="#f0f0f0" style="font-family:verdana;font-size:8pt;"> <tr><td valign="top"> <b><%# DataBinder.Eval(Container.DataItem, "LastName") + ", " + DataBinder.Eval(Container.DataItem, "firstname") %></b></td></tr> <tr><td> <span style="color:blue;"><i> <%# DataBinder.Eval(Container.DataItem, "Title")%></i></span> <p><img style="float:right;" src='image.aspx? id=<%# DataBinder.Eval(Container.DataItem, "employeeid")%>' /> <%# DataBinder.Eval(Container.DataItem, "Notes") %></td></tr></table></ItemTemplate></asp:datalist> 表格的第一行顯示職員的姓名和職務(wù),然后是相片,相片四周是注釋。相片是使用特定的 .aspx 頁面檢索的,返回從數(shù)據(jù)庫中獲取的 JPEG 數(shù)據(jù)。 分頁程序可以放置在頁面中的任何位置。本例中將它放置在合作者 DataList 控件上方并緊挨著合作者控件。
](http://www.knowsky.com/UploadFiles/20080308/20083814402682877803.gif)
圖 3:SqlPager 對 DataList 控件進(jìn)行分頁 將 SqlPager 控件與 DataGrid 控件一起使用有意義嗎?這要視情況而定。DataGrid 已經(jīng)與一個基于本文中使用的 PagedDataSource 對象的嵌入式分頁引擎一起工作。因此,假如您需要對以表格格式顯示的單個記錄集合進(jìn)行分頁時,就無需使用 SqlPager。但是,對于重要/復(fù)雜的方案,將這兩個控件一起使用并不是一種牽強(qiáng)的想法。例如,假如您要向前一個屏幕快照添加一個 DataGrid 來顯示由職員治理的訂單,則可以在同一格頁面上放置兩個相關(guān)的分頁引擎,一個要對職員進(jìn)行分頁,另一個用于滾動相關(guān)訂單。小結(jié)不管您要生成哪種類型的應(yīng)用程序(Web 應(yīng)用程序、Microsoft® Windows® 應(yīng)用程序或 Web 服務(wù)),您都很難下載和緩存要顯示的整個數(shù)據(jù)源。有時,測試環(huán)境會使您相信這種解決方案真是太棒了,非常可取。但是測試環(huán)境也會誤導(dǎo)。數(shù)據(jù)源的大小非常要害,應(yīng)用程序的規(guī)模越大,數(shù)據(jù)源的大小越要害。在 ASP.NET 中,只有 DataGrid 控件具有內(nèi)置的分頁功能。但是,分頁引擎是由相當(dāng)樣板化的代碼實現(xiàn)的,只要進(jìn)行少量的處理,就可以進(jìn)行推廣并用于多個不同的控件。本文介紹的 SqlPager 控件就實現(xiàn)了此目標(biāo)。它關(guān)注下載數(shù)據(jù),將數(shù)據(jù)分成多個頁面并通過合作者控件顯示。該控件可以檢索并緩存整個數(shù)據(jù)集,或者僅在 SQL Server 中查詢要在選定的頁面中顯示的幾個記錄。我說的是 SQL Server,這是另一個重點。SqlPager 只能用于 SQL Server,不能用于通過 OLE DB 或 ODBC 來檢索數(shù)據(jù)。也不能使用它訪問
Oracle 或 DB2 存檔。 要生成真正的通用 SQL 分頁程序組件,應(yīng)該形成數(shù)據(jù)訪問層,并生成一種能夠使用相應(yīng)的數(shù)據(jù)提供程序創(chuàng)建連接、命令和適配器的工廠類。此外,要注重為各種 SQL 源設(shè)置一個分頁引擎是最糟糕的做法。這里介紹的方法只適用于 SQL Server 7.0 和更高版本。TOP 子句在不同的環(huán)境下具有不同的特性。使用服務(wù)器游標(biāo)和臨時表格,它可適用于較大范圍的 DBMS 系統(tǒng)。但那將使代碼更為復(fù)雜。
作者簡介
Dino Esposito 是一位來自意大利羅馬的培訓(xùn)教師和顧問。他是 Wintellect(英文)小組的成員,專門研究 ASP.NET 和 ADO.NET,主要在歐洲和美國從事教學(xué)和咨詢工作。此外,Dino 還負(fù)責(zé)治理 Wintellect 的 ADO.NET 課件,并為 MSDN Magazine(英文)的“Cutting Edge”專欄撰寫文章。