作者 :斯科特·米切尔
显示一长串排序的数据时,通过引入分隔符行对相关数据进行分组非常有用。 本教程介绍如何创建此类排序用户界面。
介绍
在显示排序列中只有少量不同值的已排序数据的长列表时,最终用户可能会发现很难辨别发生差异边界的位置。 例如,数据库中有 81 个产品,但只有 9 个不同的类别选择(8 个唯一类别和 NULL
选项)。 请考虑有兴趣检查属于“海鲜”类别的产品的用户的情况。 从一个列出单个 GridView 中的所有 产品的页面中,用户可能会决定最佳选择是按类别对结果进行排序,这将将所有海鲜产品组合在一起。 按类别排序后,用户需要搜索列表,查找海鲜分组产品开始和结束的位置。 由于结果按类别名称按类别名称按字母顺序排序,查找海鲜产品并不困难,但它仍然需要仔细扫描网格中的项目列表。
为了帮助突出显示排序组之间的边界,许多网站都使用用户界面在此类组之间添加分隔符。 与图 1 中显示的分隔符一样,用户可以更快地查找特定组并识别其边界,并确定数据中存在的不同组。
图 1:已明确标识每个类别组(单击以查看全尺寸图像)
本教程介绍如何创建此类排序用户界面。
步骤 1:创建标准、可排序的 GridView
在探索如何扩充 GridView 以提供增强的排序接口之前,让我们先创建列出产品的标准可排序 GridView。 首先打开 CustomSortingUI.aspx
文件夹中的页面 PagingAndSorting
。 将 GridView 添加到页面,将其 ID
属性 ProductList
设置为 ,并将其绑定到新的 ObjectDataSource。 将 ObjectDataSource 配置为使用 ProductsBLL
类的 GetProducts()
方法来选择记录。
接下来,配置 GridView,使其仅包含 ProductName
、CategoryName
、SupplierName
和 UnitPrice
BoundFields 以及已停用的 CheckBoxField。 最后,将 GridView 配置为支持排序,方法是选中 GridView 智能标记中的“启用排序”复选框(或者通过将其属性设置为 AllowSorting
true
)。 向 CustomSortingUI.aspx
页面添加这些内容后,声明性标记应如下所示:
<asp:GridView ID="ProductList" runat="server" AllowSorting="True"
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" DataFormatString="{0:C}"
HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
SortExpression="Discontinued" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts"
TypeName="ProductsBLL"></asp:ObjectDataSource>
请稍加留意,查看我们迄今为止在浏览器中的进展。 图 2 显示的可排序 GridView 是通过按类别按字母顺序排序的。
图 2:可排序的 GridView 数据按类别排序(单击以查看全尺寸图像)
步骤 2:探索添加分隔行的技术
完成通用、可排序的 GridView 之后,现在只剩下在 GridView 中的每个唯一排序组之前添加分隔行。 但是,如何将此类行注入到 GridView 中? 从本质上讲,我们需要循环访问 GridView 的行,确定排序列中的值之间发生差异的位置,然后添加相应的分隔符行。 在考虑此问题时,解决方案似乎自然而然地位于 GridView 事件处理程序的 RowDataBound
某个位置。 正如我们在 自定义格式设置基于数据 教程中所述,此事件处理程序通常用于根据行数据进行行级格式化。 但是, RowDataBound
事件处理程序不是此处的解决方案,因为无法从此事件处理程序以编程方式将行添加到 GridView。 GridView 集合 Rows
实际上是只读的。
若要向 GridView 添加其他行,我们有三种选择:
- 将这些元数据分隔符行添加到绑定到 GridView 的实际数据
- 将 GridView 绑定到数据后,将其他
TableRow
实例添加到 GridView 控件集合 - 创建自定义服务器控件,用于扩展 GridView 控件并重写负责构造 GridView 结构的方法
如果许多网页或多个网站都需要此功能,则创建自定义服务器控件是最佳方法。 但是,这需要相当多的代码和对 GridView 内部工作的深度进行彻底的探索。 因此,我们不会考虑本教程的选项。
另外两个选项是将分隔符行添加到绑定到 GridView 的实际数据中,以及在数据绑定后操作 GridView 控件集合——它们以不同的方式来解决问题,值得讨论。
向绑定到 GridView 的数据添加行
当 GridView 绑定到数据源时,它将为数据源返回的每个记录创建一个 GridViewRow
。 因此,在将分隔符记录绑定到 GridView 之前,我们可以向数据源添加分隔符记录来注入所需的分隔符行。 图 3 说明了此概念。
图 3:一种技术涉及向数据源添加分隔符行
我在引号中使用术语分隔符记录,因为没有特殊的分隔符记录;相反,我们必须以某种方式标记数据源中的特定记录作为分隔符,而不是普通数据行。 对于我们的示例,我们将 ProductsDataTable
实例绑定到由 ProductRows
组成的 GridView。 我们可以通过将记录的属性CategoryID
设置为-1
(因为此类值通常不存在)来将记录标记为分隔符行。
若要利用此方法,我们需要执行以下步骤:
- 以编程方式检索要绑定到 GridView 的数据(
ProductsDataTable
实例) - 根据 GridView 和
SortExpression
SortDirection
属性对数据进行排序 - 遍历
ProductsRows
中的ProductsDataTable
,寻找排序列中的差异所在位置 - 在每个组边界处,将具有设置为
ProductsRow
的CategoryID
分隔符记录实例注入到 DataTable 中(或使用任何决定将记录标记为分隔符记录的指定)。 - 注入分隔符行后,以编程方式将数据绑定到 GridView
除了这五个步骤,我们还需要为 GridView 事件 RowDataBound
提供事件处理程序。 在这里,我们将检查每个 DataRow
,并确定它是否为分隔符行,其中的一个 CategoryID
设置为 -1
。 如果是这样,我们可能需要调整其格式或单元格中显示的文本。
使用此方法注入排序组边界需要比上面概述的工作多一点,因为还需要为 GridView Sorting
事件提供事件处理程序并跟踪 SortExpression
和 SortDirection
值。
在数据绑定之后操作 GridView 控件集合
在将数据绑定到 GridView 之前,我们可以在数据绑定到 GridView 后添加分隔符行,而不是在将数据绑定到 GridView 之前 发送消息。 数据绑定过程构建 GridView 的控制层次结构,实际上只是由 Table
行集合组成的实例,每个实例都由单元格集合组成。 具体而言,GridView 控件集合包含Table
根目录下的对象、GridViewRow
绑定到 GridView 的每个记录(TableRow
派生自DataSource
该类)的对象,以及TableCell
每个实例中每个GridViewRow
数据字段的对象DataSource
。
若要在每个排序组之间添加分隔符行,我们可以在创建控件层次结构后直接操作该层次结构。 我们可以确信,在页面呈现之前,GridView 的控件层次结构已经是最后一次被创建。 因此,此方法重写了 Page
类的 Render
方法,此时 GridView 的最终控件层次结构将被更新,以包括所需的分隔符行。 图 4 说明了此过程。
图 4:替代技术操作 GridView 的控件层次结构(单击以查看全尺寸图像)
在本教程中,我们将使用此后一种方法来自定义排序用户体验。
注释
本教程中展示的代码基于 Teemu Keiski 的博客文章 玩转 GridView 排序分组 中提供的示例。
步骤 3:将分隔符行添加到 GridView 的控制层次结构
由于我们只想将分隔符行添加到 GridView 控件层次结构,因为它的控制层次结构是在上次在该页访问时创建的,因此我们希望在页面生命周期结束时执行此添加,但在实际 GridView 控件层次结构呈现到 HTML 之前。 我们可实现此目的的最新可能点是 Page
类事件 Render
,可以使用以下方法签名在代码隐藏类中重写该事件:
Protected Overrides Sub Render(ByVal writer As HtmlTextWriter)
' Add code to manipulate the GridView control hierarchy
MyBase.Render(writer)
End Sub
当调用Page
类的原始Render
方法时,base.Render(writer)
页面中的每个控件都会被渲染,并基于其控件层次结构生成标记。 因此,必须调用 base.Render(writer)
这两个页面,以便呈现页面,并在调用 base.Render(writer)
之前作 GridView 控件层次结构,以便在呈现该控件层次结构之前将分隔符行添加到 GridView 控件层次结构中。
若要注入排序组标头,我们首先需要确保用户已请求对数据进行排序。 默认情况下,GridView 的内容不会排序,因此我们不需要输入任何组排序标头。
注释
如果希望在首次加载页面时按特定列对 GridView 进行排序,请在页面初次加载时调用 GridView 的 Sort
方法(但不在后续回发时调用)。 为此,请在Page_Load
事件处理程序中的if (!Page.IsPostBack)
条件中添加此调用。 有关方法的更多信息,请参阅Sort
教程。
假设数据已排序,我们的下一个任务是确定数据是按哪一列排序的,然后扫描行以查找该列的值差异。 以下代码确保已对数据进行排序,并查找数据已排序依据的列:
Protected Overrides Sub Render(ByVal writer As HtmlTextWriter)
' Only add the sorting UI if the GridView is sorted
If Not String.IsNullOrEmpty(ProductList.SortExpression) Then
' Determine the index and HeaderText of the column that
'the data is sorted by
Dim sortColumnIndex As Integer = -1
Dim sortColumnHeaderText As String = String.Empty
For i As Integer = 0 To ProductList.Columns.Count - 1
If ProductList.Columns(i).SortExpression.CompareTo( _
ProductList.SortExpression) = 0 Then
sortColumnIndex = i
sortColumnHeaderText = ProductList.Columns(i).HeaderText
Exit For
End If
Next
' TODO: Scan the rows for differences in the sorted column�s values
End Sub
如果 GridView 尚未排序,则不会设置 GridView 的属性 SortExpression
。 因此,仅当此属性具有一些值时,才需要添加分隔符行。 如果是这样,接下来我们需要确定用于排序数据的列的索引。 这是通过循环访问 GridView 的 Columns
集合来实现的,查找其 SortExpression
属性等于 GridView 的 SortExpression
属性的列。 除了列索引之外,我们还获取 HeaderText
属性,该属性在显示分隔符行时使用。
通过对数据进行排序的列的索引,最后一步是枚举 GridView 的行。 对于每一行,我们需要确定排序后的列值是否与上一行的排序后的列值有差异。 如果是这样,我们需要将新 GridViewRow
实例注入到控件层次结构中。 这是使用以下代码完成的:
Protected Overrides Sub Render(ByVal writer As HtmlTextWriter)
' Only add the sorting UI if the GridView is sorted
If Not String.IsNullOrEmpty(ProductList.SortExpression) Then
' ... Code for finding the sorted column index removed for brevity ...
' Reference the Table the GridView has been rendered into
Dim gridTable As Table = CType(ProductList.Controls(0), Table)
' Enumerate each TableRow, adding a sorting UI header if
' the sorted value has changed
Dim lastValue As String = String.Empty
For Each gvr As GridViewRow In ProductList.Rows
Dim currentValue As String = gvr.Cells(sortColumnIndex).Text
If lastValue.CompareTo(currentValue) <> 0 Then
' there's been a change in value in the sorted column
Dim rowIndex As Integer = gridTable.Rows.GetRowIndex(gvr)
' Add a new sort header row
Dim sortRow As New GridViewRow(rowIndex, rowIndex, _
DataControlRowType.DataRow, DataControlRowState.Normal)
Dim sortCell As New TableCell()
sortCell.ColumnSpan = ProductList.Columns.Count
sortCell.Text = String.Format("{0}: {1}", _
sortColumnHeaderText, currentValue)
sortCell.CssClass = "SortHeaderRowStyle"
' Add sortCell to sortRow, and sortRow to gridTable
sortRow.Cells.Add(sortCell)
gridTable.Controls.AddAt(rowIndex, sortRow)
' Update lastValue
lastValue = currentValue
End If
Next
End If
MyBase.Render(writer)
End Sub
此代码首先以编程方式引用 GridView 控件层次结构根目录中找到的 Table
对象,并创建一个名为 lastValue
的字符串变量。
lastValue
用于将当前行的排序列值与前一行的值进行比较。 接下来,枚举 GridView 集合 Rows
,并且对于每一行,将排序列的值存储到变量 currentValue
中。
注释
若要确定特定行排序列的值,请使用单元格的属性 Text
。 这适用于 BoundFields,但不适用于 TemplateFields、CheckBoxFields 等。 我们将很快了解如何考虑备用 GridView 字段。
然后将currentValue
变量和lastValue
变量进行比较。 如果它们不同,我们需要向控件层次结构添加新的分隔符行。 通过确定GridViewRow
在Table
集合中的Rows
索引、创建新GridViewRow
和TableCell
实例,然后将TableCell
和GridViewRow
添加到控件层次结构来实现。
请注意,分隔符行的单个 TableCell
被格式化以跨越整个 GridView 的宽度,使用 SortHeaderRowStyle
CSS 类并设置 Text
属性,以便同时显示排序组名称(如种类)和组值(如饮料)。 最后, lastValue
更新为值 currentValue
。
需要在文件中指定用于设置排序组标题行 SortHeaderRowStyle
格式的 Styles.css
CSS 类。 请随意使用任何你喜欢的样式设置;我使用了以下设置:
.SortHeaderRowStyle
{
background-color: #c00;
text-align: left;
font-weight: bold;
color: White;
}
使用当前代码时,排序接口会在按任何 BoundField 排序时添加排序组标头(请参阅图 5,其中显示了按供应商排序时的屏幕截图)。 但是,当按任何其他字段类型(如 CheckBoxField 或 TemplateField)排序时,排序组标题将无处可找到(请参阅图 6)。
图 5:排序接口在按 BoundFields 排序时包括排序组标题(单击以查看全尺寸图像)
图 6:排序复选框字段时缺少排序组标题(单击以查看全尺寸图像)
按 CheckBoxField 排序时缺少排序组标头的原因是代码当前只使用 TableCell
属性来确定每行已排序列的值。 对于 CheckBoxFields,TableCell
s Text
属性是空字符串;相反,该值可通过位于 TableCell
s Controls
集合中的 CheckBox Web 控件获得。
为了处理除了 BoundFields 以外的字段类型,我们需要增强代码来检查 currentValue
集合中是否存在 CheckBox,然后再分配 TableCell
变量。 将此代码替换为以下内容,而不是使用 currentValue = gvr.Cells(sortColumnIndex).Text
:
Dim currentValue As String = String.Empty
If gvr.Cells(sortColumnIndex).Controls.Count > 0 Then
If TypeOf gvr.Cells(sortColumnIndex).Controls(0) Is CheckBox Then
If CType(gvr.Cells(sortColumnIndex).Controls(0), CheckBox).Checked Then
currentValue = "Yes"
Else
currentValue = "No"
End If
' ... Add other checks here if using columns with other
' Web controls in them (Calendars, DropDownLists, etc.) ...
End If
Else
currentValue = gvr.Cells(sortColumnIndex).Text
End If
此代码检查当前行的排序列 TableCell
,以确定集合中 Controls
是否有任何控件。 如果有,并且第一个控件是 CheckBox,则 currentValue
变量将设置为“是”或“否”,具体取决于 CheckBox 属性 Checked
。 否则,该值取自 TableCell
s Text
属性。 可以复制此逻辑来处理 GridView 中可能存在的任何 TemplateFields 的排序。
添加上述代码后,按“已停止的 CheckBoxField”排序时,排序组标头现在存在(请参阅图 7)。
图 7:排序复选框字段时,现在存在排序组标题(单击以查看全尺寸图像)
注释
如果产品具有NULL
数据库值CategoryID
SupplierID
(或UnitPrice
字段),则默认情况下,这些值将在 GridView 中显示为空字符串,这意味着具有NULL
值的这些产品的分隔符行文本将如下所示:(即,类别后没有名称:如类别:饮料)。 如果要在此处显示一个值,可以将 BoundFields NullDisplayText
属性 设置为要显示的文本,也可以在 Render 方法中添加条件语句,在分配给 currentValue
分隔符行的属性 Text
时添加条件语句。
概要
GridView 不包含许多用于自定义排序接口的内置选项。 但是,使用一些低级别代码,可以调整 GridView 控件层次结构以创建更自定义的接口。 本教程介绍了如何为可排序的 GridView 添加排序组分隔符行,以便更轻松地标识不同的组和这些组边界。 有关自定义排序接口的其他示例,请查看 Scott Guthrie s A Few ASP.NET 2.0 GridView Sorting Tips and Tricks 博客文章。
快乐编程!
关于作者
斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。