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

作者 :Scott Mitchell

下载 PDF

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

简介

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

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

明确标识每个类别组

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

本教程介绍如何创建此类排序用户界面。

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

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

接下来,配置 GridView,使其仅包含 ProductNameCategoryNameSupplierNameUnitPrice BoundField 以及已停用的 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 数据按类别排序 (单击以查看全尺寸图像)

步骤 2:探索添加分隔符行的技术

完成通用的可排序 GridView 后,剩下的就是能够在每个唯一排序组之前在 GridView 中添加分隔符行。 但是,如何将此类行注入到 GridView 中? 从本质上讲,我们需要循环访问 GridView s 行,确定排序列中值之间的差异发生的位置,然后添加相应的分隔符行。 在考虑此问题时,解决方案位于 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 博客文章“ 使用 GridView 排序分组玩位”中提供的示例。

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

由于我们只希望在最后一次访问该页面时创建并创建其控件层次结构之后,将分隔符行添加到 GridView 控件层次结构,因此我们希望在页面生命周期结束时,但在将实际的 GridView 控件层次结构呈现为 HTML 之前执行此添加。 实现此 Page 目的的最新可能点是 类事件 Render ,我们可以使用以下方法签名在代码隐藏类中重写该事件:

protected override void Render(HtmlTextWriter writer)
{
    // Add code to manipulate the GridView control hierarchy
    base.Render(writer);
}

Page调用类的原始Render方法base.Render(writer)时,页面中的每个控件都将呈现,并基于其控件层次结构生成标记。 因此,我们必须同时调用 base.Render(writer),以便呈现页面,并在调用 base.Render(writer)之前操作 GridView 控件层次结构,以便分隔符行在呈现之前已添加到 GridView 控件层次结构中。

若要注入排序组标头,我们首先需要确保用户已请求对数据进行排序。 默认情况下,GridView 的内容不进行排序,因此无需输入任何组排序标头。

注意

如果希望在首次加载页面时按特定列对 GridView 进行排序,请在第一页上调用 GridView s Sort 方法访问 (但不调用后续回发) 。 为此,请将此调用添加到条件内的 Page_Load 事件处理程序中 if (!Page.IsPostBack) 。 有关 方法的详细信息Sort,请参阅分页和排序报表数据教程信息。

假设数据已排序,下一个任务是确定数据排序依据的列,然后扫描行以查找该列值的差异。 以下代码确保已对数据进行排序,并查找排序数据所依据的列:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // Determine the index and HeaderText of the column that
        //the data is sorted by
        int sortColumnIndex = -1;
        string sortColumnHeaderText = string.Empty;
        for (int i = 0; i < ProductList.Columns.Count; i++)
        {
            if (ProductList.Columns[i].SortExpression.CompareTo(ProductList.SortExpression)
                == 0)
            {
                sortColumnIndex = i;
                sortColumnHeaderText = ProductList.Columns[i].HeaderText;
                break;
            }
        }
        // TODO: Scan the rows for differences in the sorted column�s values
}

如果尚未对 GridView 进行排序,则尚未设置 GridView 的 SortExpression 属性。 因此,如果此属性具有一些值,我们只想添加分隔符行。 如果存在,接下来需要确定对数据进行排序所依据的列的索引。 这是通过循环访问 GridView 集合来实现的 Columns ,搜索其 SortExpression 属性等于 GridView s 属性的 SortExpression 列。 除了列索引外,我们还获取 HeaderText 属性,该属性在显示分隔符行时使用。

使用数据排序依据的列的索引,最后一步是枚举 GridView 的行。 对于每一行,我们需要确定排序列的 值是否与上一行的排序列 s 值不同。 如果是这样,我们需要将新 GridViewRow 实例注入到控件层次结构中。 这是通过以下代码完成的:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // ... Code for finding the sorted column index removed for brevity ...
        // Reference the Table the GridView has been rendered into
        Table gridTable = (Table)ProductList.Controls[0];
        // Enumerate each TableRow, adding a sorting UI header if
        // the sorted value has changed
        string lastValue = string.Empty;
        foreach (GridViewRow gvr in ProductList.Rows)
        {
            string currentValue = gvr.Cells[sortColumnIndex].Text;
            if (lastValue.CompareTo(currentValue) != 0)
            {
                // there's been a change in value in the sorted column
                int rowIndex = gridTable.Rows.GetRowIndex(gvr);
                // Add a new sort header row
                GridViewRow sortRow = new GridViewRow(rowIndex, rowIndex,
                    DataControlRowType.DataRow, DataControlRowState.Normal);
                TableCell sortCell = 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;
            }
        }
    }
    base.Render(writer);
}

此代码首先以编程方式引用 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 属性为空字符串;相反,该值可通过位于 集合Controls中的 TableCell CheckBox Web 控件获得。

若要处理除 BoundFields 以外的字段类型,我们需要扩充代码,currentValue其中变量分配给 检查,以确保在 集合ControlsTableCell是否存在 CheckBox。 将此代码替换为以下内容,而不是使用 currentValue = gvr.Cells[sortColumnIndex].Text

string currentValue = string.Empty;
if (gvr.Cells[sortColumnIndex].Controls.Count > 0)
{
    if (gvr.Cells[sortColumnIndex].Controls[0] is CheckBox)
    {
        if (((CheckBox)gvr.Cells[sortColumnIndex].Controls[0]).Checked)
            currentValue = "Yes";
        else
            currentValue = "No";
    }
    // ... Add other checks here if using columns with other
    //      Web controls in them (Calendars, DropDownLists, etc.) ...
}
else
    currentValue = gvr.Cells[sortColumnIndex].Text;

此代码检查当前行的排序列 TableCell ,以确定集合中 Controls 是否存在任何控件。 如果有,并且第一个控件是 CheckBox,则 currentValue 变量设置为“是”或“否”,具体取决于 CheckBox 的 Checked 属性。 否则,该值取自 TableCell s Text 属性。 可以复制此逻辑来处理 GridView 中可能存在的任何 TemplateField 的排序。

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

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

图 7:排序 CheckBoxField (单击以查看全尺寸图像)

注意

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

总结

GridView 不包含许多用于自定义排序接口的内置选项。 但是,使用一些低级别代码,可以调整 GridView 控件层次结构以创建更自定义的接口。 在本教程中,我们了解了如何为可排序的 GridView 添加排序组分隔符行,以便更轻松地标识不同的组和这些组边界。 有关自定义排序接口的其他示例,检查 Scott Guthriea Few ASP.NET 2.0 GridView Sorting Tips and Tricks 博客文章。

编程愉快!

关于作者

Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。