使用客户端行为来丰富 ASP.NET 的 DataGrid

时间:2021-10-08 16:14:21


原文出处:MSDN Magazine Jan 2004(Cutting Edge)

原代码下载 CuttingEdge0401.exe (128KB)

对大多数ASP.NET开发者而言,DataGrid 控件是一个基本工具,就象 Pizza 制作者的卷针。在ASP.NET 1.x中的 DataGrid 控 件是一个功能相当强大且多元性的工具。但是你可通过在客户端添加一点点脚本代码就可使它的功能更加强大。到目前为止,我还没有将JavaScript与DataGrid 控 件结合起来。最近,我在MSDN® Online上看到十二年前 Dave Massy 为其 "DHTML Dude" 专栏写的一篇神奇的文章。他以创新的方式将 HTML的 <table> 元素进行了重生。此外,Dave 还演示了如何通过列对一个表的内容进行索引和进行列的拖曳。
  他还演示了在一个<table>元素中DHTML行为的使用。我意识到,在生成HTML且发送到浏览器时,DataGrid 控件就是一个普通的<table>元素。因而我确信,它可能包含许多属性式样,只不过其框架是一个典型的 HTML 表格而已。这使我认识到我可以建立一个带列拖曳和客户端排序的DataGrid 控 件。该月的杂志专栏编入了我的实验。你可下载源码来确信我并非在骗你。
使用客户端行为来丰富 ASP.NET 的 DataGrid DHTML 行为快速浏览
  DHTML行为在一个功能丰富的 DataGrid 控件的实现中起了关键作用。待会你将会看到,我不能按以 Dave 定义的原始形式来使用其行为。要使这些行为在一个ASP.NET控 件的上下文中有效,需要进行一些改变。尽管在使用该修改后的组件时并不需要 JavaScript 技巧,之所以要快速地复习一下 DHTML 行为技术,主要是为了更好 地理解客户端与服务器端代码一起工作的机制。
  一个DHTML行为是将 CSS 式样绑定到一个HTML标签的脚本组件。若你的代码在不支持CSS或不识别行为式样的老浏览器上执行,则未知的式样将被忽略。为更深入了解DHTML,请见 《Scripting Evolves to a More Powerful Technology: HTML Behaviors in Depth》。(脚本提升到更加强大的技术:深入 HTML 行为)
  一个DHTML行为是一组 JavaScript 函数加上一些使用特定语法定义的公共成员的集合。通常,公共成员含属性和事件;有时也含方法。行为在 现有的 HTML 元素的最顶层工作以便允许你重写和扩展 HTML 元素的行为。为了达到这一点,一个行为可附加其代码到一个或多个DHTML标准事件。例如,一个行为可提供在 onmousedown 和 onmouseup 事件中进行列拖动的处理。更有甚者,所有特殊的 DHTML 行为可处理 oncontentready 事件,此事件在HTML子树(在特定元素的HTML中)完全解析之后被触发。oncontentready 事件是初始化 某个行为的好时机。
  行为的核心是曝露一些接口到 Microsoft® Internet Explorer(版本5.0和更高)的COM对象、但你可以把他们当作一个 C++ 二进制组件或 HTML 组件(HTC)文本文件来写。HTC文件可与使用它们的文件(HTML,ASP和ASP.NET)一起布署到服务器上,客户端不需要任何安装。
  如下代码显示了如何使用 dragdrop.htc 行为添加列拖动功能到一个<table>标签:

<table style="behavior:url(dragdrop.htc);" >  
注意:dragdrop.htc文件必须与使用它的文件布署到服务器的同一目录下。
使用客户端行为来丰富 ASP.NET 的 DataGrid可拖动的 DataGrid
  在读了Dave Massy的专栏之后,我下载了dragdrop.htc例子组件并尝试在一个例子页中捆绑它到一个DataGrid组件,如下:
      theGrid.Style["behavior"]="url(dragdrop.htc)";      
  奇怪的是,此代码并不工作。本人确信在客户端某一个 DataGrid 只不过是一个表格而矣,我决定将 Dave 的例子表中的源代码和 ASP.NET DataGrid 的 HTML 源代码进行比较、我注意到在 ASP.NET 1.x,由 DataGrid 生成的表格并不含 THEAD 和 TBODY 元素。但是元素是例子行为工作的基础。要实施列拖放行为,THEAD 和 TBODY 元素并不 是必须的,但是有了它们将会很容易定位表头和表体。
  你要么重写不带 THEAD 和 TBODY 的行为或写一个可输出带 THEAD 和 TBODY 标签的定制DataGrid 控件。对于一个象我 这样的 ASP.NET 开发者而言,我相信编写一个定制控件比编辑一个行为要容易些。我想我至少需要一个高效的调试器来单步跟踪我的代码。因此我启动了一个新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 控制并不添加拖放行为。其它两个属性定义被拖列的背景色和目标列的前景色。
  请注意这两个色彩属性并不影响 DataGrid 服务器端控制的逻辑。它们仅是简单的服务器端属性用于输出在客户端显示的 HTML 中。此两个属性当作 DataGrid 生成的标签定制属性被生成。DataGrid 的标记代码可在控 件的 Render 方法中生成,参见 Figure 1
两个行为属性添加到 DataGrid的 属性集合中,并以如下小代码生成:
      <table dragcolor="..." hitcolor="..." ...>      
  Render 方法生成带 THEAD 和 TBODY 标签的一个 <table> 元素。你仅可以一种方式来实现此功能,即捕获默认 HTML 标记并解析之。你可使用如下代码来捕获 为给定控件生成的 HTML 代码:
StringWriter writer = new StringWriter();HtmlTextWriter buffer = new HtmlTextWriter(writer);base.Render(buffer);string gridMarkup = writer.ToString();      
  你可建立一个新的 HtmlTextWrite 对象并将其捆绑到用来进行写操作的 write 对象中。在此时写操作由一内存块来代表,也即一个 StringWriter 对象。任何送到 HTML 之 writer 对象的内容实际上是聚合在写串的 writer 中。基类的 Render 方法生成 DataGrid 的标准标记,同时你可以捕获该标准标记并使用 StringWriter 的 ToString 将其转化成一个串。此方法简单且有效。此时,解析串并添加 THEAD 和 TBODY 就太容易了。THEAD 标签通常为表格的第一行,其通常含每一列有表头。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>      
最后,你将修改后的标记写到响应流。然后你就可以准备测试了。将源代码编译到一个程序集(Assembly)中并用Visual Studio.NET的工具注册该新控件。接着,将控件到添加例子页面。此步将插入如下代码到.aspx文件中:
<%@ Register TagPrefix="msdn" Namespace="Expoware.Controls"    Assembly="MyCtl" %>  
使用定制的 DataGrid 与使用基类的 DataGrid 一样。你需要改变其命名空间前缀,若需要,还添加些定制属性,如:
<msdn:datagrid runat="server" id="grid1" ...>...</msdn:datagrid>      
使用客户端行为来丰富 ASP.NET 的 DataGrid
Figure 2 拖动列

  Figure 2 显示了例子页的结果。若你列头进行拖动操作,一个代表该列的半透明面板将随鼠标移动。为改进用户体验,任何在鼠标下的列均将改变其头的背景色来反映它是放下的一个潜在目标。当你释放鼠标时,该透明列将插在刚好在光标下列的前面(左边)。最后,表格将重构来反映列的新顺序。所有这些将使用客户端 DHTML 对象模式并不需要与服务器端进行交互。鼠标事件和表格重构将由 DHTML 行为来管理。
使用客户端行为来丰富 ASP.NET 的 DataGrid增强拖放行为
  Figure 3 提供了几个基于事件处理的行为代码高级视图。oncontentready 事件代表入口点。该事件的处理例程初始化组件的状态且存取在附加元素中定义的公共属性。特别的是,dragdrop.htc 行为建立了一个单元坐标数组来用在拖放过程中检测来源列。
  每个单元的宽度使用 DHTML 的 ClientWidth 属性来跟踪,这是我对 Dave 专栏中源代码进行的一个修改。ClientWidth 属性检索 DHTML 对象含填充,但不含 旁白、边框和滚动条。使用该属性是关键,因为它可以让你不用在 HTML 源代码中明确设置宽度而使用列。既然我想应用行为到一个定制的 DataGrid 控制,要支持 ASP.NET DataGrids 的 AutoGenerateColumns 模式则使用 ClientWidth 是一种必然。
  此月下载中的 dragdrop.htc 文件也含其它一些小的修改,大多数是基于个人爱好。例如在拖放过程中反馈控件显示的式样。
  定制 DataGrid 控制在 EnableColumnDrag 属性设置时将设置行为式样。该属性保存其值在视图状态中。若属性设置成 True,则行为属性将被添加到 DataGrid 控制的 Style 对象中。注意:多个行为可被捆绑到相同的元素。
  EnableColumnDrag 属性与 ShowHeader 属性(一个标准布尔值其控制格子抬头的显示与关闭)是高度耦合的。若 DataGrid 并不显示一个表头,则列拖将自动被禁止。
  Figure 4 显示如何获取和设置两个属性的附属属性。注意你需要重量写ShowHeader 属性的附属属性来确保在 ShowHeader 属性为 False 时 EnableColumnDrag 属性也设置成 False。
  到目前为止,在客户端显示 DataGrid 时你可进行列的拖放。然而在页或 DataGrid 被刷新时其原列顺序将立即恢复。那么,如何保持新顺序 呢?
使用客户端行为来丰富 ASP.NET 的 DataGrid持续化新的列顺序
  在客户端的新列顺序信息在页刷新时必须传递到服务器端。在ASP.NET编程模式下进行客户端与服务器端的信息交互并没有什么特殊之处。将新列顺序传递到服务器端就如传递在一个文本框或下拉列表中的内容到服务器端一样。传递列顺序到服务器端并强制 DataGrid 按照新顺序生成列。让我们来先处理信息交换。
  在使用 HTML 和 HTTP 时,只有一种方式来传递客户端信息到服务器端,即使用隐藏字段。DataGrid 控制添加一个隐藏字段到页;DHTML 行为描绘新列顺序并将写之到该字段。当页面被刷新时,DataGrid 从隐藏字段检索新的顺序并相应生成它们。你可为该字段取任意的名字(只要该名字是唯一)。当然也还需要对检索该值负责。新列的顺序将以一个逗号分隔的串表示,该串还含列头的信息。注意该方法可是任意的,但是在任何情况下,你必须返回一个串。你可使用 Page.Request 来检索客户端的值:
string desiredOrder = Page.Request[HiddenFieldName].ToString();      
  此方案解决了此问题以允许你具有一致性的列拖放特征。尽管有效,但是在ASP.NET并非最好的方法。你是否用过IPostBackDataHandler接口?当你具有一个控制其从客户端输入数据时,该接口将非常有效。Figure 5显示了在定制的DataGrid控制中实施IPostBackDataHandler接口。基本上,该接口的方法允许你自动捆绑隐藏字段到控制的一个或多个属性。你并不需要自己调用Page.Request且你并不需要担心隐藏字段。ASP.NET将处理之。
  该接口列出了两个方法,此处仅使用了其一个方法:LoadPostData,该方法传递一个键和被传递的值集合。该键只是控制的ID,集合是Page.Request的一个子集。该集合含所有匹配页中存在控制的输入字段。只一种情况下,集合不匹配Page.Request,即当页中含动态建立的控制时。
  LoadPostData方法在OnInit事件之后且在OnLoad事件前被请求。ASP.NET仅在控制的ID匹配一个输入字段时才调用实施接口控制的LoadPostData方法。出于此种原因,你必须将输入字段指定与格子相同的ID。DataGrid仅在传递的数据上起作用,且并不需要发送服务器端信息到客户端。基于此,你可建立一个空串的隐藏字段,如下:
Page.RegisterHiddenField(ID, "");      
  LoadPostData方法刷新一个命名为ColumnOrder新属性的值。该属性将被用于确定在表格中列的顺序。请注意,若在两次连续的刷新中并没有发生列拖放,则ColumnOrder属性将为空。要保持以前设置的列顺序,则你必须维护ColumnOrder的值-例如:在视图模式下。此外,在刷新过程中,你应当不用空值来重写该属性。
  IPostBackDataHandler接口的第二个方法让你在刷新的值影响控制的状态时来触发服务器端事件。获取在客户端设置列顺序的串仅是任务的一半。现在你必须告诉DataGird以便新页以正确的顺序展现列。
使用客户端行为来丰富 ASP.NET 的 DataGridDataGrid 解读者
  你是否也曾关注一个 DataGrid 生成的秘密?一个朋友曾说我是一个"DataGrid 解读者", 他说是我的能力强制可恨的 DataGrid 控制按我希望的方式去动作。然而我必须承认,强制一个 DataGrid 改变列的固定顺序是相当困难的,即使对于那些具有相当经验的人而言。
  我用了几个小时企图从OnLoad事件中重新对DataGrid的列集合进行排序。此时我才意识到当列自动生成时该集合是空的。此外,当控制生成它的HTML时,任何输入的变化将会丢失。在我要放弃时,我注意到CreateColumnSet虚拟方法:
protected virtual ArrayList CreateColumnSet(   PagedDataSource dataSource,   bool useDataSource);      
  该方法是Microsoft .NET框架的一部分且未加任何说明,这是由于Microsoft认为用户并不会试着直接使用它。就我个人而言,我相信对于一个可重写的方法而言,要保持它不被使用是相当困难的.所以我就往下继续并为该方法重写了一段代码。我并不知道该方法具体能做什么,但是除了创建在DataGrid中需要的所有列之外,一个命名为CreateColumnSet的方法又能做什么呢?我写的第一个方法是一个非常小心的方法:
protected override ArrayList CreateColumnSet(   PagedDataSource dataSource, bool useDataSource){   ArrayList a;   a = base.CreateColumnSet(dataSource, useDataSource);   return a;}      
  我设置了一个断点并运行此段代码,由基方法返回的ArrayList含DataGridColumn对象且在此时输入的任何更改在生成的HTML中均作出了反应。返回的数组正确且与AutoGenerateColumns中设置的列相一致。Figure 6显示的是重写后的CreateColumnSet的最终版本。首先,你检索列集合,然后基于ColumnOrder属性对它们进行排序。对一个对象数组排序需要一个定制的比较类(如 Figure 6中定义的一个比较类)。该比较类基于其HeaderText属性来比较两个DataGridColumn对象。
  DHTML行为是一致性机制的基础。它负责在一个拖放操作完成时保存新的顺序在一个隐藏字段中(见 Figure 7)。此段JavaScript代码完成该工作。
使用客户端行为来丰富 ASP.NET 的 DataGrid有关页头的处理?
  在完成之后,我将之给我的同事用于反馈信息。不久就意识到,若 DataGrid 含一个页眉或页脚行时将会出现问题。为什么?因为页头具有不同的布局且允许一些null值进入JavaScript代码中而导致失败。可用一个小技巧在一个TFOOT块中整理页眉和页脚将解决之。DHTML行为仅移动表头和表体中的行。页脚中的任何内容将保持不变。
  不幸的是,在ASP.NET中,你可放置页头在DataGrid的顶部。该设置与THEAD块冲突(因为页头在表格的第一行显示)。如果你不放置最上层页头在THEAD中,它将当作表格的第二行显示并被当作表体的一行。这样就导致你将得到相同的运行时错误,最好与TFOOT标签一起修正之。相反,若将顶层页头放置在THEAD中,你必须修改DHTML行为以便其在真正的表格头上操作而不仅仅是在首行。也即你必须强制 DataGrid 的 Render 方法根据如下规范来生成标记文本:
<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>      
  该行为将需要检查在客户端THEAD具有多少个子,且在原表格含一个*页时选取第二行。该信息通过一个新属性HasTopMostPager传递到客户端。
<public:property name=''''HasTopMostPager'''' />if (HasTopMostPager == "true")   headRow = element.thead.children[1];else    headRow = element.thead.children[0];      
最后版本的Render方法太长,不便在此显示,你可通过下载源代码来获取之
最后有关DataGrid拖放时,请记住你可使用任意标准列,包括模板列
使用客户端行为来丰富 ASP.NET 的 DataGrid关于排序?
  
若DataGrid支持排序,则一个可排序列的头将不仅是普通的文本,而是由HTML元素组成,通常是一个锚标签。利用该列的DHTML行为你可知道如何获取其内的文本。它将用于获取点击单元格的内在文本,并自动移走任何HTML格式。
  现在让你知道如何建立客户端排序能力。在Dave Massy的专栏中证明并提供了一个强大的组件-sort.htc行为。该行为截取在一表格头的任意点击并基于从列中找到的值进行排序。该行为跟踪上次点击的列,且当你再次点击此列时,则其顺序将后反过来。此外,一个图开将添加到列头来指示排序列和它的排序方向。Figure 8显示一个使用此行为的DataGrid.图形为字体为Wingdings的一个"3"(动态建立的SPAN标签)。

使用客户端行为来丰富 ASP.NET 的 DataGrid
Figure 8 可排序的DataGrid

  sort.htc行为捕获OnContentReady事件,进行一些初始化工作,且为表格头中每个单元附加一个处理到其OnClick事件中。当表格在客户端设置之后,所有单元具有一个空的图形且其顺序是默认值。当用户点击来对一特定列排序时,则该列的内容将排序。注意基于客户端的排序将是纯文本的。若你要基于列排序,请使用DataGrid控制提供的默认服务器端排序机制。仅当前显示的记录集被排序sort.htc行为在服务器端由一个名为EnableClientSort属性来控制,它几乎等同于EnableColumnDrag且允许通过添加sort.htl到行为式样中来被允许,此两种行为(behavior)可在一起很好地运行。
  若你仅满足于客户端排序,则可说是完全地完成了。该行为(behavior)排序表格中的内容且在下次刷新时该排序将失效。客户端和服务器端排序可在相同的表格上被支持。当用户一个服务器端排序的列上点击时(见Figure 8中的ID列),页面将被刷新且触发一个服务器端事件到DataGrid。当用于点击任何其它列时,排序将在客户端上发生。此版本的sort.htc组件支持在所有列上排序。你可改进此组件以便使它接受仅在某些列上支持排序.
  是否有办法让客户端排序保持?当然可以,你可建立第二个隐藏字段,且用sort.htc组件将在服务器上用于排序的表达式赋给隐藏字段。咋一看好象是一个不错的主意,其实并非如此
问题在于在客户端你并不知道在屏幕上显示的列的数据的任何信息。你仅可知道的信息就是保存在隐藏字段中点击列的头文本或索引信息以及指示排序方面的标志信息(升序或降序)。在服务器端,该信息很难把握。首先我尝试以基于文本的方式来排序数据源。捆绑到DataGrid的数据源可为实施了IEnumerable接口的任意对象。除非你限制为一些通用的ADO.NET对象。然而最大的挑战仍是排序。若想提供给用户与在客户端产生的信息相同的行顺序,你必须以文本方式对数据源中所有数据对象进行排序。但是数据源是一个丰富类型的对象的集合,如日期,串,以及可能格式定义的数值。要确保你获取客户端相同的顺序,你必须首先转换任意对象为其标记的对应者。
最值行探讨的方法就是在Render方法中,在生成HTML行之后进行排序。我并没有这样做,但我敢打赌,使用正规表达式的一些帮助和一个定制的比较类,你可对代表一个表格的HTML串进行排序。
  要解决此问题,我选择了其它方法。我使用隐藏字段来跟踪用户上次使用的排序表达式。该信息并不在服务器端被使用。在客户端,行为读取隐藏字段的内容并相应重新初始化表格。下面几行(在Render方法中)使得此方法成为可能:
if (EnableClientSort){   string buf = "";   buf = Page.Request[HiddenFieldForSorting].ToString();   Page.RegisterHiddenField(HiddenFieldForSorting, buf);}     
  当行为被初始化时,在客户端除了以默认方式排序外,表格信息被部分显示。在此初始化阶段,行为将改变表格的结构并刷新。但这一系列操作会产生令人讨厌的闪烁。 为了解决这个问题,将为 DataGrid 而创建的表格包在 <div> 标签中并将其 visibility 设置为 hidden。 这样一来当页面首次显示时,浏览器将为表格预留空间但并不显示。当行为完成其排序时,它将检索在DHTML对象模型中的 <div> 标签并打开其visibility。该行为将通过一个 明确的ID来标识该<div>
使用客户端行为来丰富 ASP.NET 的 DataGrid总结
  DataGrid 和 DHTML 之间的共生关系是可以实现的,同时对用户来说有许多优点。但是正如你所见,你需要做一些工作才能使 DataGrid 实现你想做的任何事情。如果你认为这个小小练习对你有用,你可下载源代码并把你的结果告诉我。
发送问题和建议给 Dino
 
使用客户端行为来丰富 ASP.NET 的 DataGrid作者简介
  Dino Esposito 是在意大利罗马的一个讲师和顾问。也是 Microsoft ASP.NET(Microsoft出版社,2003)一书的作者,他将其大部分时间用于ADO.NET 和 ASP.NET 教学及演讲。可通过 cutting@microsoft.com 与他联系。
 
本文出自 MSDN MagazineJanuary 2004 期刊,可通过当地 报摊获得,或者最好是 订阅

使用客户端行为来丰富 ASP.NET 的 DataGrid本文由 VCKBASE MTT 翻译