創(chuàng)建用于ASP.NET的分頁程序控件
2024-07-10 13:05:33
供稿:網(wǎng)友
 
 
摘要:解決向任何 asp.net 控件添加分頁功能的問題。還為開發(fā)復(fù)合 asp.net 控件提供了很多有用的提示和技巧。 
下載本文的源代碼(英文)。(請(qǐng)注意,在示例文件中,程序員的注釋使用的是英文,本文中將其譯為中文是為了便于讀者理解。)
從程序員的角度來看,microsoft® sql server™ 查詢的最大缺陷之一就是返回的行數(shù)通常比應(yīng)用程序的用戶界面實(shí)際可以容納的行數(shù)要多得多。這種尷尬情形經(jīng)常將開發(fā)人員陷于困境。開發(fā)人員是應(yīng)該創(chuàng)建一個(gè)非常長的頁面,讓用戶花時(shí)間去滾動(dòng)瀏覽,還是應(yīng)該通過設(shè)置一個(gè)手動(dòng)分頁機(jī)制來更好地解決這個(gè)問題? 
哪種解決方案更好,在很大程度上取決于要檢索的數(shù)據(jù)的特性。由多個(gè)項(xiàng)目(如搜索結(jié)果)組成的較長列表,最好通過各頁大小相等、每頁相對(duì)較短的多個(gè)頁面顯示。由單個(gè)項(xiàng)目(如文章的文本)組成的較長列表,如果整個(gè)插入在一個(gè)頁面中,使用起來會(huì)更方便。最后得出的分析結(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)則不支持分頁。這些控件及其他列表控件不支持分頁,不是因?yàn)樗鼈冊(cè)诮Y(jié)構(gòu)上不支持分頁,而是因?yàn)樗鼈兣c datagrid 不同,不包含任何處理分頁的特定代碼。但是,處理分頁的代碼是相當(dāng)樣板化的,可以添加到所有這些控件中。 
scott mitchell 在最近的一篇題目為“creating a pageable, sortable datagrid”(英文)的文章中,介紹了 datagrid 分頁。該文還引用了 web 上的其他有用信息,為您提供了有關(guān)網(wǎng)格分頁基礎(chǔ)知識(shí)和其他信息。如果想查看如何使 datalist 控件可以進(jìn)行分頁的示例,可以查看此文章(英文)。該文演示了如何創(chuàng)建一個(gè)自定義的 datalist 控件,該控件具有當(dāng)前索引和頁面大小屬性,并可以啟動(dòng)頁面更改事件。 
同樣的代碼也可以用于滿足其他列表控件(如 listbox 和 checkboxlist)的分頁需要。不過,向各個(gè)控件添加分頁功能實(shí)際上并不是一種非常好的做法,因?yàn)椋缟纤觯猪摯a是相當(dāng)樣板化的。因此,對(duì)于聰明的程序員來說,有什么方法比使用一種新的通用分頁程序控件來實(shí)現(xiàn)所有這些控件的分頁功能更好的呢?
本文中將建立一個(gè)分頁程序控件,它將使合作者列表控件能夠?qū)?sql server 的查詢結(jié)果進(jìn)行分頁。該控件名為 sqlpager,它支持兩種類型的合作者控件 - 列表控件和基礎(chǔ)數(shù)據(jù)列表控件。
sqlpager 控件的顯著特點(diǎn)
sqlpager 控件是一個(gè) asp.net 復(fù)合控件,包含一個(gè)單行表格。該行又包含兩個(gè)單元格 - 導(dǎo)航條和頁面描述符。該控件的用戶界面呈條形,理想情況下,其寬度與合作者控件的寬度相同。導(dǎo)航條部分提供了可單擊的元素,以便在頁面之間移動(dòng);頁面描述符部分為用戶提供了有關(guān)當(dāng)前顯示的頁面的一些反饋信息。 
圖 1:visual studio .net 網(wǎng)頁設(shè)計(jì)器中顯示的 sqlpager 控件
與 datagrid 控件的嵌入式分頁程序一樣,sqlpager 控件具有兩種導(dǎo)航模式,即下一頁/上一頁和數(shù)字頁面。此外,其特殊屬性 pagerstyle 使您能夠選擇更方便的樣式。該控件與列表控件協(xié)同工作。您可以通過 controltopaginate 字符串屬性為分頁程序指定一個(gè)這樣的合作者控件。 
sqlpager1.controltopaginate = "listbox1"; 
 
一般情況下,分頁程序首先獲取 sql server 的查詢結(jié)果,準(zhǔn)備一個(gè)適當(dāng)?shù)挠涗涰撁妫缓笸ㄟ^合作者控件的 datasource 屬性顯示該頁面。當(dāng)用戶單擊以查看新頁面時(shí),分頁程序?qū)z索請(qǐng)求的數(shù)據(jù)并再次通過合作者控件來顯示數(shù)據(jù)。分頁機(jī)制對(duì)于列表控件是完全透明的。列表控件的數(shù)據(jù)源是通過編程方式進(jìn)行更新的,任何時(shí)候都只包含適合當(dāng)前頁面的記錄。 
控件的分頁引擎具有多個(gè) public 屬性,如 currentpageindex、itemsperpage 和 pagecount,通過這些屬性來獲取并設(shè)置當(dāng)前頁面的索引、每個(gè)頁面的大小以及要顯示的頁面的總數(shù)。分頁程序管理數(shù)據(jù)檢索和分頁所需的任何邏輯。 
selectcommand 屬性設(shè)置獲取數(shù)據(jù)所用的命令文本。connectionstring 屬性定義數(shù)據(jù)庫的名稱和位置以及連接憑據(jù)。執(zhí)行查詢時(shí)采用的方式取決于 pagingmode 屬性的值。該屬性的可能值為與其同名的 pagingmode 枚舉的值 - cached 和 noncached。如果選擇 cached 選項(xiàng),則將使用數(shù)據(jù)適配器和 datatable 對(duì)象檢索整個(gè)結(jié)果集。可以選擇將結(jié)果集放置在 asp.net 的 cache 對(duì)象中,該結(jié)果集可以重復(fù)使用直到過期。如果選擇 noncached 選項(xiàng),則查詢只檢索適合當(dāng)前頁面的記錄。這時(shí),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 屬性 同一個(gè) .aspx 頁面中的控件 id,它將顯示分頁程序檢索的記錄頁面。這是合作者控件。 
currentpageindex 屬性 獲取并設(shè)置基于 0 的頁面索引。 
itemsperpage 屬性 獲取并設(shè)置每頁要顯示的記錄數(shù)量。默認(rèn)值為每頁顯示 10 個(gè)項(xiàng)目。 
pagerstyle 屬性 該值指示分頁程序用戶界面的樣式。它可以為 pagerstyle 枚舉值:nextprev 和 numericpages 之一。在 nextprev 模式中,將顯示 vcr 式的按鈕,來轉(zhuǎn)到第一頁、上一頁、下一頁和最后一頁。而在 numericpages 模式中,將顯示一個(gè)下拉列表,列出所有可用頁面的索引。 
pagingmode 屬性 該值指示檢索數(shù)據(jù)的方式。它可以為 pagingmode 枚舉值:cached 和 noncached 之一。如果為 cached,則將使用數(shù)據(jù)適配器,且整個(gè)結(jié)果集將臨時(shí)放置在 asp.net 緩存中。如果為 noncached,則只檢索當(dāng)前頁面中的記錄。在這種情況下,不進(jìn)行緩存。 
selectcommand 屬性 用來進(jìn)行查詢的命令文本。最好為 select-from-where 形式。不支持 order by 子句。排序是通過 sortfield 屬性另外指定的。 
sortfield 屬性 用來排序的字段的名稱。此字段用于為查詢提供動(dòng)態(tài)的 order by 子句。排序是由 sql server 執(zhí)行的。 
clearcache 方法 刪除存儲(chǔ)在 asp.net 緩存中的任何數(shù)據(jù)。 
pageindexchanged 事件 默認(rèn)事件,當(dāng)分頁程序移動(dòng)到另一個(gè)頁面時(shí)發(fā)生。事件的數(shù)據(jù)結(jié)構(gòu)為 pagechangedeventargs 類,包含舊頁面和新頁面的索引。 
由于 sqlpager 控件繼承了 webcontrol,因此它也具有很多與 ui 相關(guān)的屬性來管理字體、邊框和顏色。 
生成 sqlpager 控件
將作為復(fù)合控件來生成 sqlpager 控件并讓其繼承 webcontrol 類。復(fù)合控件是 asp.net 服務(wù)器控件所特有的,它是由一個(gè)或多個(gè)服務(wù)器控件組成。 
public class sqlpager : webcontrol, inamingcontainer
{ ... }
 
除非生成完全自定義的控件或擴(kuò)展現(xiàn)有的控件,否則,創(chuàng)建新控件時(shí),大多數(shù)時(shí)間實(shí)際上是在生成復(fù)合控件。要?jiǎng)?chuàng)建 sqlpager,組合一個(gè) table 控件,并根據(jù)分頁程序的樣式,組合幾個(gè) linkbutton 控件或者一個(gè) dropdownlist 控件。 
生成復(fù)合控件時(shí),需要注意幾條原則。首先,需要覆蓋 createchildcontrols protected 方法。createchildcontrols 方法是從 control 繼承來的,當(dāng)服務(wù)器控件為了顯示而要?jiǎng)?chuàng)建子控件時(shí)或在返回后,將調(diào)用此方法。 
protected override void createchildcontrols()
{
 // 清除現(xiàn)有的子控件及其 viewstate
 controls.clear();
 clearchildviewstate();
 // 生成控件樹
 buildcontrolhierarchy();
}
 
覆蓋此方法時(shí),需要執(zhí)行幾項(xiàng)重要的操作。創(chuàng)建并初始化任何所需的子控件實(shí)例并將它們添加到父控件的 controls 集合中。但是,生成新控件樹之前,應(yīng)該刪除任何現(xiàn)有的子控件并清除子控件可能留下的任何 viewstate 信息。 
復(fù)合組件還需要實(shí)現(xiàn) inamingcontainer 接口,以便 asp.net 運(yùn)行時(shí)可以為其創(chuàng)建一個(gè)新的命名范圍。這就確保了復(fù)合控件中的所有控件都具有唯一的名稱。這還將確保能夠自動(dòng)處理子控件的返回?cái)?shù)據(jù)。 
對(duì)于 sqlpager 控件來說,成為命名容器非常重要。事實(shí)上,sqlpager 包含一些 linkbutton 控件,并且需要獲取并處理其單擊事件以便導(dǎo)航頁面。正如 asp.net 頁面中的任何其他控件一樣,linkbutton 也被賦予了一個(gè) id,用于標(biāo)識(shí)處理返回事件的控件。 
處理返回事件時(shí),asp.net 運(yùn)行時(shí)試圖查找事件的目標(biāo) id 與主窗體的任何直接子控件之間是否存在匹配關(guān)系。linkbutton 是分頁程序的子控件,因此不能運(yùn)行其服務(wù)器端的代碼。這是否意味著只有窗體的直接子控件才能啟動(dòng)并處理服務(wù)器事件?當(dāng)然不是,只要您使用命名容器。 
通過使 sqlpager 控件實(shí)現(xiàn) inamingcontainer 接口,可以將嵌入式鏈接按鈕的實(shí)際 id 從“first”更改為“sqlpager1:first”。當(dāng)用戶單擊以查看新頁面時(shí),返回事件將 sqlpager1:first 作為目標(biāo)控件。實(shí)際上,運(yùn)行時(shí)用來識(shí)別目標(biāo)控件的算法比上面介紹的要復(fù)雜一些。運(yùn)行時(shí)將事件目標(biāo)控件的名稱看作是用冒號(hào)分隔開的字符串。實(shí)際上,這種匹配是在窗體的子控件和用冒號(hào)分隔開的字符串(如 sqlpager1:first)的第一個(gè)標(biāo)記之間進(jìn)行的。由于分頁程序是窗體的子控件,因此匹配時(shí)會(huì)成功,分頁程序獲取單擊事件。如果您認(rèn)為這種解釋不夠充分或者令人費(fèi)解,只要下載 sqlpager 控件的源代碼,刪除 inamingcontainer 標(biāo)記接口并進(jìn)行重新編譯即可。您將看到分頁程序能夠返回,但不能內(nèi)部處理單擊事件。
inamingcontainer 接口是一個(gè)不具備方法的標(biāo)記接口,其實(shí)現(xiàn)只需在類聲明中指定名稱即可,無需進(jìn)行任何其他操作。
復(fù)合控件的另一個(gè)重要方面是,它們通常不需要自定義邏輯來進(jìn)行顯示。復(fù)合控件的顯示遵循其組成控件的顯示。生成復(fù)合控件時(shí),通常無需覆蓋 render 方法。 
控件的 sqlpager 樹由一個(gè)包含兩個(gè)單元格的單行表格組成。該表格繼承了分頁程序的大部分可視設(shè)置,如前景顏色和背景顏色、邊框、字體信息和寬度等。第一個(gè)單元格包含導(dǎo)航條,其結(jié)構(gòu)取決于 pagerstyle 屬性的值。如果分頁程序的樣式為 nextprev,則導(dǎo)航條將由四個(gè) vcr 式的按鈕組成。否則,它將由一個(gè)下拉列表組成。
private void buildcontrolhierarchy()
{
 // 生成環(huán)境表格(一行,兩個(gè)單元格)
 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);
}
 
將各個(gè)控件添加到正確的 controls 集合對(duì)于分頁程序的正確顯示極其重要。最外層的表格必須添加到分頁程序的 controls 集合中。鏈接按鈕和下拉列表必須添加到相應(yīng)表格單元格的 controls 集合中。
下面給出了用來生成鏈接按鈕導(dǎo)航條的代碼。每個(gè)按鈕都顯示有一個(gè) 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.tooltip = "第一頁";
 first.text = "7"; 
 first.enabled = isvalidpage && canmoveback;
 cell.controls.add(first);
 :
}
 
分頁程序的另一種樣式(在下拉列表中列出數(shù)字頁面)的生成方法如下所示:
private void buildnumericpagesui(tablecell cell)
{
 // 顯示一個(gè)下拉列表
 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)最終都會(huì)更改當(dāng)前顯示的頁面。這兩種方法都會(huì)調(diào)用一個(gè)公用的 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)
{
 // 準(zhǔn)備事件數(shù)據(jù)
 pagechangedeventargs e = new pagechangedeventargs();
 e.oldpageindex = currentpageindex;
 e.newpageindex = pageindex;
 // 更新當(dāng)前的索引
 currentpageindex = pageindex;
 // 啟動(dòng)頁面更改事件
 onpageindexchanged(e);
 // 綁定新數(shù)據(jù)
 databind();
}
 
其他導(dǎo)航按鈕的處理程序與 first_click 的區(qū)別僅在于它們傳遞給 gotopage 方法的頁碼不同。gotopage 方法負(fù)責(zé)處理 pageindexchanged 事件,并負(fù)責(zé)啟動(dòng)數(shù)據(jù)綁定過程。它準(zhǔn)備事件數(shù)據(jù)(舊頁面和新頁面索引)并觸發(fā)事件。gotopage 被定義為 private,但是可以使用 currentpageindex 屬性通過編程的方式更改顯示的頁面。 
public int currentpageindex
{
 get {return convert.toint32(viewstate["currentpageindex"]);}
 set {viewstate["currentpageindex"] = value;}
}
 
與表 1 中所列的所有屬性一樣,currentpageindex 屬性的實(shí)現(xiàn)方法也相當(dāng)簡(jiǎn)單。它將其內(nèi)容保存到 viewstate 中并從中進(jìn)行還原。在數(shù)據(jù)綁定過程中,會(huì)驗(yàn)證和使用頁面索引。
數(shù)據(jù)綁定過程
databind 方法是所有 asp.net 控件公用的,對(duì)于數(shù)據(jù)綁定控件來說,它將觸發(fā)用戶界面的刷新以反映新數(shù)據(jù)。sqlpager 控件根據(jù) selectcommand 和 connectionstring 屬性的值,使用此方法啟動(dòng)數(shù)據(jù)檢索操作。不言而喻,如果這些屬性中的任何一個(gè)為空,該過程將終止。同樣,如果合作者控件不存在,數(shù)據(jù)綁定過程將被取消。要查找合作者控件,databind 方法使用 page 類中的 findcontrol 函數(shù)。由此可見,合作者控件必須為主窗體的直接子控件。
進(jìn)行分頁顯示的控件不能為任意的 asp.net 服務(wù)器控件。它必須為列表控件或基本數(shù)據(jù)列表控件。更一般來說,合作者控件必須具有 datasource 屬性并實(shí)現(xiàn) databind 方法。可能進(jìn)行分頁的控件實(shí)際上只需要滿足這些要求。microsoft® .net framework 中所有繼承 listcontrol 或 basedatalist 的控件都滿足第一個(gè)要求;而所有 web 控件通過設(shè)計(jì)都滿足 databind 要求。使用當(dāng)前的實(shí)現(xiàn)方法,無法使用 sqlpager 控件來對(duì) repeater 進(jìn)行分頁。repeater 與合作者控件 datalist 和 datagrid 不同,不繼承 basedatalist,也不提供列表控件的功能。下表列出了可以使用 sqlpager 進(jìn)行分頁的控件。
表 2:可以由 sqlpager 控件進(jìn)行分頁的數(shù)據(jù)綁定控件 
控件 說明 
checkboxlist 從 listcontrol 派生而來,顯示為復(fù)選框列表。 
dropdownlist 從 listcontrol 派生而來,顯示為字符串下拉列表。 
listbox 從 listcontrol 派生而來,顯示為字符串可滾動(dòng)列表。 
radiobuttonlist 從 listcontrol 派生而來,顯示為單選按鈕列表。 
datalist 從 basedatalist 派生而來,顯示為模板化數(shù)據(jù)項(xiàng)目列表。 
datagrid 從 basedatalist 派生而來,顯示為數(shù)據(jù)項(xiàng)目的表格網(wǎng)格。datagrid 是唯一一個(gè)內(nèi)置有功能強(qiáng)大的分頁引擎的 asp.net 控件。 
以下代碼說明由 sqlpager 控件實(shí)現(xiàn)的數(shù)據(jù)綁定過程。 
public override void databind()
{
 // 啟動(dòng)數(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í)例上。此類提供了一些用來對(duì)數(shù)據(jù)進(jìn)行分頁的功能。特別是,當(dāng)整個(gè)數(shù)據(jù)集都存儲(chǔ)在緩存中時(shí),該類將自動(dòng)檢索當(dāng)前頁面的記錄并返回布爾值,來提供有關(guān)第一頁和最后一頁的信息。稍后將回來介紹此類的內(nèi)部結(jié)構(gòu)。在上述列表中,幫助程序的 pageddatasource 對(duì)象是由 _datasource 變量表示的。
然后,sqlpager 控件經(jīng)過計(jì)算得出合作者控件的類型,并將 pageddatasource 對(duì)象的內(nèi)容綁定到合作者控件的 datasource 屬性。
有時(shí),上述的 databind 方法還將 childcontrolscreated 屬性重新設(shè)置為 false。那么,為什么要這樣做呢? 
當(dāng)包含分頁程序的頁面返回時(shí),所有控件都要重新創(chuàng)建;分頁程序也不例外。通常情況下,所有控件及其子控件都是在準(zhǔn)備顯示頁面之前創(chuàng)建的。在每個(gè)控件接收到 onprerender 通知之前的一瞬間,protected ensurechildcontrols 方法將被調(diào)用,這樣,每個(gè)控件都可以生成自己的控件樹。此事件發(fā)生后,數(shù)據(jù)綁定過程完成,新數(shù)據(jù)已存儲(chǔ)到緩存中。 
但是,當(dāng)由于單擊分頁程序的一個(gè)組成控件而使頁面返回時(shí)(即用戶單擊以更改頁面),就會(huì)生成分頁程序的控件樹,這時(shí)遠(yuǎn)未達(dá)到顯示階段。特別是,當(dāng)處理相關(guān)的服務(wù)器端事件時(shí),就必須生成控件樹,因而是在數(shù)據(jù)綁定開始之前生成控件樹。困難在于,數(shù)據(jù)綁定將修改頁面索引,而這必須反映在用戶界面中。如果不采取某些對(duì)策的話,當(dāng)用戶切換到另一頁時(shí),分頁程序中的頁面索引將不會(huì)被刷新。
有各種方法可以解決此問題,但重要的是要弄清楚問題及其真正的原因。您可以避免生成控件樹,并在 render 方法中生成所有輸出。另外,您還可以修改樹中受數(shù)據(jù)綁定更改影響的部分。本文選擇了第三種方法,這種方法需要較少的代碼,而且,不管控件的用戶界面的哪個(gè)部分受到數(shù)據(jù)綁定更改的影響,都能夠解決問題。通過將 childcontrolscreated 屬性設(shè)置為 false,可以使以前創(chuàng)建的任何控件樹無效。這樣,在顯示之前將重新創(chuàng)建控件樹。
分頁引擎
sqlpager 控件支持兩種檢索數(shù)據(jù)的方法 - 緩存和非緩存。如果采用前者,選擇命令原樣執(zhí)行,整個(gè)結(jié)果集將綁定到在內(nèi)部進(jìn)行分頁的數(shù)據(jù)源對(duì)象上。pageddatasource 對(duì)象將自動(dòng)返回適合特定頁面的記錄。pageddatasource 類也是在 datagrid 默認(rèn)分頁機(jī)制背后運(yùn)行的系統(tǒng)組件。
檢索所有記錄但只顯示適合頁面的幾個(gè)記錄,這通常不是一種明智的方法。由于 web 應(yīng)用程序的無狀態(tài)特性,事實(shí)上,每次請(qǐng)求頁面時(shí)都可能運(yùn)行大量的查詢。要使操作有效,采用緩存的數(shù)據(jù)檢索方法必須依賴于某種緩存對(duì)象,asp.net 的 cache 對(duì)象就是一個(gè)很好的候選對(duì)象。緩存技術(shù)的使用加快了應(yīng)用程序的運(yùn)行速度,但其提供的數(shù)據(jù)快照不能反映最新的更改。另外,它需要使用 web 服務(wù)器上的較多內(nèi)存。而且荒謬的是,如果大量的數(shù)據(jù)按會(huì)話緩存的話,甚至可能造成很大的問題。cache 容器對(duì)于應(yīng)用程序來說是全局的;如果數(shù)據(jù)按會(huì)話存儲(chǔ)在其中,還需生成會(huì)話特有的項(xiàng)目名稱。
cache 對(duì)象的上面是完全支持過期策略的。換句話說,存儲(chǔ)在緩存中的數(shù)據(jù)在一段時(shí)間后可以自動(dòng)被釋放。以下代碼說明 sqlpager 類中用來獲取數(shù)據(jù)并將其存儲(chǔ)在緩存中的一個(gè) 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;
}
 
控件和請(qǐng)求的緩存項(xiàng)目名稱是唯一的。它包括頁面的 url 和控件的 id。在指定的時(shí)間(以秒計(jì)算)內(nèi),數(shù)據(jù)被綁定到緩存。要使項(xiàng)目過期,必須使用 cache.insert 方法。以下較簡(jiǎn)單的代碼將項(xiàng)目添加到緩存,但不包括任何過期策略。
page.cache[cachekeyname] = data;
 
pageddatasource 對(duì)象通過其 datasource 屬性獲取要進(jìn)行分頁的數(shù)據(jù)。值得注意的是,pageddatasource 類的 datasource 屬性只接受 ienumerable 對(duì)象。datatable 不滿足此要求,這就是為什么采取 defaultview 屬性的原因。
selectcommand 屬性確定在 sql server 數(shù)據(jù)庫上運(yùn)行的查詢。此字符串最好為 select-from-where 形式。不支持 order by 子句,如果指定了該子句,將被刪除。這正是 adjustselectcommand 方法所做的。使用 sortfield 屬性可以指定任何排序信息。adjustselectcommand 方法本身將根據(jù) sortfield 的值添加一個(gè)正確的 order by 子句。這樣做有什么原因嗎?
當(dāng)分頁程序以 noncached 模式工作時(shí),原始的查詢將被修改以確保只檢索當(dāng)前頁面的記錄。在 sql server 上執(zhí)行的實(shí)際查詢文本將采取以下形式。
select * from 
(select top itemsperpage * from 
(select top itemsperpage*currentpageindex * from 
(selectcommand) as t0 
order by sortfield asc) as t1
order by sortfield desc) as t2 
order by sortfield
 
該查詢彌補(bǔ)了 sql server 2000 中 rownum 子句的缺陷,并且對(duì)記錄進(jìn)行重新排序,使得只有 x 項(xiàng)目的“第 n 個(gè)”塊經(jīng)過正確排序后返回。您可以指定基礎(chǔ)查詢,分頁程序?qū)⑺纸鉃槎鄠€(gè)較小的頁面。只有適合某個(gè)頁面的記錄被返回。正如您看到的那樣,除了查詢命令以外,上述查詢需要處理排序字段。這就是為什么另外添加了 sortfield 屬性。此代碼的唯一缺陷是默認(rèn)情況為升序排序。通過使 asc/desc 關(guān)鍵字參數(shù)化,可以使此代碼真的非常完美:
private void fetchpagedata()
{
 // 需要經(jīng)過驗(yàn)證的頁面索引來獲取數(shù)據(jù)。
 // 還需要實(shí)際的頁數(shù)來驗(yàn)證頁面索引。
 adjustselectcommand(false);
 virtualrecordcount countinfo = calculatevirtualrecordcount();
 totalpages = countinfo.pagecount;
 // 驗(yàn)證頁碼(確保 currentpageindex 有效或?yàn)椤?1”)
 validatepageindex();
 if (currentpageindex == -1)
 return;
 // 準(zhǔn)備并運(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 對(duì)象并不提供整個(gè)數(shù)據(jù)源,因此不能計(jì)算出要進(jìn)行分頁的總頁數(shù)。進(jìn)而必須對(duì) allowcustompaging 屬性進(jìn)行標(biāo)記,并提供數(shù)據(jù)源中的實(shí)際記錄數(shù)量。通常,實(shí)際數(shù)量是使用 select count(*) 查詢進(jìn)行檢索的。此模型與 datagrid 的自定義分頁幾乎相同。此外,pageddatasource 對(duì)象中選擇的當(dāng)前頁面索引通常為 0,因?yàn)閷?shí)際上已經(jīng)存儲(chǔ)了一頁記錄。
sqlpager 控件的實(shí)現(xiàn)方法就介紹到這里,下面我們介紹一下它的使用方法。
使用 sqlpager 控件
假設(shè)存在一個(gè)包含 listbox 控件的示例頁面。要使用分頁程序,請(qǐng)確保 .aspx 頁面正確地注冊(cè)了該控件的程序集。 
<%@ register tagprefix="expo" namespace="devcenter" assembly="sqlpager" %>
 
控件的標(biāo)記取決于實(shí)際設(shè)置的屬性。以下標(biāo)記是一個(gè)合理的示例:
<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="加載第一頁" /> 
 
除了分頁程序以外,頁面還包含一個(gè)列表框和一個(gè)按鈕。列表框?qū)@示每個(gè)頁面的內(nèi)容;按鈕只用于首次填充列表框。該按鈕具有一個(gè)單擊事件處理程序,定義如下。
private void loadfirst1_click(object sender, eventargs e) {
 sqlpager1.currentpageindex = 0;
 sqlpager1.databind(); 
}
 
圖 2 顯示操作中的頁面。
圖 2:與 listbox 控件協(xié)同工作的 sqlpager 控件。
使用 datalist 控件可以生成一個(gè)更有意思的示例。目標(biāo)是使用分頁程序?yàn)g覽每個(gè) northwind 職員的個(gè)人記錄。該 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 控件上方并緊挨著合作者控件。
圖 3:sqlpager 對(duì) datalist 控件進(jìn)行分頁 
將 sqlpager 控件與 datagrid 控件一起使用有意義嗎?這要視情況而定。datagrid 已經(jīng)與一個(gè)基于本文中使用的 pageddatasource 對(duì)象的嵌入式分頁引擎一起工作。因此,如果您需要對(duì)以表格格式顯示的單個(gè)記錄集合進(jìn)行分頁時(shí),就無需使用 sqlpager。但是,對(duì)于重要/復(fù)雜的方案,將這兩個(gè)控件一起使用并不是一種牽強(qiáng)的想法。例如,如果您要向前一個(gè)屏幕快照添加一個(gè) datagrid 來顯示由職員管理的訂單,則可以在同一格頁面上放置兩個(gè)相關(guān)的分頁引擎,一個(gè)要對(duì)職員進(jìn)行分頁,另一個(gè)用于滾動(dòng)相關(guān)訂單。
小結(jié)
不管您要生成哪種類型的應(yīng)用程序(web 應(yīng)用程序、microsoft® windows® 應(yīng)用程序或 web 服務(wù)),您都很難下載和緩存要顯示的整個(gè)數(shù)據(jù)源。有時(shí),測(cè)試環(huán)境會(huì)使您相信這種解決方案真是太棒了,非常可取。但是測(cè)試環(huán)境也會(huì)誤導(dǎo)。數(shù)據(jù)源的大小非常關(guān)鍵,應(yīng)用程序的規(guī)模越大,數(shù)據(jù)源的大小越關(guān)鍵。
在 asp.net 中,只有 datagrid 控件具有內(nèi)置的分頁功能。但是,分頁引擎是由相當(dāng)樣板化的代碼實(shí)現(xiàn)的,只要進(jìn)行少量的處理,就可以進(jìn)行推廣并用于多個(gè)不同的控件。本文介紹的 sqlpager 控件就實(shí)現(xiàn)了此目標(biāo)。它關(guān)注下載數(shù)據(jù),將數(shù)據(jù)分成多個(gè)頁面并通過合作者控件顯示。該控件可以檢索并緩存整個(gè)數(shù)據(jù)集,或者僅在 sql server 中查詢要在選定的頁面中顯示的幾個(gè)記錄。我說的是 sql server,這是另一個(gè)重點(diǎn)。sqlpager 只能用于 sql server,不能用于通過 ole db 或 odbc 來檢索數(shù)據(jù)。也不能使用它訪問 oracle 或 db2 存檔。 
要生成真正的通用 sql 分頁程序組件,應(yīng)該形成數(shù)據(jù)訪問層,并生成一種能夠使用相應(yīng)的數(shù)據(jù)提供程序創(chuàng)建連接、命令和適配器的工廠類。此外,要注意為各種 sql 源設(shè)置一個(gè)分頁引擎是最糟糕的做法。這里介紹的方法只適用于 sql server 7.0 和更高版本。top 子句在不同的環(huán)境下具有不同的特性。使用服務(wù)器游標(biāo)和臨時(shí)表格,它可適用于較大范圍的 dbms 系統(tǒng)。但那將使代碼更為復(fù)雜。