创建自定义排序用户界面

本文档是 Visual C# 教程 (切换到 Visual Basic 教程)

当显示很长的排序数据列表时,可引入分隔行来将相关数据组成一组,这可能非常有用。本教程中,我们将了解怎样创建这样的排序用户界面。

« 前一篇教程  |  下一篇教程 »

简介

当显示很长的排序数据列表,而其中排序列只有为数为多的不同值时,用户可能很难识别不同值的边界出现的确切位置。例如,数据库中有 81 种产品,但是只有 9 种不同的分类选项( 8 种互不相同的分类和 NULL 选项)。设想有个用户对海鲜 (Seafood) 一类的产品感兴趣,想要查看这类产品。在通过一个 GridView 列出所有产品的页面上,用户可能认为最好的办法是将结果按分类进行排序,这会将所有海鲜产品分到一组。按分类排序之后,用户便在列表中搜寻,查找以海鲜分组的产品开始和结束的位置。由于结果按分类名称的字母顺序排序,因而查找海鲜产品并不困难,但仍需要在这个 grid 中仔细浏览各列表项。

为了突出显示各个排序组之间的边界,许多网站采用在不同组之间添加分隔行的用户界面。如图 1 所示,分隔行使用户能更快地找到特定的组,识别出该组的边界,以及确定数据中存在哪些不同的组。

图1 :可以清楚地识别每个类别组

本教程中,我们将了解怎样创建这样的排序用户界面。

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

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

然后,配置 GridView 使其只包含 ProductName 、 CategoryName 、 SupplierName 和 UnitPrice 这些 BoundField 以及 Discontinued CheckBoxField 。最后,通过选中 GridView 的智能标签中的 Enable Sorting 复选框(或者通过将其 AllowSorting 属性设置为 true )来配置 GridView 使其支持排序。完成对 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 的 RowDataBound 事件处理器来解决问题。如同我们在基于数据的自定义格式 教程中讨论的一样,当基于行的数据应用行级格式时通常使用此 event handler。但是,此处却不能使用 RowDataBound 事件处理器来解决问题,因为不能通过在这个事件处理器中编码来将行添加到 GridView 。实际上, GridView 的 Rows 集合是只读的。

要将额外的行添加到 GridView ,我们有三种方法可以选择:

  • 将这些元数据分隔行添加到绑定到 GridView 的实际数据中
  • 在 GridView 绑定数据之后 , 将额外的 TableRow 实例添加到 GridView 的控件集
  • 创建扩展 GridView 控件的自定义服务器控件 , 重写负责构造 GridView 的结构的那些方法

如果在许多网页上或在多个网站上需要此功能,创建自定义服务器控件是最好的方法。但采用这种方法要写相当多的代码并且要深入理解 GridView 的内部工作方式。因此,在此教程中我们不考虑选择该方法。

另两个方法— 向绑定到 GridView 的实际数据添加分隔行,以及在 GridView 绑定数据之后处理 GridView 的控件集,解决问题的方式不同,值得讨论。

向绑定到 GridView 的数据添加行

当 GridView 绑定到一个数据源时,它为该数据源返回的每个记录创建一个 GridViewRow 。因此,我们可以在将数据源绑定到 GridView 之前向数据源添加“分隔记录”,从而插入所需分隔行。图 3 描述了这个原理。

图3 :向数据源添加分隔行的方法

“分隔记录” 这个术语之所以加引号,是因为实际没有专门的分隔记录;但是,我们需要以某种方式来将数据源中的特定记录标记为一个分隔行而不是一个普通的数据行。在我们的例子中,我们将 ProductsDataTable 实例绑定到 GridView ,该实例由 ProductRows 组成。我们可以将一条记录的 CategoryID 属性设置为 -1 ,从而将该记录标记为“分隔行”(因为普通记录不存在这样的值)。

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

  1. 通过编码检索要绑定到 GridView 的数据(ProductsDataTable 实例 )。
  2. 根据 GridView 的 SortExpression 和 SortDirection 属性对数据进行排序。
  3. 遍历 ProductsDataTable 中的 ProductsRows , 查找排序列中出现不同的位置。
  4. 在每个组的边界,将一个“分隔记录” ProductsRow 实例插入到 DataTable 中,将“分隔记录” 的 CategoryID 设置为 -1 (或者用来将记录标志为 “分隔记录” 的任何其它标志)。
  5. 插入“分隔行” 之后,通过编码将数据绑定到 GridView 。

除了以上五个步骤之外,还需要为 GridView 的 RowDataBound 事件提供一个事件处理器。这里,我们检查每个 DataRow ,判断它是否是“分隔行”(其 CategoryID 设置为 -1 ),若是,我们可能想调整其格式或在单元格中显示的文本。

除了上面略述的工作外,用此方法来插入排序组的边界需要做更多一些的工作,因为还需要为 GridView 的 Sorting 事件提供一个事件处理器并跟踪 SortExpression 和 SortDirection 的值。

在 GridView 绑定数据之后处理 GridView 的控件集

我们可以在数据绑定到 GridView 之前处理数据来添加分隔行,取而代之,我们也可以在数据绑定到 GridView 之后添加分隔行。数据绑定的过程建立了 GridView 的控件层次结构,该层次结构实际上只是一个 Table 实例,该实例由一些行的集合组成,而每行又由一些单元格的集合组成。具体来说, GridView 的控件集在其根部包含一个 Table 对象、然后是绑定到 GridView 的 DataSource 中的每条记录的对应 GridViewRow(从 TableRow 类派生而来),以及每个 GridViewRow 实例中对应于 DataSource 中每个数据字段的 TableCell 对象。

我们可以在控件层次结构创建以后立即处理此控件层次结构,从而在每个排序组之间添加分隔行。可以确信在呈现页面时,最后一次创建了 GridView 的控件层次结构。因此,用这种方法时,我们重写 Page 类的 Render 方法,在这里更新 GridView 的最终的控件层次结构,使其包含所需的分隔行。图 4 描述了这个过程。

图4 :操作 GridView 的控件层次结构的一种替代方法

本教程中,我们使用后面这个方法来自定义排序用户体验。

注意:本教程中使用的代码是基于Teemu Keiski 的博客中的尝试一下 GridView 的“排序分组” 提供的示例。

步骤3 :在 GridView 的控件层次结构中添加分隔行

我们只想在 GridView 的控件层次结构创建之后,在页面被访问而最后一次创建该层次结构之时向该结构添加分隔行,因此我们想在页面生存周期的最后阶段但又在GridView 控件层次结构转换为HTML 之前进行此添加。 Page 类的 Render 事件是实现此目标的最后一个可能的位置,我们可以使用以下的方法签名在 Code-Behind 类中重写该事件:

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 属性来要确定特定行的排序列的值。这适合于 BoundField ,但不适合于 TemplateField 、CheckBoxField 等。我们将在后面了解如何解决其它 GridView 字段的问题。

然后对 currentValue 变量和 lastValue 变量进行比较。如果两个变量不同,就要将一个新的分隔行添加到控件层次结构中。为此,确定该 GridViewRow 在 Table 对象的 Rows 集合中的索引,创建新的 GridViewRow 实例和 TableCell 实例,然后将 TableCell 和 GridViewRow 添加到控件层次结构中。

注意,分隔行只有一个 TableCell ,这个单元格经格式化后占有 GridView 的整个宽度,格式化时使用了 SortHeaderRowStyle CSS 类,并且具有 Text 属性以便显示排序组名称(如 “Category” )和组的值(如 “Beverages” )。最后,将 lastValue 更改为 currentValue 的值。

用于格式化排序组标题行的 CSS 类,SortHeaderRowStyle ,需要在 Styles.css 文件中指定。您可以随意使用任何喜欢的样式设置;我使用的是以下设置:

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

有了当前的代码,当按任何 BoundField 排序时,排序界面会添加排序组标题(参见图 5 ,图中显示按供应商排序时的截屏)。但是,当按任何其它字段类型(如 CheckBoxField 或 TemplateField )排序时,则没有发现排序组标题(参见图 6 )。

图5 :按 BoundFields 排序时排序界面包含排序组标题

图6 :当对 CheckBoxField 排序时缺少排序组标题

按CheckBoxField 排序时看不见排序组标题的原因是,代码当前只使用 TableCell 的 Text 属性为每行确定排序列的值。而对于 CheckBoxField , TableCel l 的Text 属性是一个空字符串。但是,我们可以通过 TableCell 的 Controls 集合中包含的 CheckBox Web 控件来获得该值。

要处理非 BoundField 的字段类型,需要在为 currentValue 变量赋值的地方增加代码,检查 TableCell 的 Controls 集合中是否存在 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 ,则根据该 CheckBox 的 Checked 属性将 currentValue 变量设置为 “Yes” 或 “No” 。否则,从 TableCell 的 Text 属性取得该值。可以重复此逻辑以处理任何可能存在于 GridView 中的 TemplateField 的排序。

添加以上代码后,按 Discontinued CheckBoxField 排序,现在可看到排序组标题(参见图 7 )。

图7 :当对 CheckBoxField 排序时现在可以出现排序组标题

注意:如果数据库中有的产品的 CategoryID 、SupplierID 或 UnitPrice 字段的值为 NULL ,默认情况下,在 GridView 中这些值将显示为空字符串,意味着这些具有 NULL 值的产品的分隔行中的文本将显示为 “Category:” 。(即 “Category:” 之后没有名称,不是像 “Category:Beverages” 一样)。如果希望此处显示一个值,可以将 BoundField 的 NullDisplayText 属性 设置为想显示的文本,或者在将 currentValue 赋给分隔行的 Text 属性时在 Render 方法中添加条件语句。

小结

GridView 没有包括许多用来自定义排序界面的内置选项。但是,通过少量低层代码,可以调整 GridView 的控件层次结构以创建更高程度的自定义界面。本教程中,我们了解了怎样为可排序 GridView 添加排序组分隔行,从而使用户可以更容易地识别不同的组以及这些组的边界。有关自定义排序界面的其它示例,可以查看Scott Guthrie 的博客中的 ASP.NET 2.0 GridView 排序的几个技巧 一文。

快乐编程!

 

 

 

下一篇教程