datagrod

http://msdn.microsoft.com/zh-cn/magazine/cc164033(en-us).aspx
Cutting Edge
Extend the ASP.NET DataGrid with Client-side Behaviors
Dino Esposito

Code download available at: CuttingEdge0401.exe (128 KB)
Browse the Code Online
To the majority of ASP.NET developers, the DataGrid control is a basic and essential tool, much like the rolling pin for serious pizza makers. The DataGrid control in ASP.NET 1. x is an extremely powerful and versatile control and a great server tool. But you can make it even more powerful with the addition of just a little bit of client-side script. Until now, I hadn't played with JavaScript and DataGrid controls together. Recently I ran into a wonderful piece that Dave Massy wrote a couple of years ago for his " DHTML Dude" column on MSDN® Online. It discusses creative ways to revive the HTML <table> element. Among other things, Dave shows how to sort the content of a table by column and how to drag columns around.
He also demonstrates the use of DHTML behaviors with a <table> element. I realized that when rendered to HTML and served to the browser, the DataGrid control is a plain <table> element. Sure, it may contain a lot of style attributes, but its skeleton is a classic HTML table. This made me realize that I could create a DataGrid control with draggable columns and client-side sorting. This month's column chronicles my experiments. You can download the source code to see that I'm not cheating.

A Quick Tour of DHTML Behaviors
DHTML behaviors play an essential role in the implementation of a rich DataGrid control. As you'll see in a moment, I couldn't use Dave's behaviors in their original form. A few changes proved necessary to make those behaviors work in the context of an ASP.NET control. While no JavaScript skills are needed to use the modified components, a quick refresher of the DHTML behavior technology is in order for a better understanding of the mechanics that make client-side and server-side code work together.
A DHTML behavior is a script component that is bound to an HTML tag using a cascading style sheet (CSS) style—the "behavior" style. If your code is executed in an older browser that doesn't support CSS, or doesn't recognize the behavior style, the unknown style is simply ignored. For an in-depth look at DHTML, see Scripting Evolves to a More Powerful Technology: HTML Behaviors in Depth.
A DHTML behavior is a collection of JavaScript functions plus some public members defined using special syntax. Typically, public members include properties and events; sometimes they also include methods. Behaviors work on top of existing HTML elements and let you override and extend the behavior of HTML elements. To do so, a behavior attaches its own code to one or more DHTML standard events. For example, the behavior that provides column dragging handles the onmousedown and onmouseup events. Furthermore, all nontrivial DHTML behaviors can handle the oncontentready event, which fires when the HTML subtree (all the HTML in the particular element) has been completely parsed. The oncontentready event is a good time to initialize a behavior.
At their core, behaviors are COM objects that expose a few interfaces to Microsoft® Internet Explorer (version 5.0 and higher). However, you can write them as C++ binary components or as HTML Component (HTC) text files. HTC files can be deployed on the server alongside the files that use them (HTML, ASP, and ASP.NET), without any installation on the client.
The following code shows how to add column dragging capabilities to a <table> tag using the dragdrop.htc behavior:
<TABLE style="behavior:url(dragdrop.htc);" >
Here the file must be deployed on the server in the same directory as the file that uses it.

The Draggable DataGrid
After reading Dave Massy's column, I downloaded the sample dragdrop.htc component and tried to bind it to a DataGrid component in a sample page, like so:
theGrid.Style["behavior"] = "url(dragdrop.htc)";
Surprisingly, it didn't work. Armed with the certainty that on the client a DataGrid is nothing more than a table, I decided to compare the source code of Dave's sample table and the HTML source code of the ASP.NET DataGrid. I noticed that in ASP.NET 1. x, DataGrid-generated tables don't contain THEAD and TBODY elements. But these elements are essential for the sample behavior to work. THEAD and TBODY tags aren't strictly necessary to implement column drag and drop behavior, but they make it significantly easier to locate the table header and the table body.
You can either rewrite the behavior without THEAD and TBODY or write a custom DataGrid control that outputs a table with THEAD and TBODY tags. For an ASP.NET developer like me, I believed that it would be easier to write a custom control than edit a behavior. I thought that I'd at least have an effective debugger to step through the code. So I started a new Visual Studio® .NET solution and created a sample ASP.NET app and a Web control library project. The new DataGrid class had this prototype:
[ToolboxData("<{0}:DataGrid runat=/"server/" />")]
public class DataGrid : System.Web.UI.WebControls.DataGrid
{
public DataGrid() : base()
{
EnableColumnDrag = true;
DragColor = Color.Empty;
HitColor = Color.Empty;
}
•••
}
The constructor initializes three public custom properties: EnableColumnDrag, DragColor, and HitColor. EnableColumnDrag is a Boolean property that enables column dragging. With this property set to False, the custom DataGrid doesn't add the drag behavior. The other two properties define the background color of the dragged column and the color of the column that's the target of the drop.
Note that these two color properties don't affect the logic of the DataGrid server control. They are simply server-side properties that are used to output values to HTML that will be consumed by the client-side behavior. The values of the two properties are rendered as custom attributes to the <table> tag generated for the grid. The markup code of the DataGrid is built in the Render method of the control listed in Figure 1.
The two behavior properties are added to the Attributes collection of the DataGrid to be rendered as in the following snippet:
<table dragcolor="..." hitcolor="..." ...>
The Render method generates a <table> element with THEAD and TBODY tags. There's just one way in which you can force this—capture the default HTML markup and parse it. You can capture the HTML code generated for a given control using the following code:
StringWriter writer = new StringWriter();
HtmlTextWriter buffer = new HtmlTextWriter(writer);
base.Render(buffer);
string gridMarkup = writer.ToString();
You create a new HtmlTextWriter object and bind it to the writer object providing the writing surface. In this case, the writing surface is represented by a block of memory, a StringWriter object. Everything that is sent to the HTML writer is actually accumulated in the string writer. The base Render method generates the standard markup of the DataGrid and you capture that to a string using the ToString method of the StringWriter. Simple and effective. At this point, parsing the string and adding THEAD and TBODY is child's play. The THEAD tag wraps the first table row, which typically contains the header of each column. The TBODY identifies the body of the table:
<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>
Finally, you write the modified markup to the response stream. Then you're ready for testing. Compile the source code to an assembly and register the new control with the Visual Studio .NET toolbox. Next, add the control to the sample page. This operation inserts the following code in the .aspx file:
<%@ Register TagPrefix="msdn" Namespace="Expoware.Controls" 
Assembly="MyCtl" %>
Using the custom DataGrid is identical to using the base DataGrid. The only thing that changes is the namespace prefix and, if need be, custom properties are added:
<msdn:datagrid runat="server" id="grid1" ...>
•••
</msdn:datagrid>
Figure 2  Dragging Columns 
Figure 2 shows the sample page in action. If you grab the header of a column and drag it, a semi-transparent panel that represents the column is moved with the mouse. To improve the user's experience, any column underneath the mouse changes the background color of its header to reflect that it is a potential target for the drop. When you release the mouse button, the transported column is inserted before (to the left of) the column that's under the cursor. Finally, the table is restructured to reflect the new column order. All these operations exploit the client-side DHTML object model and require no round-trip to the server. The mouse events and the table rebuilding are governed by the DHTML behavior.

Enhancing the Dragdrop Behavior
Figure 3 provides a high-level view of the behavior's code, based on a few event handlers. The oncontentready event represents the entry point. The handler for this event initializes the state of the component and accesses public properties as defined in the attached element. In particular, the dragdrop.htc behavior builds an array of cell coordinates to be used later during the dragging and dropping to detect the underlying columns.
The width of each cell is tracked using the DHTML ClientWidth property. This is one of the changes that I made to the original source code from Dave's column. The ClientWidth property retrieves the width of the DHTML object including padding, but not including margin, border, or scroll bar. The use of this property is a key enhancement as it allows you to support columns with no explicit width set at the HTML source level. Since I intend to apply the behavior to a custom DataGrid control, the use of ClientWidth is essential to supporting the AutoGenerateColumns mode of ASP.NET DataGrids.
The dragdrop.htc file in this month's code download also contains other minor modifications, mostly due to personal preferences. One example is the style of the feedback control displayed during the drag and drop operation.
The custom DataGrid control sets the behavior style when the EnableColumnDrag property is set. The property stores its value in the view state. If the property is set to True, the behavior attribute is added to the Style object of the DataGrid control. Note that multiple behaviors can be bound to the same element.
The EnableColumnDrag property is tightly coupled to ShowHeader—the standard Boolean property that turns the header of the grid on and off. If the DataGrid doesn't display a header, the column dragging is silently disabled. Figure 4 shows the get and set accessors of both properties. Note that you need to override the set accessor of the ShowHeader property to ensure that EnableColumnDrag is set to False if the ShowHeader property is False.
So far so good. You can now drag and drop columns when the DataGrid is displayed on the client. However, the original column order is restored as soon as the page or the DataGrid posts back. Is there something you can do to persist the new order?

Persisting the New Column Order
The new column order on the client is a piece of information that must be passed to the server when the page posts back. Exchange of information between the client- and server-side environments is nothing special in the ASP.NET programming model. Communicating the new column order is like passing to the server the text typed in a textbox or the selected item in a dropdown list. You pass the desired sequence of columns to the server and force the DataGrid control to render the columns accordingly. Let's tackle information exchange first.
Using HTTP and HTML today, there's only one way to carry client-side information to the server—using a hidden field. The DataGrid control adds a hidden field to the page; the DHTML behavior figures out the new sequence of columns and writes it out to the field. When the page posts back, the DataGrid retrieves the desired sequence from the hidden field and renders itself accordingly. You can choose any name for the hidden field as long as it's unique. You are also responsible for retrieving the posted value. The desired order of columns can be expressed with a comma-separated string that concatenates the header of each column. Note that this approach is arbitrary but, in any case, you must return a string. You retrieve the client value using Page.Request:
string desiredOrder = Page.Request[HiddenFieldName].ToString();
This solution solves the problem and allows you to have a persistent column drag feature. Although functionally correct, this is not the best approach in ASP.NET. Have you ever run across the IPostBackDataHandler interface? Well, that interface comes in handy when you have a control that imports data from the client. Figure 5 shows the implementation of the IPostBackDataHandler interface in the custom DataGrid control. Basically, the methods of the interface allow you to automatically bind the data coming through a hidden field with one or more properties of the control. You don't have to call Page.Request on your own and you don't have to worry much about the hidden field. ASP.NET takes care of most of the plumbing.
The interface lists two methods, only one of which is used here: LoadPostData, which is passed a key and the collection of posted values. The key is simply the ID of the control; the collection is a subset of Page.Request. The collection contains all the posted input fields that match existing controls in the page. There's just one case in which the collection doesn't match Page.Request—when the page contains dynamically created controls.
The LoadPostData method is invoked after the OnInit event but before the OnLoad event. ASP.NET calls the LoadPostData method of a control that implements the interface only if the ID of the control matches one of the input fields. For this reason, you should give the input field the same ID as the grid. The DataGrid only works on posted data, and has no need to send server-side information to the client. For this reason, you can create the hidden field with an empty string, like this:
Page.RegisterHiddenField(ID, "");
The LoadPostData method refreshes the value of a new property named ColumnOrder. This property will be used to determine the order of the columns in the grid. It is important to note that if no column dragging occurs between two consecutive postbacks, the ColumnOrder property is empty. To persist the previously set order you must persist the value of ColumnOrder—for example, in the view state. In addition, you should never overwrite the property with an empty posted value.
The second method of the IPostBackDataHandler interface gives you a chance to raise a server-side event when posted values affect the state of the control. Getting the string with the column order set on the client is only half the task. Now you have to tell the Data-Grid so that the new page presents the columns in the right order.

The DataGrid Whisperer
Have you ever tried to whisper to a DataGrid about rendering? A friend once called me "the DataGrid whisperer." He said it was for my ability to force the darned control to behave any way I want it to behave. However, I must confess that forcing a DataGrid to change the fixed order of columns proved quite difficult even for someone with lots of experience.
I spent a few hours trying to reorder the Columns collection of the DataGrid from within the OnLoad handler. Then I realized that the collection is empty when columns are autogenerated. Plus, any changes entered are lost when the control renders its HTML. Just before I gave up, I noticed the CreateColumnSet protected virtual method:
protected virtual ArrayList CreateColumnSet(
PagedDataSource dataSource,
bool useDataSource
);
The method is considered part of the Microsoft .NET Framework and left undocumented because it is not intended to be used directly from user code. Personally, I believe that it's hard to keep it from being used this way when you're talking about a protected virtual (overridable) method. So I went on and wrote an override for that method. I didn't know exactly what the method was doing, but what else could a method named CreateColumnSet do but create all the columns needed in a DataGrid? My first approach was a cautious one:
protected override ArrayList CreateColumnSet(
PagedDataSource dataSource, bool useDataSource)
{
ArrayList a;
a = base.CreateColumnSet(dataSource, useDataSource);
return a;
}
I put in a breakpoint and ran the code. The ArrayList returned by the base method contains DataGridColumn objects and any change entered at this point was reflected in the HTML generated by the grid. The returned array is correct and up to date whatever setting AutoGenerateColumns has. Figure 6 shows the final version of the method that overrides CreateColumnSet. First, you retrieve the column set and then order it based on the ColumnOrder property. Sorting an array of objects requires a custom comparer class like the one in Figure 6. The comparer class compares two DataGridColumn objects on the value of their HeaderText property.
The DHTML behavior is essential to the persistence mechanism. It is responsible for storing the new order in the hidden field when a drag completes (see Figure 7). The JavaScript does the work.

What About Paging?
Proud of my creation, I passed it on to colleagues for feedback. It didn't take long to realize that something bad happens if the DataGrid contains a pager or footer row. Why? Because the pager has a different layout and some null values creep into the JavaScript code, making it fail. Wrapping pager and footer in a TFOOT block does the trick. The DHTML behavior moves only the header row and the rows in the body. Anything in the footer is left unchanged.
Unfortunately, in ASP.NET you can place the pager on top of the DataGrid as well. This setting clashes with the THEAD block because the pager is displayed as the first row of the table. If you don't place the topmost pager in the THEAD, it will be displayed as the second row in the table and treated as a body row. As a result, you'll receive the same runtime errors, just fixed with the TFOOT tag. On the other hand, if you place the topmost pager in the THEAD you have to edit the DHTML behavior so that it operates on the real header row and not just the first one found. In other words, you have to force the Render method of the DataGrid to generate the markup according to the following schema:
<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>
The behavior will need to check how many children the THEAD property has on the client and pick the second row if the original grid contains a topmost pager. This information is passed on to the client using a new attribute—HasTopMostPager.
<public:property name='HasTopMostPager' />
if (HasTopMostPager == "true")
headRow = element.thead.children[1];
else
headRow = element.thead.children[0];
The final version of the Render method is too long to show here. You can check it out by downloading the source code.
As a final note about DataGrid drag and drop, remember that you can use any standard column, including templated columns.

What About Sorting?
If the DataGrid supports sorting, the header of a sortable column isn't plain text but is made of HTML elements, typically an anchor tag. The DHTML behavior you get with this column knows how to get the text inside. It takes the inner text of the clicked cell, automatically removing any HTML formatting.
Now let's see how you can build some client-side sorting capabilities. Once again, Dave Massy's column proves useful and provides another powerful component—the sort.htc behavior. The behavior intercepts any clicking on the header of a table and sorts the table by the values found in the column. The behavior tracks the last clicked column, and if you click the column again, the order is reversed. In addition, a glyph is added alongside the header of the column to indicate the sorting column and its direction. Figure 8 shows a DataGrid that utilizes this behavior. The glyph is a "3" in Wingdings font inserted through a dynamically created SPAN tag.
Figure 8  Sortable DataGrid 
The sort.htc behavior captures the OnContentReady event, does some initialization work, and attaches a handler to the OnClick event for each cell in the table's header. When the grid is set up on the client, all cells have an empty glyph and the order is the default one. When the user clicks to sort by a particular column, the textual content of the column is sorted. Note that the client-side sorting is purely text based. If you want column-based sorting, use the default server-side sorting mechanism that the DataGrid control provides. Only the currently displayed set of records are sorted.
The sort.htc behavior is controlled on the server by a new property named EnableClientSort, which is nearly identical to EnableColumnDrag and enables sorting by adding the sort.htc URL to the behavior style. The two behaviors work together easily.
If you're content with client-only sorting, you're pretty much finished. The behavior sorts the contents of the table and the sort is lost with the next postback. Client- and server-side sorting can be supported in the same grid. When the user clicks on a server-sortable column (see the ID column in Figure 8), the page posts back and fires a server event to the DataGrid. When the user clicks on any other columns, the sorting takes place on the client. This version of the sort.htc component supports sorting on all columns. You can improve the component to make it accept sorting on only a subset of columns.
Is there a way to make client-side sorting persistent? You bet. You create a second hidden field and have the sort.htc component put the expression to use on the server to sort data into that hidden field. At first, this looks like a good idea, but it turns out not to be.
The problem is that on the client you have no information at all about the data columns that generated the data on the screen. The only information the behavior can store to the hidden field is the index or the header text of the clicked column and a flag to indicate the direction (ascending or descending) of the sorting.
On the server, this information is difficult to make sense of. The first thing I tried to do was sort the data source in a text-based way. The data source bound to the DataGrid can be any object that implements the IEnumerable interface. Unless you want to limit yourself to a few common ADO.NET objects, this is the object to work with. However, the big challenge lies in the way you perform the sort. Consider that to offer users the same order of rows they produced on the client, you must sort all data objects in the data source as text. But the data source is a collection of strongly typed objects—dates, strings, numbers with possible formatting options. To be sure you get exactly the same order of the client, you should first transform any object into its markup counterpart.
An approach that is worth exploring is sorting in the Render method immediately after having generated the HTML rows. I haven't tried this but I bet that with a little help from regular expressions and a custom comparer class, you can sort an HTML string that represents a table.
To solve the problem, I've chosen another approach. I use the hidden field to track the sort expression in use on the client the last time the user worked with the page. This information is not used on the server. On the client, the behavior reads the content of the hidden field and reinitializes the table accordingly. The following lines in the Render method make this possible:
if (EnableClientSort)
{
string buf = "";
buf = Page.Request[HiddenFieldForSorting].ToString();
Page.RegisterHiddenField(HiddenFieldForSorting, buf);
}
When the behavior is initialized, the table may be partially rendered on the client but with the default sorting. In the initialization step, the behavior changes the structure of the table and refreshes. But this sequence of operations produces a nasty flickering effect. To remedy this, wrap the table created for the DataGrid in a <div> tag and set its visibility to hidden. So when the page is first displayed, the browser saves the space for the table but doesn't display it. When the behavior has completed its sorting, it retrieves the <div> in the DHTML object model and turns the visibility on. The behavior identifies the <div> through a well-known ID.

Summing It Up
A symbiotic relationship between the DataGrid and DHTML can be attained and has lots of advantages for users. But as you can see, it takes a bit of work to coerce the DataGrid into doing everything you want it to do. If you think this little exercise will be useful to you, download the source code and let me know the outcome.

Send your questions and comments for Dino to  cutting@microsoft.com.


Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Programming Microsoft ASP. NET (Microsoft Press, 2003) he spends most of his time teaching classes on ADO.NET and ASP.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com.

===============================================================================================

对大多数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 控 件。该月的杂志专栏编入了我的实验。你可下载源码来确信我并非在骗你。
 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文件必须与使用它的文件布署到服务器的同一目录下。
可拖动的 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>

Figure 2 拖动列

  Figure 2 显示了例子页的结果。若你列头进行拖动操作,一个代表该列的半透明面板将随鼠标移动。为改进用户体验,任何在鼠标下的列均将改变其头的背景色来反映它是放 下的一个潜在目标。当你释放鼠标时,该透明列将插在刚好在光标下列的前面(左边)。最后,表格将重构来反映列的新顺序。所有这些将使用客户端 DHTML 对象模式并不需要与服务器端进行交互。鼠标事件和表格重构将由 DHTML 行为来管理。
增强拖放行为
  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 按照新顺序生成列。让我们来先处理信息交换。
  在使用 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以便新页以正确的顺序展现列。
DataGrid 解读者
  你是否也曾关注一个 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代码完成该工作。
有关页头的处理?
  在完成之后,我将之给我的同事用于反馈信息。不久就意识到,若 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拖放时,请记住你可使用任意标准列,包括模板列
关于排序?
  
若DataGrid支持排序,则一个可排序列的头将不仅是普通的文本,而是由HTML元素组成,通常是一个锚标签。利用该列的DHTML行为你可知道如何获取其内的文本。它将用于获取点击单元格的内在文本,并自动移走任何HTML格式。
  现在让你知道如何建立客户端排序能力。在Dave Massy的专栏中证明并提供了一个强大的组件-sort.htc行为。该行为截取在一表格头的任意点击并基于从列中找到的值进行排序。该行为跟踪上次点 击的列,且当你再次点击此列时,则其顺序将后反过来。此外,一个图开将添加到列头来指示排序列和它的排序方向。Figure 8显示一个使用此行为的DataGrid.图形为字体为Wingdings的一个"3"(动态建立的SPAN标签)。


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>
总结
  DataGrid 和 DHTML 之间的共生关系是可以实现的,同时对用户来说有许多优点。但是正如你所见,你需要做一些工作才能使 DataGrid 实现你想做的任何事情。如果你认为这个小小练习对你有用,你可下载源代码并把你的结果告诉我。
发送问题和建议给 Dino
 
作者简介
  Dino Esposito 是在意大利罗马的一个讲师和顾问。也是 Microsoft ASP.NET(Microsoft出版社,2003)一书的作者,他将其大部分时间用于ADO.NET 和 ASP.NET 教学及演讲。可通过 cutting@microsoft.com 与他联系。
 
本文出自 MSDN MagazineJanuary 2004 期刊,可通过当地 报摊获得,或者最好是 订阅

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值