DataGrid的客户端分页
原著:Jeff Prosise
翻译:xiaojin
原文出处:MSDN Magazine Feb 2004 (Client-side Paging for DataGrids)
原代码下载:WickedCode0402.exe (120KB)
开发人员对ASP.NET中的DataGrid控件又爱又恨。一方面,DataGrids大大简化了在HTML中通过表格中描述数据源,另一方面,DataGrid在服务器端如此重要,导致了大量的页面请求被传回服务器。例如,当用户使用分页DataGrid时,从一个页面转到另一个页面就会导致页面请求传回服务器。这样就会降低系统的性能。毫无疑问,广大开发人员的心愿就是有一种好的DataGrid,能够使包括数据记录的页面时不必重复发送请求到服务器。
从理论上讲,建立一个在客户端进行分页处理的HTML表格并不困难,因为有功能强大的客户端脚本。Figure 1中的HTML页面演示了实现客户端分页的一种方法。当页面在浏览器中被查看时(见Figure
2),显示为一张表格。但是实际上有3张表格放在了<DIV>范围内。每一个<DIV>确保任一时刻只有一个表格是可见的。要显示前一页或后一页,可以通过少量的JavaScript语句隐藏当前的<DIV>模块来实现。表格的显示和不显示使之看起来就像是在一张表格。

Figure 2:分页的HTML表格
我们可以用这个技术来实现DataGrid控件实现客户端分页。难点是如何修改DataGrid类来产生如Figure 1那样的外观。
这就是Wicked Code部分所要讨论的:通过建立一个插件取代客户端的分页,来增强DataGrid控件的性能。为了增加这个非常有用的工具,本文介绍一个通过继承并增加新功能来修改ASP.NET控件的好例子。

ClientPageDataGrid是从DataGrid控件继承而来并增加了客户端分页支持的ASP.NET控件。由于它在功能上取代了DataGrid,因此你可以在任何能使用DataGrid控件的地方使用它。
Figure 3中的WEB页面是按以往的方法实现分页的:重复发送数据请求到服务器。该DataGrid被设置为允许分页(AllowPaging="true")和每页显示16条记录(PageSize="16")。点击该页中DataGrid下面任何一个按钮,页面请求都将被发送到服务器并且触发PageIndexChanged事件,同时激发OnNewPage方法。OnNewPage方法将设置DataGrid的CurrentPageIndex属性为上一页或下一页的索引,并将新页面返回给客户端。
Figure 4显示了用ClientPageDataGrid实现的页面,页面变化如红色所示。DataGrid控件被ClientPageDataGrid控件取代了。为了实现分页,AllowPaging="true"改成了AllowClientPaging="true",PageSize="16"
改成了ClientPageSize="16"。OnPageIndexChanged属性和OnNewPage方法在新控件中已经被删除了。现在就可以实现完全在客户端分页了。
要使用ClientPageDataGrid 这个控件,请先将ClientPaging.aspx拷贝到ASP.NET
WEB服务器的一个虚目录中(例如:wwwroot),接着拷贝动态库ClientPageDataGrid.dll到该虚目录的bin子目录,最后在浏览器中打开ClientPaging.aspx这个文件。你就可以看到如Figure
5中所示的页面,可以用左下角的箭头向前或向后翻页。ClientPaging.aspx看起来并且感觉就像是ServerPaging.aspx,但实际上是大不相同的。ServerPaging.aspx页面多次从服务器端装入,并且翻页的速度受限于服务器和客户端的连接情况。相反,ClientPaging.aspx装入速度稍微慢点,是因为要下载页面全部记录,这比一页的数据要多。但是一旦页面装入完毕,翻页速度就与连接速度无关了。

Figure 5 ClientPageDataGrid
ClientPageDataGrid的开发接口包括4个公共属性和其他从DataGrid继承来的属性(如Figure 6所示)。AllowClientPaging属性设置是否允许翻页,默认为允许。因此你可以在设置ClientPageDataGrid时略过AllowClientPaging=true。ClientPageSize属性设置每页显示的记录条数,ClientPageCount获得页面的总数。ClientCurrentPageIndex设置当先哪页被显示。下面的Page_Load方法设置名字为MyDataGrid的ClientPageDataGrid控件显示第2页(索引0表示第1),而不是第1页。
void Page_Load (Object sender, EventArgs e)
{
if (!IsPostBack) {
BindDataGrid ();
MyDataGrid.ClientCurrentPageIndex = 1;
}
}
当ClientPageDataGrid所在页面的请求发送到服务器后,页面返回后,ClientCurrentPageIndex属性就被更新,来指示显示哪一页。服务器并不知道用户在看哪一页。

ClientPageDataGrid输出标页数的HTML表格从使用Render方法开始,如Figure 7所示。你可以通过本文上面的链接下载完整的源代码。Render是一个从System.Web.UI.Control继承来的一个虚方法。在Microsoft?
.NET Framework中,当包括该控件的页面被请求时,Render方法被用来将控件加入到HTML中去。
ClientPageDataGrid重写了DataGrid的方法并且多次调用基类的实现,而不是一次。特别说明的是,ClientPageDataGrid在输出每一页时调用DataGrid.Render,同时通过对RenderBeginTag和RenderEndTag的调用将输出加入到<DIV>模块中。在调用基类的Render方法前,ClientPageDataGrid.Render调用一个本地的ShowItems方法来隐藏那些在页面上不显示的记录,方法是设置那些行的Visible属性为否(见Figure 8)。
当ClientPageDataGrid被设置为每页显示16条记录,也就意味着第1个<DIV>包括该表格的前16条记录,第2个<DIV>包括该表格紧接着的16条记录,以此类推。除1个之外所有的<DIV>模块通过设置样式
style=”display:none”来隐藏。只有显示页的索引等于ClientCurrentPageIndex时,<DIV>模块才被显示,它的样式被设置为style=”display:block”。
ClientPageDataGrid中的每一页包括一个分页器,该分页器存在于DataGrid当中,因为ClientPageDataGrid重写了DataGrid的DataBind方法,并且通过设置AllowPaging为真、PageSize设置为每页显示条数来调用。ClientPageDataGrid.cs中大部分代码就是来使页面正常工作,确保支持Previous/next-style分页器(<PagerStyle
Mode="NextPrev">)和数字分页器(<PagerStyle Mode="NumericPages">)。
传统的包括LinkButtons的DataGrid分页器,发送页面请求到服务器,然后产生PageIndexChanged事件。ClientPageDataGrid用指向客户端的JavaScript函数的链接取代了这些LinkButtons。UpdatePager方法(如Figure 9所示),在Render调用基类的Render方法之前被取代。UpdatePager通过在DataGrid中搜索类型为ListItemType.Pager的行,来找到要显示的页面,接着就删除页面中的控件,加入需要的控件。
下面是一个传统的DataGrid,触发分页器时输出HTML的例子:
<tr>
<td colspan="3">
<a href= "javascript:__doPostBack(''MyDataGrid$_ctl20$_ctl0'','''')">
<</a>
<a href="javascript:__doPostBack(''MyDataGrid$_ctl20$_ctl1'','''')">
></a>
</td>
</tr>
下面是由ClientPageDataGrid控件实现的同一个页面:
<tr>
<td colspan="3">
<a href="javascript:__onPrevPage (''MyDataGrid'');"><</a>
<a href="javascript:__onNextPage (''MyDataGrid'');">></a>
</td>
</tr>
当ClientPageDataGrid.OnPreRender调用Page.RegisterClientScriptBlock时,导航标记所指向的JavaScript函数就被打上标记。调用RegisterClientScriptBlock确保包括这些函数的脚本块仅仅被输出一次,甚至当页面包括ClientPageDataGrid的多个实例时也是这样。
虽然ClientPageDataGrid并没有包括任何自己的服务器请求,但是页面的其他控件仍然会导致向服务器的请求。这产生了两个问题。第一,当请求发生时,ClientCurrentPageIndex应当进行更新,因此服务器端代码能决定哪一页被显示。第二,当页面从服务器反馈回后,ClientPageDataGrid应当能被当前页保存。用另外的话说就是,当第3页显示时向服务器请求时,ClientPageDataGrid的代码应当加入一个样式style="display:
block"给包括第3页的<DIV>模块,而其他<DIV>模块没有。
为解决这些问题,ClientPageDataGrid注册了一个隐含字段controlid__ PAGENUM:
Page.RegisterHiddenField (ClientID + "__PAGENUM",
ClientCurrentPageIndex.ToString ());
当页面更新时,在客户端中通过输出页面进行分页的JavaScript函数来更新隐含的字段。当页面请求发给服务器后,隐含字段的值被加入到服务器返回数据中。ClientPageDataGrid的LoadViewState方法从返回的数据中读取这个值,并且通过RestoreCurrentPageIndex方法设置ClientCurrentPageIndex的值,代码如下:
string page = Page.Request[ClientID + "__PAGENUM"];
...
ClientCurrentPageIndex = Convert.ToInt32 (page);
请求时间处理代码能通过ClientCurrentPageIndex属性的值判断用户要读取哪一页。同时因为ClientPageDataGrid.Render使用ClientCurrentPageIndex来判断哪一个<DIV>被显示,因此当前显示页直到页面数据请求发生后才隐藏。
在LoadViewState带来的反馈数据中包括当前页索引非常有用。首先,当前页的索引不会被设置直到控件被组装,存在LoadViewState中。此外,由于LoadViewState在主页的装入事件触发前被调用,Page_Load方法能通过ClientCurrentPageIndex属性值来判断用户的当前页的索引。那样是非常重要的,因为当前页的索引会带来其他的变化。
RestoreCurrentPageIndex同样被ClientPageDataGrid的DataBind方法调用,而且仅仅是,如果不是被LoadViewState调用的话。为什么呢?因为如果视图状态被禁用了(即EnableViewState=false),则LoadViewState就不能被ASP.NET运行库调用。如果视图状态被禁用,页面就会调用DataBind重新装载控件,因此调用DataBind方法就是根据返回数据恢复当前页的索引就很自然了。
或许你注意到了ClientPageDataGrid的DataBind方法调用绑定数据并不是一次,而是两次(见Figure 10)。第1次调用是基类的DataBind方法给ClientPageDataGrid提供记录条数,产生一个包括所有数据的DataGrid。DataGrid放在ClientPageDataGrid下面,为描述数据源提供服务。
ClientPageDataGrid的DataBing方法另一个特别的地方是它将DataReaders转变为DataSets.。有两个理由这么做。首先,DataReader只能被绑定一次,因为它是只能向前读取的数据源。其次,通常的DataGrid并不支持DataReaders作为数据源,如果AllowPaging值为true的话,直到AllowCustomPaging也为true时才行。在程序内部巧妙地将DataReaders转换为DataSets就解决了这些问题,并且确保ClientPageDataGrid工作起来像使用DataReaders一样,而实际上是在使用DataSets。

难道在客户端分页就没有缺点吗?你讲对了。数据量越大,页面装入速度就会越慢,因为要装入全部的数据,而不只是显示页的数据。相对较小的记录集,1000条记录或更少,你就可以不关注这个问题了。对于非常大的记录集,装入时间就将会是难以接受的长。
另外一个要考虑的因素是带宽,大的DataGrid会在两方面增加下载时间。第一会增大HTML表格,第二会增加视图状态的大小。因为ASP.NET通过一个隐含字段保持视图状态,DataGrid的内容本来就是在HTTP请求时显示一次,在HTTP响应时显示两次。这就是要保证数据量少,大的数据量还是最好用服务器端分页。当有大量的记录时你还要使用ClientPageDataGrid,最好设置控件的EnableViewState属性为false,并且在每一个页面请求时绑定数据。这样会减少相应大小约2/3。
对ClientPageDataGrid的最后一个思考是,你大概会设置ClientPageSize为一个偶数,如果你使用AlternatingItemStyle来呈现奇数项来区别于偶数项。第一页将在顶端拥有一个不交互的项。第二页将有一个交互的项,并且其他也这样。这样会导致用户考虑为什么表格格式变来变去,每次都不同于前面的页面。

DataGrid是ASP.NET中最重要的功能之一,归功于它在重用类中隐含了复杂的构造和非凡的逻辑。ClientPageDataGrid
将给服务器端的控件带来一个适应未来步伐的思想,证明了不仅仅要考虑如何修改嵌入控件来使用,而且还要考虑如何扩展客户端的功能。服务器控件依赖于客户端脚本,工作将更有效率,这是一个非常好的主意。希望所有控件的作者同意这个看法。
有什么问题或建议,请发邮件到:wicked@microsoft.com。

Jeff Prosise是MSDN杂志的资深编辑,和一些书的作者,例如《Programming Microsoft
.NET》(微软出版社,2002),同时他还是Wintellect网站(http://www.wintellect.com)的共同创办人,该网站是一个专门提供Microsoft
.NET的咨询和教育公司。
本文出自
MSDN Magazine,2004年2月号。你可以从附近的报摊获得,更好的方法是
订阅。
