Cutting Edge:使用客户端行为扩展ASP.NET DataGrid 控件

(可拖动列和客户端排序)

英文原文:Extend the ASP.NET DataGrid with Client-side Behaviors
作者:Dino Esposito
翻译:MasterLRC
源码:CuttingEdge0401.exe

如同比萨饼厨师的擀面杖,DataGrid 控件,对于一个熟练的ASP.NET开发者来说是非常基本而且有用的工具(译者:老外的比喻,感觉好奇怪)。 虽然,在 ASP.NET 1.x 中,DataGrid已经是非常强大而且用途广泛的控件和服务器工具。 但是,我们依然可以通过给它加一点客户端脚本,来使它的功能变得更加强大。 最近,我看到了Dave Massy在几年前为 MSDN Online 的“DHTML Dude”栏目写的一些精彩内容。 Dave 论述了一些使 HTML的 <Table> 元素功能强大的创造性的方法,其中之一就是如何对table的内容进行排序和在Table中拖动列。

他还演示了<Table>元素的的DHTMLBehavior的用法。我意识到,当DataGrid控件在浏览器上呈现为 HTML 时,它完全就是一个<Table>元素; 虽然它可能包含了许多样式属性,但它的基本结构依然是一个典型的 HTML Table。这使我意识到我可以创建带客户端排序和可拖动列功能的 DataGrid 控件。 这就是我们这个月的栏目内容,你可以下载源代码来验证我所说并非虚言。

  • DHTML Behavior快速指南

    DHTML Behavior在我们的扩展的 DataGrid 控件中扮演非常重要的角色。 一会儿你就会注意到:我并没有使用Dave在他的原始form中使用的方法,为了使 behavior 在 ASP.NET 控件可以工作,我做了一点更改。 虽然使用这个修改后的组件不需要任何 Javascript 技能,但是了解一下 DHTML Behavior的技术可以使你更好的理解服务器端和客户端协同工作的机制。

    DHTML Behavior 就是利用 CSS(cascading style sheet层叠样式表)的 behavior 样式,绑定到一个 HTML 标记的脚本组件 对于那些不支持CSS或不识别 behavior 样式的老浏览器中,将自动怱略未识别的样式。要想深入了解 DHTML 请参见: Scripting Evolves to a More Powerful Technology: HTML Behaviors in Depth 一个 DHTML behavior 就是一个 JavaScrip 函数集,这个函数集中加入了一些由特殊句法定义的公共成员。一般来讲,这些公共成员是一些属性和事件,有时也可能是方法;Behavior 工作在现有的 HTML 元素之上, 允许你覆盖和扩展 HTML 元素的 behavior 。方法是:behavior 把它自己定义的代码关联到 DHTML 的事件上。例如,提供拖动列功能的 behavior 操作 onmousedown 和 onmouseup 事件。而且,所有关键的 DHTML behavior 都支持 oncontentready 事件,当 HTML subtree(在指定元素内的所有 HTML)被解析完成时将激发这一事件。当 oncontentreadey 事件激发时,是初始化 behavior 的好时机。

    其实,behavior 的核心就是向 Microsoft Internet Explorer(4.0或更高版本)浏览器暴露一系列接口的 COM 对象。最终就是你可以把它写成 C++ 元组件或 HTML 组件(HTC)文本文件 HTC文件可以布署在承载应用它们的文件(HTML,ASP,ASP.NET)的服务器上,不需要在客户端安装。

    下面代码表示如何通过使用 “dragdrop.htc”behavior 添加一个具有可拖动列功能的<table&gl;标签:

    <TABLE style="behavior:url(dragdrop.htc);">
    这里“dragdrop.htc”文件必须和使用它的文件布署于同一目录。

     

  • 可拖动的 DataGrid

    在阅读了 Dave Massy 的文章之后,我下载了 dragdrop.htc 示例组件,然后我尝试在一个实验页面中把它绑定到 DataGrid 组件。如下:

    theGrid.Style["behavior"] = "url(dragdrop.htc)";
    奇怪的是,组件没起作用。考虑到 DataGrid 在客户端完全是一个 Table 这一不争事实,我决定比较一下 Dave 的示例 Table 的代码和由 ASP.NET DataGrid 生成的 HTML 代码有什么不同。我注意到:在 ASP.NET 1.x 中的 DataGrid 生成的 HTML Table 不包含 THEAD 和 TBODY 元素。而这两个元素恰恰是示例 behavior 起作用的关键因素。虽然拖动和下压列并不直接需要 THEAD 和 TBODY 两个元素,但是这两个元素 非常有助于定位 table 的表头和表体。

     

    有两个办法解决这一问题:重写一个不使用 THEAD 和 TBODY 的 behavior ;自己写一个生成带 THEAD 和 TBODY 标签的 Table 的 DataGrid 控件。 对于一个象我这样的 ASP.NET 开发者来说,我相信开发一个自定义控件比编写一个 behavior 要容易。因为至少我们可以进行有效的进行代码根踪调试。 于是,我新建一个 Visual Studio .NET 解决方案,添加一个 ASP.NET 应用程序工程和一个 Web 控件库工程。就有了下面的新 DataGrid 控件的雏形:

     [ToolboxData("<{0}:DataGrid runat=\"server\" />")]
    public class DataGrid : System.Web.UI.WebControls.DataGrid
    {
    public DataGrid() : base()
    {
    EnableColumnDrag = true;
    DragColor = Color.Empty;
    HitColor = Color.Empty;
    }
    ...
    }
    

     

    构造器初始化三个公共自定义属性:EnableColumnDrag、DragColor和HitColor。EnableColumnDrag 是一个表示是否可以拖动列的布尔属性。 如果此属性设置成 False ,自定义 DataGrid 控件将不会添加拖动列 behavior。另外两个属性分别表示被拖动的列的背景色和将下压列的颜色。

    注意这两个颜色属性不会影响 DataGrid 服务器控件的任何逻辑。他们是那种仅输出 HTML 值(这个值只对客户端 behavior 有用)的服务器端属性。 这两个属性的值被呈现为由 grid 的生成的 <table> 标签的自定义属性。DataGrid 的标记代码是在控件的 Render 方法中创建的,如下:

     protected override void Render(HtmlTextWriter output)
    {
    // Sets attributes for the DragDrop behavior
    if (EnableColumnDrag)
    {
    if (DragColor != Color.Empty)
    Attributes["DragColor"] = DragColor.Name.ToLower();
    if (HitColor != Color.Empty)
    Attributes["HitColor"] =
    HitColor.Name.ToLower(); // Capture
    the default
    output
    of the DataGrid StringWriter
    writer =
    new StringWriter(); HtmlTextWriter
    buffer =
    new  HtmlTextWriter(writer); base.Render(buffer);
    string gridMarkup =
    writer.ToString();
    //Parse the markup to insert missing tags
    //Find the first occurrence of >  and insert
    int insertPoint;
    insertPoint = gridMarkup.IndexOf(">")+ 1;
    // len of >
    gridMarkup = gridMarkup.Insert(insertPoint, "     ");
    insertPoint = gridMarkup.IndexOf("</tr>") + 5;
    // len of </tr>
    gridMarkup = gridMarkup.Insert(insertPoint, "");
    // Replace with </tbody></table>
    gridMarkup = gridMarkup.Replace("</table>", "</tbody></table>">;
    // Output the markup
    output.Write(gridMarkup);
    return;
    }
    base.Render(output);
    return;
    }
    

     

    被添加到 DataGrid 属性集合的这两个 behavior 属性将呈现为下面的代码片断:

     <table dragcolor="..." hitcolor="..." ... ID="Table1">
    
    Render 方法生成带有 THEAD 和 TBODY 标签的 <table>元素。要实现这一功能只有一个方法,那就是获取默认的 HTML 然后对它进行解析。 你可以使用下面的代码获取某个控件生成的 HTML 代码:
     StringWriter writer = new StringWriter(); HtmlTextWriter buffer = new
    HtmlTextWriter(writer); base.Render(buffer); string gridMarkup =
    writer.ToString();
    

     

    创建一个 HtmlTextWriter 对象,并把它绑定到一个提供写出层面(writing surface:不知应该怎么译)的“写”对象。 writing surface 是以内存块的形式存在的——StringWriter 对象。发送到 HTML 写出器(HTML writer)的所有内容都累计在字符串写出器(string writer)中。 Render 方法的基方法生成 DataGrid 的基本标记,然后我们获取它,并使用 StringWriter 的 ToString 方法把它写到一个字符串中。简单而有效! 基于此,解析字符串并添加 THEAD 和 TBODY 标记就是小菜一碟了。用 THEAD 标签括住 table 的第一行,一般来讲第一行包括每一列的标题。TBODY 标识 表体:

     <table>
    <thead>
    <tr><td>Column 1</td><td>Column 2</td></tr>
    </thead>
    <tbody>
    <tr><td>Some</td><td>Data</td></tr>
    <tr><td>More</td><td>Info</td></tr>
    </tbody>
    </table>
    

     

    最后,把修改过的标记写入响应流(response stream)。接下来就可以准备进行测试了。将源代码编译成程序集,并注册到 Visual Studio.NET 的工具栏。 然后,把控件添加到测试页面。以上操作在 .aspx 页面中生成下列代码:

     <%@ Register TagPrefix="msdn" Namespace="Expoware.Controls" Assembly="MyCtl" %>
    
    除了命名空间前缀和根据需要添加的自定义属性之外,使用自定义的 DataGrid 控件和使用原 DataGrid 控件没什么不同:
     <msdn:datagrid runat="server" id="grid1" ...>
    ...
    </msdn:datagrid>
    

    图2:拖动列

     

    图2是操作中的测试页面。如果你在列头按下鼠标并拖动,一个表示正被移动的列的半透明的区域将跟随鼠标移动。 为改进用户体验,在鼠标下面的列将改变列头的背景色,来表示可以在这一列放开鼠标。 当你放开鼠标时,透明列就被插入到处天鼠标光标下的列的之前(左边)。最后 table 将以新的列顺序进行重新构造。 所有这些操作都在客户端通过 DHTML 对象模型完成,不需要往返服务器端。鼠标事件和 table 的重建完全由 DHTML behavior 控制。

  • DragDrop behavior 的增强

    图3给出了这个 behavior 代码的伪代码。

    <public:property name='DragColor' />
    <public:property name='HitColor' />
    <public:attach event=oncontentready onevent="init();" />
    <script>
    // Declare some global variables
    // Handles the OnContentReady event for the attached element (<table>)
    function init()
    {
    // 存储 THEAD 和 TBODY 的相关内容
    // 这时给全局变量赋值
    // 为点击测试创建与单元格相同的数组
    // 为每个单元格关联一个 OnMouseDown 事件的处理器
    // 添加 OnMouseMove 和 OnMouseUp 事件的处理器
    }
    function onMouseDown(e)
    {
    // 获取最外面的 TD 标签,
    // 以便达到无论在哪个单元格点击都可以开始拖动
    // 创建在拖动操作期间用于回馈的 panel
    // 把这个 panel 设置成透明,并使它的高度和宽度都和实际列相同,
    // panel 开始是隐藏的,它支持绝对位置,并具有虚线边框
    // 在鼠标点击完成之前插入这个 panel
    }
    function onMouseMove(e)
    {
    // 保存鼠标的当前位置
    // 打开 panel 的 visibility ,使它可见
    // 指出下层的单元格,并刷新它的虚设置
    // 停止事件冒泡
    }
    function onMouseUp(e)
    {
    // 指示被挤开的目标单元格
    // 从 DHTML 树中删除动态创建的 panel
    // 根据点击测试更新单元格对应的数组
    // 重建 table 的表头和表体
    }
    // 其它辅助函数
    </script>
    
    图3:DragDrop 的伪代码
    它基于对一些事件的处理。oncontentready 事件是入口点。这一事件的处理程序要初始化 组件状态并访问在关联元素中定义的公共属性。详细说就是:dragdrop.htc 这个 behavior 创建一个和单元格对应的数组,用于将来在拖放过程中检测划过的列。

     

    单元格的宽度是从 DHTML 对象的 ClientWidth 属性获取的。这是我对来自 Dave 的文章的 源码的小小改动之一。ClientWidth 属性获取 DHTML 对象包含 padding 但不包含 margin , border 或滚动条的宽度。使用这个属性是一个关键的改进,因为这样我们就可以支持没有在 HTML 源中明确指出宽度值的列了。由于我们要把这个 behavior 应用到我们的自定义 DataGrid 控件中,ClientWidth 属性的使用,使我们从根本上完全支持 DataGrid 控件的 AutoGenerateColumns (自动生成列)模式。

    本文所附的源码中的 dragdrop.htc 文件还做了其它一些小幅修改,以更适合单独参照。其中之一就是 控件在执行拖放时回馈显示的样式。

    当设置了 EnableColumnDrag 属性时,自定义 DataGrid 控件设置 behavior 样式。属性的值存储在 视图状态中。如果它是值为 True,behavior 属性就被添加到 DataGrid 控件的 Style 对象。注意对于同一个 元素可以绑定多个 behavior

    EnableColumnDrag 属性和控制 grid 是否显示表头的布尔型属性 ShowHeader 密切相关。当 DataGrid 控件不显示 表头时,自然就不能进行列拖放了。图4列出了这两个属性的 get 和 set 访问器。注意,我们必须重写 ShowHeader 属性的 set 访问器,以保证当 ShowHeader 属性为 False 时 EnableColumnDrag 属性也被设置为 False

    public bool EnableColumnDrag
    {
    get {return Convert.ToBoolean(ViewState["EnableColumnDrag"]);}
    set {
    if (!ShowHeader)
    {
    ViewState["EnableColumnDrag"] = false;
    return;
    }
    ViewState["EnableColumnDrag"] = value;
    string behavior = Style["behavior"];
    if (behavior == null)
    behavior = "";
    behavior = behavior.Replace("url(dragdrop.htc)", "");
    if (value)
    behavior += " url(dragdrop.htc)";
    Style["behavior"] = behavior;
    }
    }
    public override bool ShowHeader
    {
    get {return base.ShowHeader;}
    set {
    base.ShowHeader = value;
    if (!ShowHeader)
    EnableColumnDrag = false;
    }
    }
    
    图4:Get 和 Set

     

    目前为止,一切顺利。现在,在 DataGrid 的客户端显示上你就可以拖放列了。但是,一旦 DataGrid 或页面发生了回传, 页面上就又是原始的列顺序了。用什么办法可以保持新的列顺序呢?

  • 保持新的列顺序

    当页面回发时,新的列顺序必须作为一段信息发送到服务器端。在 ASP.NET 编程模型中在客户端和服务器端 交换数据没什么特殊方法。不论是文本框的文本,或是下拉列表的选择项,还是列顺序,它们被回传到服务器的方式是一样的。 你把所需的列顺序回传到服务器,让 DataGrid 控件按指定的顺序呈现列就OK了。让我们先看一下信息交换。

    当前,使用 HTTP 和 HTML 只有和种方式将信息从客户端发送到服务器端——隐藏域。DataGrid 控件在页面上添加一个隐藏域; DHTML behavior 算出列的排列顺序并写到隐藏域中。当页面回传时,DataGrid 从隐藏域中获取要求的顺序,并依此进行呈现。 你可以为隐藏域取任何名称,但必须是唯一的。我们还要负责获取回传的值。所要求的列顺序可以表示成用逗号分隔的字符串, 这些字符串就是每个列的标题。你不一定非得这样,但不管怎样,你得返回一个字符串。使用 Page.Request 获取客户端的值:

    string desiredOrder = Page.Request[HiddenFieldName].ToString();
    

     

    问题解决了,现在可以保持拖放后的列顺序了。虽然功能正确,但是在 ASP.NET 中这不是最好的方法。 你曾经用过 IPostBackDataHandler 接口吗?当你的控件要从客户端引入数据时这个接口可以派上用场。 图5就是我们的自定义 DataGrid 控件对 IPostBackDataHandler 接口的实现。 基本上,你可以通过这个接口实现隐藏域和控件的一个或多个属性之间的数据绑定。 你不用自己调用 Page.Request,也不用管隐藏域。ASP.NET 将为你打理一切。

    public class DataGrid : System.Web.UI.WebControls.DataGrid, IPostBackDataHandler
    {
    public DataGrid() : base()
    {
    EnableColumnDrag = true;
    DragColor = Color.Empty;
    HitColor = Color.Empty;
    ColumnOrder = "";
    }
    // *************************************************************
    // IPostBackDataHandler::LoadPostData
    // 自动根据和此控件具有相同 name 的隐藏域的内容自动更新 ColumnOrder 属性
    public virtual bool LoadPostData(string key, NameValueCollection
    values)
    {
    string currentColumnOrder = ColumnOrder;
    string postedColumnOrder = Convert.ToString(values[key]);
    // 如果为空则放弃新值
    if (postedColumnOrder == "")
    return false;
    // 新值已经提交了吗(提交的是新的顺序吗)?
    if (!currentColumnOrder.Equals(postedColumnOrder))
    {
    ColumnOrder = postedColumnOrder;
    return true;
    }
    return false;
    }
    // *************************************************************
    // *************************************************************
    // IPostBackDataHandler::RaisePostDataChangedEvent
    public virtual void RaisePostDataChangedEvent()
    {
    // 我们不需要激发服务器端事件,所以这里什么都不做
    }
    // *************************************************************
    // *************************************************************
    // 返回隐藏域的 name
    private string HiddenFieldForDragging
    {
    get {return ID;}
    }
    // *************************************************************
    // *************************************************************
    // 保存表示列顺序的逗号分隔的字符串
    private string ColumnOrder
    {
    get {return Convert.ToString(ViewState["ColumnOrder"]);}
    set {ViewState["ColumnOrder"] = value;}
    }
    // *************************************************************
    ...
    }
    
    图5:实现 IPostBackDataHandler 接口

     

    这个接口有两个方法,我们只需要使用其中的一个:LoadPostData,这个方法将传递一个键和一个回传值集合。 键就是控件的 ID,集合是 Page.Request 的一个子集。这个集合包含了与页面中控件匹配的所有 input 域的回传值。 仅有一种情况集合不匹配 Page.Request——当页面中有动态创建的控件时。

    LoadPostData 方法在 OnInit事件之后OnLoad 事件之前被调用。如果控件和一个 Input 域的 ID 匹配, 并且控件实现了 IPostBackDataHandler 接口,ASP.NET 就调用控件的 LoadPostData 方法。 因此我们必须给 input 域一个和 DataGrid 相同的 ID 。由于 DataGrid 控件仅在回传数据时工作, 并且不需要从服务器端向客户端发送数据,所以我们可以创建一个空字符串的隐藏域,如下:

    Page.RegisterHiddenField(ID, "");
    

     

    LoadPostData 方法更新 ColumnOrder 这个新属性的值。这个属性用于确定 grid 的列顺序。尤其要注意的是: 如果在两次连续回传发生中间没有进行拖动列操作,那么 ColumnOrder 属性为空。要想保持上一次的列顺序, 你必须保持 ColumnOrder 属性的值——例如,可以使用视图状态。另外,绝对不能使用回传回来的空值来设置 ColumnOrder 属性。

    IPostBackDataHandler 还有一个方法,这个方法可以使你有机会在回传数据影响了控件的状态时激发一个事件。 从客户端获取表示列顺序的字符串,这仅完成了一半任务。现在你还得让 DataGrid 把列呈现为正确的顺序。

  • DataGrid 私语者

    你现在是不是想爬在 DataGrid 的耳边悄悄告诉它呈现的事呢?有一次一个朋友叫我“the DataGrid whisperer.”(“DataGrid 私语者”)。 他的意思是说我可以随心所欲的让 DataGrid 干这干那,做任何事。然而,我必须坦白的说: 要想让 DataGrid 改变列的顺序,即使对一个有非常经验的人来说,也是一件非常困难的事件。

    我花费了几个小时尝试在 OnLoad 处理中重新排列 Columns 集合的顺序。最后我发现,当自动生成列的时候这个集合是空的。 而且当控件呈现它的 HTML 时,将丢失任何进入的更改。 就在我要放弃的时候,我想到了 CreateColumnSet 这个保护型的虚方法: protected virtual ArrayList CreateColumnSet( PagedDataSource dataSource, bool useDataSource );

    
    

     

    这个主方法被看作 Microsoft .NET 框架的一个空闲预留部分,因为它并不被考虑在用户代码中使用。 个人认为,我们所说的 protected virtual(overidable)方法,几乎不可能不被使用。 所以我决定得写这个方法。我并不知道这个方法实际上是做什么的,但是一个叫作 CreatColumnSet 的方法除了在 DataGrid 中创建列集这外,还能干什么呢? 我的第一个方法很小心:

    protected override ArrayList CreateColumnSet(PagedDataSource dataSource, bool useDataSource)
    {
    ArrayList a;
    a = base.CreateColumnSet(dataSource, useDataSource);
    return a;
    }
    

     

    设置断点运行代码。从基方法返回的一个包含 DataGridColumn 对象的数组,而且在这里为入口进行的修改都将被 DataGrid 反映到 HTML 生成。 不论是否是自动生成列,这里返回的数组都是正确的和最新的。图6表示 CreateColumnSet 方法被重写后的最终版本。首先我们获取列集, 然后按照 ColumnOrder 属性对它进行排序。对一个对象数组进行排序,我们需要如图6所示的一个自定义比较类。 比较类使用两个 DataGridColumn 对象的 HeaderText 属性来比较它们。

    protected override ArrayList CreateColumnSet(PagedDataSource dataSource,
    bool useDataSource)
    {
    // Let the grid generate the base column set (take the
    // AutoGenerateColumns property into account)
    ArrayList a = base.CreateColumnSet(dataSource, useDataSource);
    // If column dragging is disabled or reordering unnecessary
    if (ColumnOrder == "" || !EnableColumnDrag)
    return a;
    // Apply the desired sequence of columns
    if (ColumnOrder != "")
    {
    // Reorder the column set to reflect the client-side changes
    IComparer myComparer = new ColumnComparer(ColumnOrder);
    a.Sort(myComparer);
    }
    return a;
    }
    public class ColumnComparer : IComparer
    {
    private string[] _columnsOrder;
    public ColumnComparer(string order)
    {
    _columnsOrder = order.Split(',');
    }
    int IComparer.Compare(object x, object y)
    {
    DataGridColumn dgc1 = (DataGridColumn) x;
    int indexOf1 = Array.IndexOf(_columnsOrder, dgc1.HeaderText);
    DataGridColumn dgc2 = (DataGridColumn) y;
    int indexOf2 = Array.IndexOf(_columnsOrder, dgc2.HeaderText);
    if (indexOf1 < indexOf2)
    return -1;
    if (indexOf1 == indexOf2)
    return 0;
    else
    return 1;
    }
    }
    
    图6:重写 CreateColumnSet

     

    DHTML behavior 也是我们的持久化机制必不可少的一部分。 它负责当完成列拖动时将新的列顺序存储在隐藏域中。JavaScrip 完成这一工作,如图7

    protected override ArrayList CreateColumnSet(PagedDataSource dataSource, bool useDataSource)
    {
    ArrayList a;
    a = base.CreateColumnSet(dataSource, useDataSource);
    return a;
    }
    

     

    设置断点运行代码。从基方法返回的一个包含 DataGridColumn 对象的数组,而且在这里为入口进行的修改都将被 DataGrid 反映到 HTML 生成。 不论是否是自动生成列,这里返回的数组都是正确的和最新的。图6表示 CreateColumnSet 方法被重写后的最终版本。首先我们获取列集, 然后按照 ColumnOrder 属性对它进行排序。对一个对象数组进行排序,我们需要如图6所示的一个自定义比较类。 比较类使用两个 DataGridColumn 对象的 HeaderText 属性来比较它们。

    var buf = "";
    for(var i=0; i<headRow.children.length; i++) {
    buf = headRow.children[i].innerText;
    if (i < headRow.children.length-1)
    buf += ",";
    }
    // 一直移动到发现 FORM 为止的层次
    var obj = element.parentElement;
    while (obj.tagName != "FORM") {
    obj = obj.parentElement;
    if (obj == null) return;
    }
    // 写隐藏域
    var hiddenField = element.id;
    if (hiddenField != null)
    obj[hiddenField].value = buf;
    
    图7:写隐藏域

     

  • 分页怎么办?

    我开始向同事们夸耀我的得意之作。但是,当 DataGrid 包含分页或者页脚的时候,问题出现了。为什么呢? 因为分页使用完全不同的布局,并且把一些空值带入了 JavaScript 代码,导致其失败。把分页栏和页脚包含在 TFOOT 标签中来骗过。DHTML 只是移动头行(THEAD)和表体内(TBODY)的行。那么在页(表)脚内的内容就不会被改变。

    非常不幸的是,在 ASP.NET 中也可以把 DataGrid 的分页栏放在顶部。 这个设置正好和 THEAD 块冲突,因为分页栏成为了表的第一行。如果你不把置顶的分页栏放在 THEAD 中, 那么它将被显示成 table 的第二行,从而被当作一个 body 行处理。因此,仅修改 TFOOT 标签,还是会出现运行错误。 另一方面,如果你把置顶的分页栏放入 THEAD 中,你就必须修改 DHTML behavior,使它能在 THEAD 中找到真正的表头, 而不是就把第一行当作表头。换句话说:你必须修改 Render 方法,让 DataGrid 按下面的结构生成标记:

    <table>
    <thead>
    <tr>pager</tr>
    <tr><td>Column 1</td><td>Column 2</td></tr>
    </thead>
    <tbody>
    <tr><td>Some</td><td>Data</td></tr>
    <tr><td>More</td><td>Info</td></tr>
    </tbody>
    <tfoot><tr>footer</tr><tr>pager</tr></tfoot>
    </table>
    

     

    在客户端,behavior 要检查 THEAD 有几个子节点,如果原始的 DataGrid 包含置顶的分页栏,就选第二行作为表头。 这一信息通过一个新的属性在客户端传递——HaseTopMostPager。

    <public:property name='HasTopMostPager' />
    if (HasTopMostPager == "true")
    headRow = element.thead.children[1];
    else
    headRow = element.thead.children[0];
    

     

    Render 方法的最终修改版本太长了,不能直接贴在下面,你可以下载源码进行查看。 可拖动列的 DataGrid 的最后提醒:记住,你可以使用任何基本形式的列,包括模板列。

  • 排序怎么办?

    如果 DataGrid 支持排序,那么可排序列的表头就不是纯文本,而是由 HTML 标签构成的,一般是锚标签。 DHTML behavior 如何知道获取这个列头里面的文本呢。它会自动移除任何 HTML 格式, 获取点击的单元格的内部文本。

    现在让我们看看怎么进行客户端排序。 又一次,我们用到了 Dave Massy 的文章及其提供的另一个功能强大的 behavior 组件:sort.htc。 这个 behavior 会截取在表头的任何点击事件,并且根据此列的值对 table 进行排序。 这个 behavior 并记录最后点击的列,如果再次点击了同一个列头,排序逆转。 令外,表头将有一个符号来标识当前的排序列及其排序方向。图8显示了这一 behavior 的应用。 那个符号在 Wingdings 字体中是“3”,它通过一个 SPAN 标签被动态加入。

    图8:可排序的 DataGrid

    sort.htc 这个 behevior 也获取 OnContentReady 事件,做一些初始化工作, 然后为表头的每个单元格的 Click 事件关联一个处理器。 当 DataGrid 在客户端被建立起来的时候,每个单元格有一个空符号,原来的是默认的。 当用户在特定的列点击排序时,列的文本内容被排序。注意客户端的排序是完全基于文本的。 如果想基于列排序,请使用 DataGrid 提供的默认的服务器端自动排序功能。 现在只是对当前显示的内容进行排序。

    sort.htc behavior 在服务器端由一个新的属性——EnableClientSort——来控制。 它的工作方式和 EnableColumnDrag 相同,通过给 behavior 样式添加 sort.htc 的 URL 来允许排序。 两者可以容易的一同工作。

     

    想要纯客户端排序,这个基本已达到要求了。behavior 对 table 的内容进排序,当发生回传时排序无郊。 在一个 DataGrid 中可以同时支持客户端排序和服务器端排序。 当用户点击一个服务器端排序的列时(例如:图8中的 ID 列), 页面回传并激发 DataGrid 的服务器端事件。当用户在其它列点击时,排序就是在客户端进行的了。 这个版本的 sort.htc 是在所有的列上进行客户端排序,你可以改进它,使它支持只对部分列进行排序。

    有办法保持客户端排序吗?你打赌?创建第二个隐藏域, 然后由 sort.htc 组件把用来在服务器端进行排序的信息存到这个隐藏域中。 这个办法看上不很不错,但结果却并非如此。

    问题是:在客户端你无法得到关于生成显示数据的真正的数据列的完整信息。 behavior 能保存到隐藏域的信息只有用户点击列的头文本或索引,还有就是表示排序方向(递增或递减)的标记。

    在服务器端,这些信息很难被搞清楚。首先,我要尝试以基于文本的形式对数据源进行排序。 绑定到 DataGrid 的数据源可以是实现 IEnumerable 的任何对象,这才是我们面对的数据源, 除非你自己非要把数据限制在 ADO.NET 的几个对象上。但是,在你的排序之路上有个大挑战。 考虑到用户要求得到和在客户端完全相同的排序。你必须在以文本的形式排序数据源中对象。 但是数据源是强类型对象的集合——日期,字符串,数字和一切可能的选项格式。 要确保达到和客户端相同的顺序,你首先要把所有对象转换成它的标记副本。

    另一个值得一试的方法是:在 Render 方法中,当生成 HTML 行后,立即进行排序。我没试过这个方法, 但是我打赌,要对一个 table 的 HTML 进行排序,你一定得借助正则表过式和自定义比较类。

    解决这一问题,我使用了其它的方案。我使用一个隐藏域来保存用户在页面操作的最近一次排序的信息。 这些信息并不被用于服务器端。behavior 在客户端读取此域的内容并依此初始化 table。 以下 Render 方法中的代码保证这一过程:

     if (EnableClientSort)
    {
    string buf = "";
    buf = Page.Request[HiddenFieldForSorting].ToString();
    Page.RegisterHiddenField(HiddenFieldForSorting, buf);
    }
    

     

    当 behavior 被初始化时时候,table 可能有一部分已经以默认的排序形式被呈现到客户端了。 在初始化阶段,behavior 改变并刷新 table 的结构。但这样的操作顺序会产生一个非常令人不舒服的闪动。 要解决这一问题,可以把 DataGrid 产生的 table 包在一个 DIV 之间,并把它设成隐藏。所以当页面它一次显示时, 浏览器保存 DataGrid 的 table 的位置,但不显示出来。当 behavior 完成排序以后,它从 DHTML 对象模型中获取这个 DIV, 然后打开 visibility。behavior 通过一个共知的 ID 来识别 DIV

    这样,我们有了一个和 DHTML 相互依赖的 DataGrid ,并且有许多优点。但是你也看到,要想使 DataGrid 为你所用还是要做点工作的。 如果你认为这点技巧对你有用你可以下载源码,并告诉我效果如何。
    (全文完)