报表数据的分页与排序24
简介
在联机应用程序中,分页和排序是在显示数据时极为常用的两项功能。例如,当我们在一家网络书店搜索 ASP.NET 书籍时,也许会有上百本相关书籍,但是罗列搜索结果的报表仅在每页显示十条匹配的记录。而且,这些搜索结果还可以根据标题、价格、页数、作者姓名和其它条件进行排序。在前面的23 篇教程中,我们已经学习了如何构建各式各样的报表,包括允许添加、编辑和删除数据的界面。但我们还没有学习如何对数据进行排序,并且仅在研究 DetailsView 和 FormView 控件时看到了分页的示例。
在本教程中,我们将了解怎样向报表添加排序和分页功能,这两项功能的实现仅通过选中几个复选框就可以完成。然而,这种简单的实现方式也有其弊端— 排序界面存在不尽如人意之处,分页例程的设计并不适用于对规模庞大的结果集进行有效地分页。在后续教程中,我们将会探讨如何克服这些自带的分页和排序解决方案的局限性。
步骤1 :添加分页和排序教程网页
在开始本教程之前,我们首先要添加本教程和后面三篇教程需要使用的ASP.NET 页面。首先,在项目中创建一个名为 PagingAndSorting 的新文件夹。然后,为该文件夹添加以下五个 ASP.NET 页面,并将这些页面的母版页配置为 Site.master :
- Default.aspx
- SimplePagingSorting.aspx
- EfficientPaging.aspx
- SortParameter.aspx
- CustomSortingUI.aspx
图1 :创建 PagingAndSorting 文件夹,添加 ASP.NET 教程页面
然后,打开 Default.aspx 页面,并将 UserControls 文件夹中的 SectionLevelTutorialListing.ascx 用户控件拖放到设计界面。我们在 母版页与网站导航 教程中创建的这个用户控件会遍历站点地图,并在当前区域以列表的形式显示相关教程。
图2 :向 Default.aspx 添加 SectionLevelTutorialListing.ascx 用户控件
要让项目符号列表显示我们将要创建的分页和排序教程,我们需要将这些教程添加到站点地图中。打开 Web.sitemap 文件,将以下标记添加到 “Editing, Inserting, and Deleting” 站点地图节点标记之后:
<siteMapNode title="Paging and Sorting" url="~/PagingAndSorting/Default.aspx"
description="Samples of Reports that Provide Paging and Sorting Capabilities">
<siteMapNode url="~/PagingAndSorting/SimplePagingSorting.aspx"
title="Simple Paging & Sorting Examples"
description="Examines how to add simple paging and sorting support." />
<siteMapNode url="~/PagingAndSorting/EfficientPaging.aspx"
title="Efficiently Paging Through Large Result Sets"
description="Learn how to efficiently page through large result sets." />
<siteMapNode url="~/PagingAndSorting/SortParameter.aspx"
title="Sorting Data at the BLL or DAL"
description="Illustrates how to perform sorting logic in the Business Logic
Layer or Data Access Layer." />
<siteMapNode url="~/PagingAndSorting/CustomSortingUI.aspx"
title="Customizing the Sorting User Interface"
description="Learn how to customize and improve the sorting user interface." />
</siteMapNode>
图3 :更新站点地图,使其包含新的 ASP.NET 页面
步骤2 :在GridView 中显示产品信息
在真正实现分页和排序功能之前,我们首先创建一个不具备排序和分页功能的标准GridView ,其中显示产品信息。这项任务我们已经做过很多次了,因此我们应当非常熟悉以下步骤。首先,打开 SimplePagingSorting.aspx 页面,将一个 GridView 控件从 Toolbox 拖放到 Designer ,并将其 ID 属性设置为 Products 。然后,新建一个 ObjectDataSource 控件,该控件使用ProductsBLL 类的 GetProducts() 方法来返回所有产品信息。
图4 :使用 GetProducts() 方法获取所有产品的信息
由于该报表为只读报表,因此不需要将 ObjectDataSource 的 Insert() 、Update() 或 Delete() 方法映射到相应的 ProductsBLL 方法;因此,在 UPDATE 、INSERT 和 DELETE 选项卡中,从下拉列表中选择 (None) 。
图5 :在 UPDATE 、INSERT 和 DELETE 选项卡中,从下拉列表中选择 (None) 选项
然后,定制 GridView 的字段,使其只显示产品的名称、供应商、类别、价格和 断货 状态。此外,我们可随意进行任何字段级的格式更改,例如调整 HeaderText 属性,或将价格格式化为一种货币形式。在进行完这些更改之后, GridView 的声明标记应如下所示:
<asp:GridView ID="Products" runat="server" 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" HeaderText="Price"
SortExpression="UnitPrice" DataFormatString="{0:C}"
HtmlEncode="False" />
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
SortExpression="Discontinued" />
</Columns>
</asp:GridView>
图6 显示了通过浏览器查看的效果。注意:该页面在一个屏幕上罗列出所有产品的信息,显示每个产品的名称、类别、供应商、价格和断货状态。
图6 :此处列出了所有产品
步骤3 :添加分页支持
在一个屏幕中列出所有 的产品会导致用户阅读数据时发生信息过载。为使结果更便于管理,我们可以将数据分割为小一些的数据页面,让用户在某一时间仅对一个页面上的数据进行分析。只需选中 GridView 智能标记中的 Enable Paging 复选框(此操作将 GridView 的 AllowPaging 属性 设置为真)即可实现。
图7 :选中 Enable Paging 复选框来添加分页支持
启用分页功能后会限制每页显示记录的数目,并在 GridView 中添加了分页界面。如图 7 所示,默认的分页界面是一系列页码,允许用户迅速地从一个页面导航到另一个页面。分页界面看起来应当比较熟悉,我们在前面的教程中为 DetailsView 和 FormView 控件添加分页支持时曾经见过该界面。
DetailsView 和 FormView 控件在每页都只显示一条记录。但是,GridView 可通过 PageSize 属性 来确定每页显示的记录数(该属性的默认值为 10 )。
GridView 、DetailsView 和 FormView 的分页界面可使用如下属性进行定制 :
- PagerStyle - 指出分页界面的样式信息,可指定 BackColor 、ForeColor 、CssClass 和 HorizontalAlign 等设置。
- PagerSettings - 包含大量可定制分页界面功能的属性 ;PageButtonCount 指示分页界面上显示的数字页码的最大数量(默认值为 10); Mode 属性指出分页界面的操作形式,可设置为:
- NextPrevious – 显示“下一页” 和 “上一页” 按钮,让用户逐页前进或回退。
- NextPreviousFirstLast – 除了“下一页” 和 “上一页” 按钮之外,还提供 “第一页” 和“最后一页” 按钮,让用户可以快速进入第一页或最后一页。
- Numeric – 显示一系列页数,让用户直接点击数字进入相应的页面。
- NumericFirstLast – 除了页数按钮外,还提供“第一页” 和“最后一页” 按钮,让用户可以快速进入第一页或最后一页;当分页界面上的所有页码都不能覆盖显示范围时,“第一页” 和“最后一页” 按钮才会显示。
此外,GridView 、DetailsView 和 FormView 都提供 PageIndex 和 PageCount 属性,分别指示当前呈现的页面和页面总数。PageIndex 属性从 0 开始编号,这意味着我们在浏览首页时,PageIndex 为0 。而 PageCount 从 1 开始记数,这意味着 PageIndex 的取值范围是 0 到 PageCount - 1 之间。
让我们花点时间来改进一下 GridView 分页界面的默认外观。为分页界面添加右对齐属性,并将背景颜色设置为浅灰色。我们不希望直接通过 GridView 的 PagerStyle 属性来设置这些属性,而是在 Styles.css 中创建一个名为 PagerRowStyle 的 CSS 类,然后通过 Theme 文件为 PagerStyle 的 CssClass 属性赋值。首先打开 Styles.css ,添加以下 CSS 类的定义:
.PagerRowStyle { text-align: right; }
然后,在 App_Themes 文件夹中,打开 DataWebControls 文件夹下的 GridView.skin 文件。正如我们在母版页与网站导航 教程中所述,Skin 文件可用于指定一个 Web 控件的默认属性值。因此,我们将 PagerStyle 的 CssClass 属性设置为 PagerRowStyle 。同样,让我们使用 NumericFirstLast 分页界面配置分页界面最多显示五个页码按钮。
<asp:GridView runat="server" CssClass="DataWebControlStyle">
<AlternatingRowStyle CssClass="AlternatingRowStyle" />
<RowStyle CssClass="RowStyle" />
<HeaderStyle CssClass="HeaderStyle" />
<FooterStyle CssClass="FooterStyle" />
<SelectedRowStyle CssClass="SelectedRowStyle" />
<PagerStyle CssClass="PagerRowStyle" />
<PagerSettings Mode="NumericFirstLast" PageButtonCount="5" />
</asp:GridView>
分页用户体验
我们选中了 GridView 的 Enable Paging 复选框,并通过 GridView.skin 文件进行了 PagerStyle 和 PagerSettings 的配置,之后,我们通过浏览器访问该网页。图 8 显示了该网页在浏览器中的呈现。注意,每页仅显示十条记录,分页界面指示我们当前浏览的是第一个页面。
图8 :启用分页功能后,每次只显示部分数据
当用户单击分页界面的页码时,页面回传并且加载,显示所请求页面的记录。图 9 显示了选择查看最后一页数据时的效果。注意,该页只有一条记录;这是因为总记录数为 81 条,因此生成 8 个每页具有 10 条记录的页面和 1 个具有 1 条记录的页面。
图9 :单击页码按钮会引发页面回传并显示一组相应的记录
分页的服务器端工作流
当终端用户单击分页界面上的按钮时,页面回传,以下服务器端工作流开始运行 :
- GridView (或 DetailsView 、FormView)的PageIndexChanging 事件触发。
- ObjectDataSource 再次从 BLL 请求所有数据;GridView 的 PageIndex 和 PageSize 属性值用来确定从 BLL 返回的哪些记录将在 GridView 中显示。
- GridView 的 PageIndexChanged 事件触发。
在步骤 2 中,ObjectDataSource 再次从其数据源请求所有数据。这种风格的分页通常被称为 默认分页,因为这是将 AllowPaging 属性设定为真后即应用的默认分页行为。应用默认的分页功能时,数据 Web 控件会单纯地为每个页面获取所有记录,但只有部分记录会呈现在发送给浏览器的 HTML 中。除非 BLL 或 ObjectDataSource 对数据库的数据进行缓存,否则对于大数据量的 结果集 或拥有大量并发用户的 web 应用程序,默认的分页功能无法有效运行。
在下一个教程中,我们将学习如何实现自定义分页。通过自定义分页,我们可以指示 ObjectDataSource 精确地获取用户请求页面所需的记录。可想而知,对于大数据量的结果集,自定义分页可以显著提升分页效率。
注意: 默认的分页方式不适合对大数据量的结果集或具有大量并发用户的网站进行分页。但要实现自定义分页需要进行很多更改和操作,不像默认分页方式那样仅需选中一个复选框。因此,默认分页对于小规模、低访问量的网站或数据量较小的系统是个理想的选择,因为它的实现确实非常简单、快捷。
例如,如果已知数据库中的产品数量不会超出100 ,那么自定义分页所带来的微小性能收益不足以抵消为实现它所付出的工作。但是,如果我们拥有成千上万种产品, 不 实现自定义分页将会极大地限制应用程序的可扩展性。
步骤4 :自定义分页体验
数据 Web 控件提供了大量的属性来提升用户的分页体验。例如, PageCount 属性指示总页面数,而 PageIndex 属性指示当前访问的页面,我们可通过设置该属性快速切换到指定的页面。为演示如何应用这些属性来提升用户的分页体验,我们在页面上添加一个 Label Web 控件(用于提示当前访问的页面)和一个 DropDownLis 控件(允许用户快速跳转到任意指定的页面)。
首先,在页面上添加一个Label Web 控件,将其 ID 属性设置为 PagingInformation ,并清空其 Text 属性。然后,为 GridView 的 DataBound 事件创建事件处理程序,并添加以下代码:
protected void Products_DataBound(object sender, EventArgs e)
{
PagingInformation.Text = string.Format("You are viewing page {0} of {1}...",
Products.PageIndex + 1, Products.PageCount);
}
事件处理程序将 PagingInformation Label 的 Text 属性赋值为一条消息,该消息提示用户当前访问的页面(Products.PageIndex + 1)(加 1 是因为 Products.PageIndex 属性值由 0 开始计数)和总页数(Products.PageCount )。选择在 DataBound 事件处理程序中对 Label 的 Text 属性赋值,而不是在 PageIndexChanged 事件处理程序中赋值的原因在于:DataBound 事件在每次将数据绑定到 GridView 时都会触发,而 PageIndexChanged 事件处理程序仅在页面索引发生改变时触发。当 GridView 最初与第一页的数据绑定时,PageIndexChanging 事件不会触发(而 DataBound 事件会触发)。
此时,用户会看到一条消息,提示当前访问的是哪个页面,以及页面的总数是多少。
图10 :显示当前页码和总页数
除了 Label 控件,我们再来添加一个 DropDownList 控件,该控件在 GridView 中列出页码,并选定当前查看的页面。其用意在于:仅通过从 DropDownList 中选择新的页面索引,用户就可以快速地从当前页面跳转到另一页面。首先,将 DropDownList 添加到 Designer ,将其 ID 属性设置为 PageList ,并在其智能标记中选中 Enable AutoPostBack 选项。
然后,返回至 DataBound 事件处理程序并添加如下代码:
// Clear out all of the items in the DropDownList
PageList.Items.Clear();
// Add a ListItem for each page
for (int i = 0; i < Products.PageCount; i++)
{
// Add the new ListItem
ListItem pageListItem = new ListItem(string.Concat("Page ", i + 1), i.ToString());
PageList.Items.Add(pageListItem);
// select the current item, if needed
if (i == Products.PageIndex)
pageListItem.Selected = true;
}
这段代码首先清除了 PageList DropDownList 的列表项。这个操作看起来有些多余,因为用户不会预料到页数的改变。但是同时在使用该系统的其他用户可能会对 Products 表进行记录的添加或删除操作。这些插入或删除操作能改变数据的页数。
然后,我们需要重新生成页数,默认情况下,选择与当前 GridView PageIndex 相对应的值。我们使用一个从 0 到 PageCount – 1 的循环来实现:在每次循环中,新增一个 ListItem ,当循环索引与 GridView 的 PageIndex 属性相同时,则设置其 Selected 属性为真。
最后,我们需要为 DropDownList 的 SelectedIndexChanged 事件创建事件处理程序,每次用户从列表中选择一个不同的选项时,该事件处理程序都会触发。要创建该事件处理程序,在 Designer 中双击 DropDownList ,而后添加以下代码:
protected void PageList_SelectedIndexChanged(object sender, EventArgs e)
{
// Jump to the specified page
Products.PageIndex = Convert.ToInt32(PageList.SelectedValue);
}
如图 11 所示,只改变 GridView 的 PageIndex 属性,就可以将数据重新绑定到 GridView 。在 GridView 的 DataBound 事件处理程序中,选择相应的 DropDownList ListItem 。
图11 :在下拉列表项中选择 "Page 6" ,则系统自动切换至第 6 页
步骤5 :添加双向排序支持
添加双向排序支持与添加分页支持一样简单:仅需在 GridView 的智能标记中选中 Enable Sorting 选项(该操作将 GridView 的AllowSorting 属性 设为真 )。这样,每个 GridView 字段的标题都显示为 LinkButtons ,单击后引发一次回传,所单击的列的所有数据会以升序排序。再次单击同一标题的 LinkButton ,该列的数据以降序重新排序。
注意:如果使用的是自定义的数据访问层而不是Typed DataSet ,则在 GridView 的智能标记上不会有 Enable Sorting 选项。只有绑定到特定数据源(本身支持排序)的 GridViews 才会提供该复选框。 Typed DataSet 提供现成的排序支持,因为 ADO.NET DataTable 提供了一种排序方法,在调用该方法时,系统会应用指定的标准对 DataTable 的 DataRows 排序。
如果数据访问层返回的对象不支持排序,则需要配置 ObjectDataSource 将排序信息传递到业务逻辑层,然后由业务逻辑层或数据访问层对数据排序。我们会在后面的教程中讨论如何在业务逻辑层和数据访问层对数据排序。
排序的 LinkButtons 以 HTML 超级链接的形式呈现,它的当前颜色(未访问过的链接为蓝色,访问过的链接为深红色)与标题行的背景颜色冲突。让我们设置所有标题行中的链接在访问过和未访问的情况下都为白色。将以下代码添加到 Styles.css 类中,即可实现上述设置:
.HeaderStyle a, .HeaderStyle a:visited { color: White; }
此语法指出:在显示使用 HeaderStyle 类的元素中的超级链接时,使用白色文本。
添加完这段 CSS 代码后,使用浏览器访问该页面时的屏幕应当与图12 类似。图 12 显示了单击 Price 字段的标题链接后的效果。
图12 : 结果根据 UnitPrice 以 升序排列
研究排序工作流
所有 GridView 字段:BoundField 、CheckBoxField 、TemplateField 等等都有 SortExpression 属性来指示特定的表达式,单击某字段的排序标题链接后,该字段的数据会应用上述表达式进行排序。GridView 也有一个 SortExpression 属性。当单击排序标题 LinkButton 后, GridView 将该字段的 SortExpression 值分配给 SortExpression 属性。然后,从 ObjectDataSource 重新读取数据并根据 GridView 的 SortExpression 属性排序。下面列出了一个终端用户在 GridView 中对数据排序的一系列步骤:
- GridView 的 Sorting 事件触发。
- 设置 GridView 的 SortExpression 属性为特定字段 ( 该字段的排序标题 LinkButton 被单击 ) 的 SortExpression 。
- ObjectDataSource 重新从 BLL 获取所有数据,然后根据 GridView 的 SortExpression 对数据排序。
- 将 GridView 的 PageIndex 属性重新设置为 0 , 这意味着 , 排序后将首页数据返回给用户 ( 假定实现了分页支持 ) 。
- GridView 的 Sorted 事件触发。
同默认分页类似,默认排序选项也将从 BLL 重新获取所有 的记录。如果在未分页或使用默认分页的情况下应用排序,则无法弥补性能损失(缓存数据库数据的情况除外)。但是,我们将在后续教程中发现,应用自定义分页时对数据进行高效排序是可行的。
在通过 GridView 智能标记中的下拉列表将ObjectDataSource 绑定到 GridView 时,每个 GridView 字段自动将其 SortExpression 属性分配给 ProductsRow 类中数据字段的名称。例如,ProductName BoundField 的 SortExpression 被设置为 ProductName ,声明标记如下所示:
<asp:BoundField DataField="ProductName" HeaderText="Product" SortExpression="ProductName" />
我们可以通过清空一个字段的 SortExpression 属性( 赋值为空字符串 )来禁止对它排序。为证明这一点,不妨假设我们不希望让客户根据价格来对我们的产品进行排序。那么,我们可以通过声明标记或通过 Fields 对话框(可在 GridView 的智能标记中单击 Edit Columns 链接来访问该对话框)清除 UnitPrice BoundField 的 SortExpression 属性。
图13 :结果根据 UnitPrice 以升序排列
一旦删除了 UnitPrice BoundField 的 SortExpression 属性,标题不再以链接方式(而是以文本方式)呈现,这样用户就无法根据价格进行排序了。
图14 :删除 SortExpression 属性后,用户无法再根据价格对产品进行排序
通过编码实现 GridView 排序
通过使用 GridView 的 Sort 方法 ,我们还可以编码方式实现对 GridView 内容的排序。只需传入排序所依据的 SortExpression 值和 SortDirection (升序或降序),GridView 的数据就被重新排序。
假设我们担心客户仅购买价格最低的产品,因此希望禁止根据 UnitPrice 排序。然而,由于我们想鼓励客户购买最昂贵的产品,所以又希望他们能够根据价格对产品排序,但只能从价格最高到最低进行排序。
要实现上述要求,我们向页面添加一个 Button Web 控件,将其 ID 属性设置为 SortPriceDescending ,Text 属性设置为 “Sort by Price” 。然后,在 Designer 中双击 Button 控件,为该控件的 Click 事件创建事件处理程序。将以下代码添加到事件处理程序中:
protected void SortPriceDescending_Click(object sender, EventArgs e)
{
// Sort by UnitPrice in descending order
Products.Sort("UnitPrice", SortDirection.Descending);
}
单击该按钮,返回到第一页,该页中的产品根据价格从最高到最低排序(参见图 15 )。
图15 :单击按钮后,产品从价格最高到最低排序
小结
在本教程中,我们了解了怎样实现默认的分页和排序功能,两个功能都仅需选中一个复选框来实现!当一个用户对数据进行排序或分页时,工作流程如以下所示:
- 页面回传
- 数据 Web 控件的 pre-level 事件触发(PageIndexChanging 或 Sorting ) 。
- ObjectDataSource 重新获取所有数据
- 数据 Web 控件的 post-level 事件触发(PageIndexChanged 或 Sorted ) 。
尽管基本的分页和排序功能可以轻松实现,但是要利用更高效的自定义分页或进一步提升分页或排序界面,我们则需要付出更多努力。后面的教程将会继续探讨相关主题。
快乐编程!