创建自定义的排序用户界面 (VB)

作者 :Scott Mitchell

下载 PDF

显示已排序数据的长列表时,通过引入分隔符行对相关数据进行分组非常有用。 在本教程中,我们将了解如何创建此类排序用户界面。

简介

当显示排序数据的长列表时,如果排序列中只有少量不同的值,最终用户可能会发现很难辨别差异边界的确切位置。 例如,数据库中有 81 个产品,但只有 9 个不同的类别选项 (8 个唯一类别外加 NULL 选项) 。 假设某个用户有兴趣检查“海鲜”类别下的产品。 在列出单个 GridView 中的所有 产品的页面中,用户可能决定最佳选择是按类别对结果进行排序,这将将所有海鲜产品组合在一起。 按类别排序后,用户需要搜索列表,查找海鲜分组产品的开始和结束位置。 由于结果按类别名称的字母顺序排序,因此查找海鲜产品并不困难,但仍需要仔细扫描网格中的项列表。

为了帮助突出显示已排序组之间的边界,许多网站都使用在此类组之间添加分隔符的用户界面。 使用类似于图 1 所示的分隔符,用户可以更快地找到特定组并识别其边界,以及确定数据中存在哪些不同的组。

已明确标识每个类别组

图 1:每个类别组 (单击查看全尺寸图像)

在本教程中,我们将了解如何创建此类排序用户界面。

步骤 1:创建标准、可排序的 GridView

在探索如何增强 GridView 以提供增强的排序界面之前,让我们先创建一个列出产品的标准、可排序的 GridView。 首先打开 CustomSortingUI.aspx 文件夹中的页面 PagingAndSorting 。 将 GridView 添加到页面,将其 ID 属性设置为 ProductList,并将其绑定到新的 ObjectDataSource。 将 ObjectDataSource 配置为使用 ProductsBLL 类方法 GetProducts() 选择记录。

接下来,配置 GridView,使其仅包含 ProductNameCategoryNameSupplierNameUnitPrice BoundFields 和已停用的 CheckBoxField。 最后,通过选中 GridView 智能标记 (中的“启用排序”复选框或将其 AllowSorting 属性设置为) ,将 GridView 配置为 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,当其数据按字母顺序按类别排序时。

可排序的 GridView 数据按类别排序

图 2:可排序的 GridView s 数据按类别排序 (单击以查看全尺寸图像)

步骤 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 重新绑定到由 组成的 ProductRowsGridView。 我们可能会通过将记录的 属性设置为 CategoryID (,将记录标记为 -1 分隔符行,因为此类值通常) 不存在。

若要利用此方法,需要执行以下步骤:

  1. 以编程方式检索要绑定到 GridView (ProductsDataTable 实例)
  2. 根据 GridView SortExpressionSortDirection 属性对数据进行排序
  3. 循环访问 ProductsRows 中的 ProductsDataTable,查找排序列的差异所在位置
  4. 在每个组边界处,将分隔符记录 ProductsRow 实例注入到 DataTable 中, CategoryID 该实例设置为 -1 (或决定将记录标记为分隔符记录的任何指定 )
  5. 注入分隔符行后,以编程方式将数据绑定到 GridView

除了这五个步骤,我们还需要为 GridView 事件 RowDataBound 提供事件处理程序。 在这里,我们检查每个DataRow行,并确定它是否是一个分隔符行,其CategoryID设置为 -1。 如果是这样,我们可能需要调整其格式或单元格中显示的文本 () 。

使用此技术注入排序组边界所需的工作量比上面概述的要多一些,因为还需要为 GridView 事件 Sorting 提供事件处理程序并跟踪 SortExpressionSortDirection 值。

在数据绑定后操作 GridView 控件集合

我们可以在数据绑定到 GridView 之后添加分隔符行,而不是在将数据绑定到 GridView 之前 发送消息。 数据绑定过程构建了 GridView 控件层次结构,实际上它只是一个 Table 由行集合组成的实例,其中每个行都由一组单元格组成。 具体而言,GridView 控件集合在其根目录中包含一个 Table 对象、一个 GridViewRow 派生自 TableRow 绑定到 GridView 中每条记录 DataSource) 类的 (,以及 TableCell 每个 GridViewRow 实例中每个数据字段的 DataSource对象。

若要在每个排序组之间添加分隔符行,我们可以在创建此控件层次结构后直接对其进行操作。 我们可以确信,在呈现页面时,GridView 控件层次结构是最后一次创建的。 因此,此方法替代 Page 类方法 Render ,此时 GridView 的最终控件层次结构将更新为包含所需的分隔符行。 图 4 演示了此过程。

另一种技术操作 GridView 控件层次结构

图 4:一种替代技术操作 GridView 控件层次结构 (单击以查看全尺寸图像)

在本教程中,我们将使用后一种方法来自定义排序用户体验。

注意

我在本教程中介绍的代码基于 Teemu Keiski 博客文章 Playing a Bit with GridView Sort Grouping 中提供的示例。

步骤 3:将分隔符行添加到 GridView 控件层次结构

由于我们只想在最后一次访问该页面时创建并创建 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调用base.Render(writer)类的原始Render方法时,将呈现页面中的每个控件,并基于其控件层次结构生成标记。 因此,我们必须同时调用 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 s 属性的列来实现的 SortExpression 。 除了列索引之外,我们还获取 HeaderText 属性,该属性在显示分隔符行时使用。

使用数据排序依据的列的索引,最后一步是枚举 GridView 的行。 对于每一行,我们需要确定排序列的值是否不同于上一行排序的列 s 值。 如果是这样,我们需要将新 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

此代码首先以编程方式引用 Table 在 GridView 控件层次结构的根目录中找到的对象,并创建名为 的 lastValue字符串变量。 lastValue 用于比较当前行的排序列值与上一行的值。 接下来,枚举 GridView 的 Rows 集合,并且对于每一行,排序列的值存储在 变量中 currentValue

注意

为了确定特定行排序列的值,我使用单元格 s Text 属性。 这适用于 BoundFields,但不适用于 TemplateFields、CheckBoxFields 等。 我们稍后将介绍如何考虑备用 GridView 字段。

currentValue然后比较 和 lastValue 变量。 如果它们不同,则需要向控件层次结构添加新的分隔符行。 这是通过确定 对象 Rows 集合中 TableGridViewRow索引,创建新的 GridViewRowTableCell 实例,然后将 和 GridViewRow 添加到TableCell控件层次结构来实现的。

请注意,分隔符行 lone TableCell 的格式设置为跨 GridView 的整个宽度,使用 SortHeaderRowStyle CSS 类设置格式,并具有其 Text 属性,使其显示排序组名称 ((如 Category ) )和组值 (如饮料 ) 。 最后, lastValue 更新为 的值 currentValue

用于设置排序组标题行 SortHeaderRowStyle 格式的 CSS 类需要在 文件中指定 Styles.css 。 随意使用任何吸引你的样式设置;我使用了以下内容:

.SortHeaderRowStyle
{
    background-color: #c00;
    text-align: left;
    font-weight: bold;
    color: White;
}

使用当前代码时,排序接口在按任何 BoundField 排序时添加排序组标题 (请参阅图 5,其中显示了按供应商) 排序时的屏幕截图。 但是,当按任何其他字段类型 ((如 CheckBoxField 或 TemplateField) )进行排序时,无法找到排序组标头, (请参阅图 6) 。

排序接口包括按 BoundFields 排序时的排序组标头

图 5:按 BoundFields 排序时,排序接口包括排序组标题 (单击以查看全尺寸图像)

排序 CheckBoxField 时缺少排序组标头

图 6:排序 CheckBoxField 时缺少排序组标题 (单击以查看全尺寸图像)

按 CheckBoxField 排序时缺少排序组标头的原因是,代码当前仅 TableCell 使用 s Text 属性来确定每一行的排序列的值。 对于 CheckBoxFields,s TableCellText 属性是一个空字符串;相反,该值通过位于 s Controls 集合中的 TableCell CheckBox Web 控件提供。

若要处理除 BoundFields 以外的字段类型,我们需要扩充currentValue将变量分配给 检查的代码,以证明集合ControlsTableCell是否存在 CheckBox。 请将此代码替换为以下内容,而不是使用 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 中可能存在的任何 TemplateField 的排序。

通过上述代码添加,按已停用的 CheckBoxField 进行排序时,现在会出现排序组标头 (请参阅图 7) 。

排序 CheckBoxField 时,排序组标题现在存在

图 7:排序 CheckBoxField 时,排序组标题现在存在 (单击以查看全尺寸图像)

注意

如果产品具有 NULL 、、 或 UnitPrice 字段的数据库值CategoryID,则默认情况下,这些值将在 GridView 中显示为空字符串,这意味着具有NULL值的产品的分隔符行文本将类似于 Category: (也就是说,类别:如类别SupplierID:饮料 ) 。 如果要在此处显示值,可以将 BoundFields NullDisplayText 属性 设置为要显示的文本,也可以在将 分配给 currentValue 分隔符行属性 Text 时在 Render 方法中添加条件语句。

总结

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 技术。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上mitchell@4GuysFromRolla.com联系他,也可以通过他的博客(可在 中找到http://ScottOnWriting.NET)。