Cutting Edge
Moving DataGrid Rows Up and Down
Dino Esposito
Code download available at:CuttingEdge0503.exe(136 KB)
Contents
The RowOver DHTML Behavior
So What's Wrong with RowOver?
The Mover Behavior
The TableGrid Control
Under the Hood of the Table Rendering
That Darn PostBack
ASP.NET 2.0 Considerations
Note on the Sample Code
Imagine opening your Inbox one morning and finding a message that reads "Dear Mr. DataGrid, I urgently need an ASP.NET DataGrid that lets my users move rows on the client. You're my last hope. Will you please help me?"
Wouldn't you do your best to answer such a plea? When I got that very message, I pointed its author to my January 2004 column where I discuss how to enhance an ASP.NET DataGrid to drag columns and sort displayed rows on the client. That solution was built atop DHTML behaviors. Although internally it's pure script, a DHTML behavior is really more than that. It adds object orientation to plain JavaScript and lets you expose properties and methods to drive virtually any client-side HTML element.
However, when my needy programmer friend was still unable to solve his problem, it was time for Mr. DataGrid to save the day (yep, just like a cartoon superhero). Thus was born this month's column—not merely an account of how the specific problem was solved, but also a sneak preview of the control development process as it will work in ASP.NET 2.0.
The RowOver DHTML Behavior
The RowOver.htc file is part of the Microsoft® Internet Explorer DHTML Behavior Library, a collection of free components for advanced client functionalities (rowover Behavior). Specifically designed for Internet Explorer 5.0 and later, behaviors add interactive effects to Web pages and provide a means for script encapsulation and code reuse. Applying a behavior to an HTML element is as easy as attaching a style. In its original version, the RowOver.htc behavior adds alternate row shading and highlighting for table elements:
<table Style="behavior:url(rowover.htc)" ID="grid" Selectable="true"> ... </table>
The Selectable attribute refers to a public property exposed by the RowOver behavior. A behavior can be seamlessly attached to ASP.NET controls too, and the following lines of code adds one to the output of a DataGrid:
<asp:DataGrid Runat="server" ID="grid" Style="BEHAVIOR:url(rowover.htc)" Selectable="true" />
In Figure 1, the ordinary output of the DataGrid is extended with row highlighting and selection. The row under the mouse is tracked as you move across the table and is displayed with a different set of graphical attributes. In addition, when you click on a highlighted row, the row is tracked as selected. It is important to note that all this occurs on the client with no postback to the server.
Figure 1** Ordinary DataGrid **
While all of this looks great, once you start testing the feature more thoroughly you will discover a number of issues with the RowOver behavior that prevent it from addressing the problem at hand.
So What's Wrong with RowOver?
The row highlighting feature selects all table rows found, including caption, head, and foot rows, and there's no way to get the index of the currently selected row. In addition, the RowOver behavior is not designed to move rows up and down after the user clicks on client buttons, but my friend's ultimate goal was to get a list of rows that could be ordered (moved up and down) using client-side buttons and then posted to the server.
The solution is to build a custom DataGrid control and implement IPostBackDataHandler. But before I get started on this, I'll first enhance the original RowOver.htc behavior to make it support row movements. Next, I'll build a DataGrid control that returns a better-formed table with THEAD, TBODY, and TFOOT tags. This custom control will implement IPostBackDataHandler and carry data back and forth using its own hidden field. Let's start by going over the behavior enhancements.
The Mover Behavior
My new Mover.htc behavior is different from RowOver.htc on several fronts. Mover.htc has none of the original code that does row shading. If you want alternate row coloring you'd be better off using DataGrid styles today. (The original RowOver source code dates back to late 1999, before ASP.NET was born.)
Internally, RowOver.htc tracks the currently selected row and stores a reference to it in an internal variable. However, the behavior doesn't have a public variable to read the index of this row:
<PROPERTY NAME="SelectedIndex" />
This is just what the new SelectedIndex property does. Using this property you can write the following event handler for the button that was shown in Figure 1:
function DoSelect() { alert("Selected Row: " + grid.SelectedIndex); }
Mover.htc also features two new methods to move the selected row one position up and down:
<METHOD NAME="MoveUp" /> <METHOD NAME="MoveDown" />
Figure 2 shows the source code for these methods. It is interesting to note the richness of the DHTML object model. The DHTML Table object includes a method that moves rows from a starting to an ending position, as shown here:
element.moveRow(moveFrom, moveTo);
Figure 2 MoveUp and MoveDown Methods on the DHTML Behavior
//+---------------------------------------------------------------------- // Function: InitComponent // Description: Called during the initialization of the behavior. // Caches some internal-use variables // Arguments: none // Returns: nothing //----------------------------------------------------------------------- function InitComponent() { tBody = element.tBodies[0]; bodyRowCount = tBody.rows.length-1; nFirstBodyIndex = tBody.rows[0].rowIndex; nLastBodyIndex = tBody.rows[bodyRowCount].rowIndex; SelectedIndex = -1; } //+---------------------------------------------------------------------- // Function: MoveUp // Description: Move the selected row one position up // Arguments: none // Returns: nothing //----------------------------------------------------------------------- function MoveUp() { if (!eval(Selectable)) return false; if (oSelectRow == null) return false; // Determine starting and ending position var moveFrom = oSelectRow.rowIndex; var moveTo = oSelectRow.rowIndex-1; // Should not move out of <tbody> rows if (moveTo < nFirstBodyIndex) moveTo = nLastBodyIndex; // Move element.moveRow(moveFrom, moveTo); } //+---------------------------------------------------------------------- // Function: MoveDown // Description: Move the selected row one position down // Arguments: none // Returns: nothing //----------------------------------------------------------------------- function MoveDown() { if (!eval(Selectable)) return false; if (oSelectRow == null) return false; // Determine starting and ending position var moveFrom = oSelectRow.rowIndex; var moveTo = oSelectRow.rowIndex+1; // Should not move out of <tbody> rows if (moveTo > nLastBodyIndex) moveTo = nFirstBodyIndex; // Move element.moveRow(moveFrom, moveTo); }
To get the index of the selected row, you use the rowIndex property. You should be aware that rowIndex returns the absolute index of the row in the table element; it is not a value relative to the body. In order to get the relative index, I use a couple of internal helper variables, as you can see in the code.
Now that I have a fully functional behavior to move rows up and down, I just need to create a slightly modified DataGrid control to create the client table. Mover.htc works with or without a TBODY tag, but having that tag in place automatically limits highlighting and selecting the body rows.
The TableGrid Control
The new control is named TableGrid and overrides the rendering mechanism of the grid to insert THEAD, TBODY, and TFOOT tags. The control automatically binds to the Mover.htc behavior and exports a new Selectable property to activate client-side selection from the server. Figure 3 shows the implementation of the Selectable Boolean property.
Figure 3 Selectable Property on the Custom Control
namespace Msdn { public class TableGrid : System.Web.UI.WebControls.DataGrid { // ************************************************************** // PROPERTY: Selectable // Enables the mover.htc component to work on the grid's output public bool Selectable { get { object o = ViewState["Selectable"]; if (o == null) return true; return (bool) o; } set { ViewState["Selectable"] = value; Attributes["Selectable"] = value.ToString().ToLower(); if (value) Style["behavior"] = "url(mover.htc)"; else Style.Remove("behavior"); } } ••• } }
As I mentioned, THEAD, TFOOT, and TBODY tags are important because the HTC detects their presence and automatically adjusts its runtime behavior. The issue is that the DataGrid control is not designed to output those tags. In the January 2004 column, I faced the same problem while building a drag and drop DataGrid. In that case, I first had the control output the standard markup to an in-memory buffer and then parsed the text to inject any missing tags. Although functional, this approach is neither elegant nor particularly efficient. The primary drawback lies in the fact that you need to both hold in memory and process the entire output of the DataGrid, which can be a large chunk of text and which could limit scalability under certain conditions. Note that the default DataGrid is much faster because it writes directly to the output buffer. In this context, page output caching can help alleviate your scalability troubles at least for the first request of the page, that is, non-postback requests.
Is there a way to avoid parsing the whole markup for the DataGrid? My understanding is that this is impossible unless you completely replace the rendering engine of the DataGrid. Let's take a look at this engine. The most interesting thing that is going on here is that the DataGrid control doesn't have its own rendering engine; it simply relies on the rendering engine of its constituent controls. The DataGrid is a composite control based on a table that works by building a collection of rows and cells. Next, it iteratively calls to these controls to render out to HTML. In the end, stating that the ASP.NET 1.1 DataGrid control doesn't support the TBODY tag is technically incorrect because the DataGrid doesn't even attempt to render any HTML markup.
The point is that the Table control (and related controls like TableRow) in ASP.NET 1.1 don't support those tags. All things considered, I still believe that parsing up the generated markup is a reasonable way out in ASP.NET 1.1. If you foresee major scalability issues with the approach outlined here (and also implemented in the January 2004 drag and drop control) then you might also want to try output filters.
An output filter is a custom stream object that you attach to the page's output buffer to post-process any outgoing markup. In this way, you process the full markup of the page in a single shot, apply your changes and, best of all, you don't need to use custom controls and or modify existing pages.
An alternative way to support the table section tags, and one that's arguably more elegant, is to derive from Table and override the RenderContents method in order to output section tags. Then you would override DataGrid's CreateControlHierarchy method to create your own Table type and override CreateItem to create your own DataGridItem type. This is a much more complicated solution because you have to override CreateControlHierarchy, but it's more efficient at run time. That said, the solution I've proposed works and, for most situations, it's perfectly adequate.
The following code snippet shows the overridden Render method of the custom DataGrid control:
protected override void Render(HtmlTextWriter writer) { string markup = GetDefaultMarkup(); markup = FixMarkup(markup); writer.Write(markup); }
For more details, take a look at the code available for download at the link at the top of this article.
Under the Hood of the Table Rendering
To better understand the DataGrid's rendering machinery, it's useful to review how the DataGrid's implementation has changed after the hotfix discussed in Knowledge Base article 823030 ("FIX: DataGrid Made Compliant with Section 508 of the Rehabilitation Act Amendments of 1998"). In ASP.NET 1.1, the DataGrid's markup is not compliant with Section 508 of the Rehabilitation Act Amendments of 1998, a U.S. law regarding accessibility standards. To resolve this problem, Microsoft made a hotfix rollup package available, as explained in the aforementioned article. In short, the act states that data tables that contain two or more rows or columns must clearly identify row and column headers. This means that, at the very minimum, the header row(s) of an HTML table must define its cells through the TH tag instead of the ordinary TD tag.
This change has been implemented by slightly modifying the DataGrid's protected virtual method InitializeItem:
protected virtual void InitializeItem( DataGridItem item, DataGridColumn[] columns)
Any method marked as protected and virtual can be overridden in a custom control that inherits from the class. InitializeItem checks the type of the item being added to the grid; if the header is being initialized, it adds TableHeaderCell objects to the Cells collection instead of TableCell objects. The code in Figure 4 shows the method before and after the fix. TableHeaderCell inherits from TableCell and replaces the TD tag with TH:
public TableHeaderCell() : base(HtmlTextWriterTag.Th) { }
Figure 4 How the DataGrid Distinguishes TD and TH Cells
Original ASP.NET 1.1 Code
protected virtual void InitializeItem( DataGridItem item, DataGridColumn[] columns) { TableCellCollection cells = item.Cells; for (int i=0; i<columns.Length; i++) { TableCell cell = new TableCell(); columns[i].InitializeCell(cell, i, item.ItemType); cells.Add(cell); } }
Code Fixed as for KB823030 (Same Code in ASP.NET 2.0)
protected virtual void InitializeItem( DataGridItem item, DataGridColumn[] columns) { TableCellCollection cells = item.Cells; for (int i=0; i<columns.Length; i++) { TableCell cell; if ((item.ItemType == ListItemType.Header) && UseAccessibleHeader) { // Renders out as TH cell = new TableHeaderCell(); // Cell provides header information for its column cell.Attributes["scope"] = "col"; } else { cell = new TableCell(); } columns[i].InitializeCell(cell, i, item.ItemType); cells.Add(cell); } }
Unfortunately, InitializeItem can't be used to add extra wrapper tags like TBODY and THEAD. TBODY and similar tags don't simply replace a tag; rather they divide the contents of a table into logical sections. To correctly implement TBODY, THEAD, and TFOOT you must intervene in the process that builds a <table> out of the DataGrid contents. This would require you to rewrite the code that renders the Table class and add new members to the DataGrid to add section information to each added row!
The good news is that the rendering of tables changes significantly in ASP.NET 2.0. The TableRow class boasts a new property named TableSection to indicate which section the row belongs to. Possible values are defined in the TableRowSection enum: TableBody, TableHeader, TableFooter. When the Table control renders out to HTML, it checks the section of the row and inserts TBODY, THEAD, and TFOOT as appropriate.
The bad news, as far as ASP.NET 2.0 Beta 1 is concerned, is that neither the DataGrid nor the GridView reflect these infrastructure changes. In other words, DataGrid and GridView in Beta 1 don't output rich tables (those that include THEAD, TFOOT, and TBODY) but merely HTML tables (that have only TABLE, TR, TD). However, if you override the CreateItem method of both (protected virtual method), you can make them output THEAD, TFOOT, and TBODY. This was impossible with DataGrids in ASP.NET 1.1. By default, the CreateItem method creates and returns an instance of the control's row class—DataGridItem for DataGrid and GridViewRow for GridView. Both classes inherit from TableRow but neither sets the TableSection property accordingly. Figure 5 shows a quick fix for the GridView.
Figure 5 Set Table Section
protected override GridViewRow CreateRow( int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState) { GridViewRow row; row = base.CreateRow(rowIndex, dataSourceIndex, rowType, rowState); switch (rowType) { case DataControlRowType.Header: row.TableSection = TableRowSection.TableHeader; return row; ••• // also deal with pager rows } }
To sum it up, having TBODY, THEAD, and TFOOT tags in the grid output is not strictly required. However, these tags simplify the development of advanced and rich client-side functionalities to make the grid more interactive and pleasant to use.
That Darn PostBack
The new Mover.htc behavior, when applied to a new DataGrid control that exports grouping tags (the TableGrid control), allows you to present a table of rows that can be moved up and down, as in Figure 6. This would be just what's needed if it weren't for postbacks. When you refresh the sample page in Figure 6, all of your changes will be lost. Obviously you need to make client-side changes persistent across postbacks and, more importantly, expose the new order of rows to the server-side code.
Figure 6** Mover Behavior in Action on Sample Page **
The typical way for a server-side control to receive values from the client is through input fields, including hidden fields. Whenever an HTTP request is posted, the ASP.NET runtime matches the names of the posted fields to server-side controls. If a match is found, the runtime looks for the IPostBackDataHandler interface on the server-side control and invokes its methods. In light of this, the simplest solution you can implement is to create a hidden field with the same name as the grid control and make the control implement the IPostBackDataHandler interface. Let's modify the Render method of the TableGrid control to register a hidden field:
Page.RegisterHiddenField(ClientID, "");
The IPostBackDataHandler interface consists of two methods—LoadPostData and RaisePostDataChangedEvent, as you can see in Figure 7. LoadPostData is loaded to give the control a chance to update its state based on posted values. RaisePostDataChangedEvent, in turn, looks at the return value of the other method and fires a server-side event to notify of any occurred changes.
Figure 7 Implementing the IPostBackDataHandler
// ********************************************************************** // METHOD: LoadPostData // Process the posted values (non-empty if the order changed) public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) { // Skip if no values was posted (field empty) string postedRowsOrder = Convert.ToString(postCollection[postDataKey]); if (postedRowsOrder.Length==0) return false; // New order posted (will fire a server-side event) _rowsOrder = postedRowsOrder; return true; } // ********************************************************************** // METHOD: RaisePostDataChangedEvent // Fires the RowsOrderChanged server-side event public virtual void RaisePostDataChangedEvent() { // Fire a server-side event to let developers update the data source RowsOrderChangedEventArgs e = new RowsOrderChangedEventArgs(_rowsOrder); OnRowsOrderChanged(e); }
In this implementation of the interface, you check for the control's posted value, that is, what the behavior wrote in the hidden field. The contents of the hidden field will provide you with the user's activity on the client with the rows of the table. I'll return to this topic in a moment, but for now let's just assume that this content—a string—represents the order of rows as they are modified on the client. If the string is empty, then it means that the user didn't reorder any rows. In this case, LoadPostData returns false and no server event is ever fired. If a new order has been set, the contents of the hidden field are stored internally and passed as an argument to a custom server event—RowsOrderChanged. The page author will use this value to update the application's state accordingly. Note that it's the developer's responsibility to save the new order to the cache or to a database so that the grid will reflect the new order through ordinary data binding.
In Figure 7, RaisePostDataChangedEvent triggers a custom event defined in the following manner:
public delegate void RowsOrderChangedEventHandler( object sender, RowsOrderChangedEventArgs e); public event RowsOrderChangedEventHandler RowsOrderChanged;
The RowsOrderChangedEventArgs data structure defines the arguments being passed to the page's event handler. The class extends EventArgs by adding an array of integers:
public class RowsOrderChangedEventArgs : EventArgs { public int[] RowsOrder; ••• }
Why is it so? The structure of this class depends mostly on the format chosen to stream data to the hidden field. To close the circle, some changes to the behavior are in order. Whenever a row is moved up or down, the resulting order should be persisted to the hidden field. Each row must be uniquely identified through an invariant ID that represents the original position. This ID is dynamically assigned when the behavior loads up. Each displayed body row gets a progressive number:
for(i=0; i<bodyRowCount; i++) { var nnm = tBody.rows[i].attributes; var namedItem = document.createAttribute("pos"); namedItem.value = i.toString(); nnm.setNamedItem(namedItem); }
When the row has been moved, these IDs are composed to a pipe-separated string to reflect the new order. For example, if you move row #2 one position up in a table that contains three rows, the strings written to the hidden field looks like "2|1|3", reflecting the fact that the second row is now the first. Figure 8 illustrates the code to stream the row order to the hidden field.
Figure 8 Persisting Changes to the Hidden Field
//+---------------------------------------------------------------------- // Function: SaveChanges // Description: Serializes the new order of rows to the hidden field. // This function is called from within MoveUp and // MoveDown // Arguments: none // Returns: nothing //----------------------------------------------------------------------- function SaveChanges() { // Loop through the body rows and create a pipe-separated string var chrSep = "|"; var sTmp = ""; for(i=0; i<bodyRowCount; i++) { sTmp += tBody.rows[i].attributes["pos"].value; if (i<bodyRowCount-1) sTmp += chrSep; } // Save the string down to the hidden field (same name as the element) // Go up in the hierarchy until the FORM is found var obj = element.parentElement; while (obj.tagName.toLowerCase() != "form") { obj = obj.parentElement; if (obj == null) return; } var hiddenField = element.id; // same ID of the server-side control if (hiddenField != null) obj[hiddenField].value = sTmp; }
A similar string is passed to the server with each postback and is received by the LoadPostData method. From here, the string makes its way to RaisePostDataChangedEvent and then to the constructor of the RowsOrderChangedEventArgs class, as in Figure 7. The pipe-separated string is then split to an array of integers and served to the code in charge of handling the RowsOrderChanged event.
This code takes the responsibility of persisting the row order as required by the application. You can, for instance, store the new order to a database or to an object in the ASP.NET cache. The sample application updates the cache entry from where the binding mechanism retrieves the data to bind to the grid:
private void BindData() { DataTable table = (DataTable)Cache["MyData"]; grid.DataSource = table; grid.DataBind(); }
How can you apply the new row order to a DataTable object? This is mostly up to you; however, the approach I've used here entails cloning the existing DataTable object and filling it with the same rows bound in the order suggested by the integer array as shown in Figure 9.
Figure 9 Applying Row Changes to the Bound Data Source
private void grid_RowsOrderChanged(object sender, Msdn.RowsOrderChangedEventArgs e) { // e.RowsOrder is an int-array with the new order // Update the data in the cache and rebinds int [] newPositions = e.RowsOrder; DataTable table = (DataTable) Cache["MyData"]; DataTable newTable = table.Clone(); for(int i=0; i<newPositions.Length; i++) { int posToImport = newPositions[i]; DataRow oldRow = table.Rows[posToImport]; newTable.ImportRow(oldRow); } table.Rows.Clear(); table.Columns.Clear(); table.Dispose(); Cache["MyData"] = newTable; }
ASP.NET 2.0 Considerations
The sample code that accompanies this column is for ASP.NET 1.1 and requires that you make available the Mover.htc file along with the assembly that contains the TableGrid control. In the context of a real-world application this is a non-issue. However, the fact that Mover.htc is just a small text file doesn't prevent you from researching a better approach to wrapping everything up. This is an instance of a more general problem—packaging resources in ASP.NET assemblies. In ASP.NET 1.1, the problem can be addressed through a custom HTTP handler that retrieves and downloads the resource, setting the proper MIME type. Using an HTTP handler requires you to enter a new line in the Web.config file and likely a new entry in the IIS metabase.
In ASP.NET 2.0, a new system HTTP handler (named WebResource.axd) and the WebResource attribute let you retrieve resources from assemblies. WebResource is an assembly-level attribute through which you mark embedded resources as URL-accessible. You first add the Mover.htc file to the project as an embedded resource and then insert the following code to the AssemblyInfo file of the project:
[assembly: WebResource("Mover.htc", "text/javascript")]
To programmatically access the resource (that is, to compile the Style property of the TableGrid control) you use a new method on the Page class—the GetWebResourceUrl method.
In this example, I used a DataGrid-based control, the TableGrid control, which is probably overkill if all that you need is to let users reorder some data-bound rows. The DataGrid, in fact, contains paging, sorting, and editing capabilities that are of no use in this scenario. The rub lies in the fact that in ASP.NET 1.1, building a data-bound table-like control is significantly harder than you may think at first. For this reason, many developers just end up with a customized version of the DataGrid control. Fortunately, this aspect of Web programming is going to change with ASP.NET 2.0.
In ASP.NET 2.0, creating custom data-bound controls is just as easy as picking up the right base class—DataBoundControl, CompositeDataBoundControl, BaseDataBoundControl, or HierarchicalDataBoundControl. These base classes incorporate most of the boring and repetitive boilerplate code (and best practices) you had to learn and manually code in ASP.NET 1.1. What's left out of these extremely useful and well-designed classes is exposed through virtual methods. Finally, creating custom collections for binding and advanced properties is child's play thanks to generics. In ASP.NET 2.0 you don't have to rack your brains to work with data-bound controls.
Note on the Sample Code
When I first tested the MoveGridRows.aspx sample page, I noticed it was suffering from a common software disease—page refresh-itis. Imagine you move row #2 one position up and post values to the server. The grid is updated and redisplayed bound to a new data object that reflects the new order. Looks great. Now try hitting F5 or refresh the page from the toolbar. The most recent request is reissued and the last move (row #2 moved one position up) is repeated. A second, and likely unexpected, movement is performed. This may, or may not, be a problem for you. If it is, though, you're experiencing page refresh-itis.
In a recent article in the ASP.NET Developer Center, I discussed an HTTP module and a new Page class that inform you if the postback results from a regular submit or a browser's page refresh. Once you have downloaded the code, make the codebehind class of the ASPX page inherit from Msdn.Page, and use the IsPageRefresh property to check against page refreshes. In addition, enable the RefreshModule HTTP module in the Web.config file.
Send your questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and his newest book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.