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

作者 :斯科特·米切尔

下载 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 配置为支持排序,方法是选中 GridView 智能标记中的“启用排序”复选框(或者通过将其属性设置为 AllowSortingtrue)。 向 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 的行,确定排序列中的值之间发生差异的位置,然后添加相应的分隔符行。 在考虑此问题时,解决方案似乎自然而然地位于 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(因为此类值通常不存在)来将记录标记为分隔符行。

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

  1. 以编程方式检索要绑定到 GridView 的数据( ProductsDataTable 实例)
  2. 根据 GridView 和SortExpressionSortDirection属性对数据进行排序
  3. 遍历 ProductsRows 中的 ProductsDataTable,寻找排序列中的差异所在位置
  4. 在每个组边界处,将具有设置为 ProductsRowCategoryID 分隔符记录实例注入到 DataTable 中(或使用任何决定将记录标记为分隔符记录的指定)。
  5. 注入分隔符行后,以编程方式将数据绑定到 GridView

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

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

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

在将数据绑定到 GridView 之前,我们可以在数据绑定到 GridView 后添加分隔符行,而不是在将数据绑定到 GridView 之前 发送消息。 数据绑定过程构建 GridView 的控制层次结构,实际上只是由 Table 行集合组成的实例,每个实例都由单元格集合组成。 具体而言,GridView 控件集合包含Table根目录下的对象、GridViewRow绑定到 GridView 的每个记录(TableRow派生自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 的 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 的 SortExpression 属性的列。 除了列索引之外,我们还获取 HeaderText 属性,该属性在显示分隔符行时使用。

通过对数据进行排序的列的索引,最后一步是枚举 GridView 的行。 对于每一行,我们需要确定排序后的列值是否与上一行的排序后的列值有差异。 如果是这样,我们需要将新 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);
}

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

注释

若要确定特定行排序列的值,请使用单元格的属性 Text 。 这适用于 BoundFields,但不适用于 TemplateFields、CheckBoxFields 等。 我们将很快了解如何考虑备用 GridView 字段。

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

请注意,分隔符行的单个 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)。

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

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

对 CheckBoxField 进行排序时缺少排序组标头

图 6:排序复选框字段时缺少排序组标题(单击以查看全尺寸图像

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

为了处理除了 BoundFields 以外的字段类型,我们需要增强代码来检查 currentValue 集合中是否存在 CheckBox,然后再分配 TableCell 变量。 将此代码替换为以下内容,而不是使用 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 中可能存在的任何 TemplateFields 的排序。

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

排序组标题在对 CheckBoxField 进行排序时现在存在

图 7:排序复选框字段时,现在存在排序组标题(单击以查看全尺寸图像

注释

如果产品具有NULL数据库值CategoryIDSupplierID(或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 联系到他。