Cutting Edge

Extend the ASP.NET DataGrid with Client-side Behaviors

Dino Esposito

Code download available at:CuttingEdge0401.exe(128 KB)

Contents

A Quick Tour of DHTML Behaviors
The Draggable DataGrid
Enhancing the Dragdrop Behavior
Persisting the New Column Order
The DataGrid Whisperer
What About Paging?
What About Sorting?
Summing It Up

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.

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.

Figure 1 Render Method

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 <thead> int insertPoint; insertPoint = gridMarkup.IndexOf(">") + 1; // len of > gridMarkup = gridMarkup.Insert(insertPoint, "<thead>"); insertPoint = gridMarkup.IndexOf("</tr>") + 5; // len of </tr> gridMarkup = gridMarkup.Insert(insertPoint, "</thead><tbody>"); // Replace </table> with </tbody></table> gridMarkup = gridMarkup.Replace("</table>", "</tbody></table>"); // Output the markup output.Write(gridMarkup); return; } base.Render(output); return; }

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** 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.

Figure 3 Dragdrop Pseudocode

<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() { // Store references to THEAD and TBODY elements // Fill out global variables with information available at this time // Build an array of cell coordinates for hit testing // For each cell, attach an handler to the OnMouseDown event // Attach event handlers for the OnMouseMove and OnMouseUp events } function onMouseDown(e) { // Retrieve the outermost TD tag so that drag starts wherever you // click in the cell // Create the panel to use for feedback during the drag and drop // operation // Configure the panel to be transparent, have the same width and // height of the real column, // support absolute positioning, and have a dashed border. The panel // is hidden at first // Insert the panel before the cell clicked } function onMouseMove(e) { // Save the current mouse position // Turn visibility on for the panel // Figure out the underlying cell and refresh its visual settings // Stop bubbling of the event } function onMouseUp(e) { // Figure out the cell target of the drop // Remove the dynamically created panel from the DHTML tree // Update the array of cell coordinates for hit testing // Rebuild header and body of the table } // Some more helper functions </script>

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.

Figure 4 Get and Set

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; } }

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.

Figure 5 Implementing IPostBackDataHandler

public class DataGrid : System.Web.UI.WebControls.DataGrid, IPostBackDataHandler { public DataGrid() : base() { EnableColumnDrag = true; DragColor = Color.Empty; HitColor = Color.Empty; ColumnOrder = ""; } // ************************************************************* // IPostBackDataHandler::LoadPostData // Automatically updates the ColumnOrder property based on the // content of the hidden field named as this control public virtual bool LoadPostData(string key, NameValueCollection values) { string currentColumnOrder = ColumnOrder; string postedColumnOrder = Convert.ToString(values[key]); // Discard new values if empty if (postedColumnOrder == "") return false; // New value posted? if (!currentColumnOrder.Equals(postedColumnOrder)) { ColumnOrder = postedColumnOrder; return true; } return false; } // ************************************************************* // ************************************************************* // IPostBackDataHandler::RaisePostDataChangedEvent public virtual void RaisePostDataChangedEvent() { // Do nothing here // No need of firing server-side events } // ************************************************************* // ************************************************************* // Return the name of the hidden field private string HiddenFieldForDragging { get {return ID;} } // ************************************************************* // ************************************************************* // Stores the comma-separated string indicating the column order private string ColumnOrder { get {return Convert.ToString(ViewState["ColumnOrder"]);} set {ViewState["ColumnOrder"] = value;} } // ************************************************************* ••• }

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.

Figure 6 Overriding CreateColumnSet

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; } }

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.

Figure 7 Write to Hidden Field

var buf = ""; for(var i=0; i<headRow.children.length; i++) { buf = headRow.children[i].innerText; if (i < headRow.children.length-1) buf += ","; } // Move up the hierarchy until the FORM is found var obj = element.parentElement; while (obj.tagName != "FORM") { obj = obj.parentElement; if (obj == null) return; } // Write to the hidden field var hiddenField = element.id; if (hiddenField != null) obj[hiddenField].value = buf;

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

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.