|Server-side ASP.NET Data Binding, Part 3: Interactive DataGrids|
| Download the code for this article: Cutting0105.exe (42KB)|
Browse the code for this article at Code Center: ASP.NET Data Binding 3
For a DataGrid component, interactivity usually means the ability to select one or more rows, edit, and apply the changes to whatever storage device is in use. Unless you're going to use custom ActiveX® controls or Java-language applets, DHTML is an absolute must. Of course, using DHTML reduces the size of the potential audience somewhat. Selection and in-place editing, when accomplished using plain old HTML and script, requires that the page be refreshed to reflect edits and updates.
The typical workaround is to deliver two versions of the application. One version is targeted at Microsoft® Internet Explorer 4.0 and higher using the power of DHTML and scriptlets. The other is targeted at HTML 3.2-compliant browsers and is based on a mechanism that detects the click on a table row (a <TR> tag), then moves to another page. This page, in turn, displays the selected row in a different color or provides a form whose input fields have already been initialized with the various cell valuesthe <TD> tags. Keep in mind this is a simple but effective schema because it closely maps to what ASP.NET does with significant help from the postback events mechanism.
In this column, I'll first provide the solution using scriptlets and Internet Explorer behaviors. Next, I'll move to ASP.NET and review the DataGrid capabilities for selection and in-place editing.
DHTML Interactive GridsDHTML lets you easily modify your page layout to reflect interactive events such as selection and editing. Once you have a table of records displayed, selection is as easy as tracking down the index of the currently selected row and modifying its style after a click event, as you'll see in the following code:
The previous snippet comes from the demo scriptlet you'll find in this month's source code (see the link at the top of this article).
Basically, this code intercepts any click on any table row. (All rows in the table have been given the same IDnamely, tableRow.) When there is a click event, the script runs and it calls into the SelectRow function, which simply modifies some CSS styles to reflect the new state. A global variable called m_selectedRow holds the currently selected row as a DHTML object. Within the body of SelectRow, the first action taken is to deselect the current row. This is accomplished by changing the name of the CSS class associated with the underlying <TR> tag. Next, the variable is updated to contain the newly selected row, whose style is changed to that of a selected item.
This code is pretty simple, yet it requires DHTML or at least special browser support for dynamic changes to CSS styles. The script code you need is plain client-side script that I've embedded in a scriptlet for better reusability. To read the content of the clicked row, use the innerHTML property of the object it represents:
So much for selection in a table. Editing, though, is a different story. First of all, you must figure out a way to prompt the user with the current content of the row. Next, you must give them a chance to modify it and store all the changes. No matter which technique you use to bring the content of the table to the client (XML data islands, hidden fields, hidden elements), you will most likely need to go to the server to finalize any editing that the user performed. The only exception to this is when you're using client-side persistence through COM objects or the Internet Explorer 5.0 persistence behavior. However, this is almost never the case on the Internet.
In creating interactive DHTML grids, you can design a compelling user interface with dialog boxes that pop up or unroll from the edges, but while they may look slick, they still require round-trips to the server. Figure 1 shows the scriptlet in action.
Figure 1 The Scriptlet in Action
ASP.NET has been specifically designed to make the interaction between users and pages easy to code and portable across browsers. If you don't use DHTML, more round-trips to the server will be necessary for updating pages. The DataGrid server-side control and the column templates I explored last month have much more to offerin particular, the selectable style and edit template for rows.
Selecting Items in DataGridAs explained earlier, a selected item is just an ordinary item rendered with a different style or layout. To enable item selection in a DataGrid, you must provide two parameters: the action that fires the selection process and the new style for a selected element. In turn, the DataGrid raises a specific event, SelectedIndexChanged, whenever the selected item changes.
As you know, a user selects items by clicking them. Normally, they're allowed to click anywhere on the row of interest. However, the DataGrid control also gives you a chance to narrow the area that can be clicked to select a row. For example, you could add a new column to the table whose only purpose is selecting the parent row.
Select is a special command name that the DataGrid control knows how to handle. Whenever a user clicks on a button in a grid item, the ItemCommand event is fired. If the command name is Select, then the following action takes place. The DataGrid determines which item was clicked, updates the value of the DataGrid's SelectedIndex property accordingly, and changes the style of the selected row to that of the SelectedItemStyle property. If a different row was previously selected, it is reassigned the default style. Notice that all this takes place automatically. All you have to do is add a button column, as shown previously, and specify a selected style:
Figure 2 shows the final result of this style setting.
Figure 2 Selected Style
To improve the interface, you could replace the text of the button with an image, like so:
Figure 3 shows the new table.
Figure 3 Displaying a Button for Select
There are several reasons to write a custom handler for the SelectedIndexChanged event. For example, you might want to switch to a new image when the row is selected. A more likely scenario is that you might want to refresh some other parts of the overall user interface. Let's see how to do this.
To start out, add a handler for the SelectedIndexChanged event:
The handler takes the form of:
The SelectedIndex property has the read/write attribute. To change the selection state of a given row, you can assign its zero-based index to the property. If you define a SELECT command, then there's no need for you to manually set the property. In fact, the DataGrid updates it internally within the code that handles that standard command. In the SelectionIndexChanged event you can just take it for granted and utilize the new value. For example,
Figure 4 shows the final result.
Figure 4 New Selection Displayed
Notice that the selected index doesn't contain page information. For example, suppose that you select row 2 on page 1 and then move to page 2 of the grid. What you would expect to see is no row selected. Instead, the DataGrid still shows row 2 selected. However, if you access the selected item through the SelectedItem property, which returns a DataGridItem object, the content correctly refers to the row on the previous page.
This behavior is by design. The grid's selected index doesn't change automatically in case the code in the PageIndexChanged event handler pages conditionally. For example, suppose that someone is editing the selected row. In this case, you may want to prompt the user to commit the changes before navigating to the next page.
Figure 5 Selected Items Don't Match
If not properly handled, this behavior could lead to the side effect shown in Figure 5. In the right screen, the item that is graphically selected doesn't match the information on the status barthis information comes from the SelectedItem property. To work around this, set the SelectedIndex to -1 in case your code navigates to the next page.
The Selected ItemSelectedItem is a read-only property that returns the currently selected DataGrid's row as an object of type DataGridItem. Through this method you can retrieve the actual content of the row, then use it at your convenience. Typically, you would use it in the SelectedIndexChanged event to refresh the text shown on the status bar.
The grid shown in Figure 5 uses the code in Figure 6 to refresh the user interface once the SELECT command has been issued. Each grid row is represented by a DataGridItem which, in turn, is a collection of TableCell objectsone for each cell of the resulting HTML table. The code walks through the Controls collection of the row to read the contents one item at a time. Once you access the cell with the image, you can change the picture to reflect the modified state of the row.
As mentioned earlier, SelectedIndex is a read/write property. Thus, you could set it manually to change the selected row. If you associate the following code with the OnClick event of a button and define a textbox called RowNumber, you can select the specified row using a zero-based index.
In this case, the row is properly selected and the specific CSS style is applied correctly. Surprisingly, however, the SelectedIndexChanged event is never fired. As a result, the code you wrote to refresh the user interface according to the selected row never runs. This also occurs by design. The SelectedIndexChanged event fires only in response to user interaction with the page.
A simple and effective workaround is making the code fall directly into the event handler. To do so, just add the following line to the SelectRow procedure shown in the previous snippet.
To deselect the currently selected row, assign a value of -1 to the SelectedIndex property. However, be very careful with your SelectionIndexChanged event handler. When you deselect, you cannot expect to find a non-null value for the SelectedItem property. One thing you can do is wrap all the code that refreshes the user interface in a try/catch block:
If you are trying to apply some special formatting only to the selected item, you can also handle the ItemCreated event and test that the new item's type is equal to:
At this time, the selection mechanism of the DataGrid doesn't support multi-row selection. Adding this feature wouldn't be particularly complex in terms of the control's programming interface. In fact, it would be sufficient to transform the SelectedIndex property in a collection and add a couple of more specific methods. However, in terms of the internal rendering capability, things aren't that simple. A DataGrid that allows multiselection would be an extremely cool and useful custom control to develop, but it requires a great deal of extra work, even with .NET inheritance.
You've probably noticed that selecting a row in a DataGrid is possible as long as you click on a particular cell. A few years ago, Microsoft introduced the ListView control, which featured a similar behavior. But because it was impossible to select an item clicking anywhere along the row, Microsoft soon provided a new style to enable the full row select. Is it possible to enable the same behavior with DataGrids? No, not unless you create your own. All the events you can handle assume that you're clicking on a button column. A normal text column is not sensitive to mouse activity.
A possible workaround is to create a DataGrid in which all the columns are button columns. In this case, you can take two approaches to handle the event. You can either assign the same command name to all columns or define a handler for the OnItemCommand event. This event fires whenever the user clicks on any button column. Notice, though, that this event reaches your code before the more specific column-based commands are processed. If you plan to handle the ItemCommand event, bear in mind the signature of the method:
The Item property of the DataGridCommandEventArgs points to the DataGridItem you clicked on. To get the index of the selected row, use the DataGridItem's ItemIndex property.
In-place EditingIf you're planning to use a DataGrid control in your applications, chances are that what you really need is an editable DataGrid that works in much the same way as a Microsoft Excel worksheet. So it comes as no surprise that the ASP.NET DataGrid control provides a hook to insert pieces of code specifically designed to allow in-place editing. The key elements in this context are: a button column which handles the editing command bar; one or more cells marked as read/write; and three procedures to handle basic events such as begin edit, update, and cancel edit.
Almost everything you need is already built into or easily obtained from the DataGrid control. However, the DataGrid doesn't provide for physically updating the underlying data source, either directly or indirectly. I'll have more to say on this point later. For now, let's concentrate on how to modify the grid to support in-place editing.
First, add three event handlers to manage the basic operations.
Second, insert an edit button column somewhere in the grid. This column will serve the same function as the select button for selection. I would make this column the first one in the list. I'd make it second if selection is already supported. However, you can define it as the rightmost column or even insert it in the middle of the grid; it's up to you.
An edit column is rendered through an object of class EditCommandColumn. It features a number of properties and methods, but you really need to be familiar with EditText, CancelText, and UpdateText. These properties store the HTML text that the DataGrid will display to signal where to click to begin editing, cancel, or save changes. Like the select button column, you can use images instead of plain text by simply assigning the HTML text that displays the image you want. Notice that images will be surrounded by frames unless you add a border=0 attribute to the IMG tag. Other attributes that I have found to be particularly useful are align=absmiddle and alt="some text", which gives the button a tooltip.
When you use an EditCommandColumn (or any other button column), you create a button column whose items are link buttons. In other words, you click on a hyperlink to fire a certain function. You control the style of the button through the ButtonType property. It offers a very limited range of choices: LinkButton or PushButton. If you use code like this
the buttons will have a 3D appearance. In this case, you cannot use HTML text. If you assign the EditText property text like the following, then the text will be used as is to render the button's caption.
Handling EventsFigure 7 shows the minimal code you need to start playing with Web-editable grids. The DataGrid's EditItemIndex indicates the item you're currently editing. When it's set to -1, there's no item being edited. Setting EditItemText to -1 also cancels any editing in progress. All the handlers take a DataGridCommandEventArgs argument. To find out the index of the item being edited, you can call the ItemIndex property of the DataGridItem object, which is accessed through Item. Hence, to begin editing a row, use the following code:
ItemIndex is an index relative to the page being displayed and contains no information about the absolute position of the row in the dataset. Should you need to get an object reference to the selected item, use the Item.Cells or Item.Controls collection. These collections contain slightly different information and shouldn't be used interchangeably. Item.Cells contains all the TableCell objects that form the grid row. (Bear in mind that a grid row is rendered as a <TR>.) Item.Controls, on the other hand, includes all the controls found in the various cells. In other words, using Cells you can be sure you're targeting the second TableCell object. However, using Controls, you can't be sure because it depends on the template used to render the various cells of the grid.
To make sure that everything works fine, you must modify the code I showed in Figure 7 to include rebinding. (In fact, all the event handlers you write must contain the instructions needed to rebind to the data source.)
When you just begin the editing, you only have to change the layout of one row. Rebinding is an absolute necessity since it is the process that causes the DataGrid to be repopulated and redrawn.
With in-place editing, however, there is a modification of the data source. This means that at least for UpdateCommand you need to reread data from storage. For UpdateCommand you need to retrieve the new values for the columns. For EditCommand and CancelCommand, you don't strictly need to reread data from the data source, so the call to DataBind could be hidden. However, my understanding is that they made it mandatory in order to provide a more consistent and flexible programming interface.
When you begin editing, the grid presents a textbox instead of the label that normally shows the field text. The textbox is initialized with the same text previously shown by the label. As you can see, this step is just a matter of rerendering the table. Next, you start working with the textbox. When it comes to saving the changes, you're responsible for retrieving all the values from all the fields you need and figuring out how to send them to the data source. Then you need to reflect the changes. Here's the typical format for the UpdateCommand handler:
The Update command is trickier than it might appear. All the DataGrid does is replace label controls with input boxes. You are responsible for extracting the right pieces of information from the textboxes to update the data source. In Figure 8 you can see how in-place editing works on the previous grid. The second field (Name) doesn't map to any one database field. Instead, its value is a string obtained from three different fields: TitleOfCourtesy, FirstName, and LastName. What if you want to edit these fields separately? You have two options here. Either you assume you can extract the various chunks of information from a single input text, or you modify the template for editing this column (more on this later). In both cases, you need nontrivial and application-specific code in your UpdateCommand handler. To make sure the grid is always refreshed properly, you must recreate the dataset and assign it to the DataSource property.
Figure 8 In-place Editing
Canceling the edit operation also means that you must restore the previous values. Here, a call to DataBind is sufficient because the original values are still stored in the DataGrid's associated dataset. Figure 9 shows the script code for the pages that I've shown so far.
The DataGrid's LayoutSo, to enable in-place editing you must implement a special column. This column normally shows the HTML text you specified through the EditText property. When you click on this link, the DataGrid enters editing mode. The content of the column changes and becomes two links, one for saving the changes (UpdateText) and one for canceling the whole operation (CancelText).
You can control which of the columns must be editable. By default, all of them are subject to editing unless you set the column's Readonly attribute to True. For example, in Figure 8 the Employee ID field is not modifiable because of this column declaration:
Also, EditItemIndex has the same behavior as SelectedIndex. In other words, when the index you select is 1, you get the second row in the grid because the index of the first row is zero (see Figure 10).
Figure 10 Index Doesn't Match Row Number
Updating the Data SourceThe most important thing to do within the UpdateCommand handler is save your sensitive data to the underlying data source. The DataGrid's DataSource property refers to an object that descends from ICollection. Normally, it is one view of one of the DataTable objects stored in an ADO.NET DataSet object.
The UpdateCommand handler must retrieve this collection of rows and then use the programming interface of DataTable objects to apply changes. At this point, though, data has been updated only in memory. To go back to the server and physically modify the data source, you must use the facilities that ADO.NET provides, especially the Update method of the SQLDataSetCommand. Notice that if you're targeting any OLE DB provider other than Microsoft SQL Server, you should use ADODataSetCommand. Note that these classes are subject to renaming in beta 2 of ASP.NET.
The Update method takes a DataSet and submits its content to the data source. It normally uses autogenerated SQL statements to cope with INSERT, DELETE, and UPDATE changes in the dataset. However, the DataSetCommand objects expose three properties (InsertCommand, UpdateCommand, and DeleteCommand) that you can use to decide which statements must be used to apply changes to the data source.
Changing the Column TemplateWhat I've covered only scratches the surface of DataGrids. The schema analyzed for in-place editing works great only if you have a one-to-one correspondence between data fields and columns and as long as the editing is simple. But what if you need to validate the values entered? You could always rely on the UpdateCommand code, even though this approach is neither flexible nor particularly powerful. Even worse, this check is possible only when you attempt to apply the changes; it doesn't allow you to reject the data and continue editing. You can avoid saving the data, but then the user must explicitly click again to start another editing operation. What if you want to limit the values to those listed in a combobox? What if you want to use a checkbox for Boolean values?
It goes without saying that in real-world scenarios it's necessary to change the column template to make sure you use the controls you really need: validators, comboboxes, checkboxes, and all the accessories (labels, images, tooltips) that can make the editing process easier.
Don't worry! This is exactly what I'm going to cover next month.
Send questions and comments for Dino to email@example.com.
| Dino Esposito is a trainer and consultant based in Rome, Italy. Author of several books for Wrox Press, he now spends most of his time teaching classes on ASP.NET and ADO.NET for Wintellect (http://www.wintellect.com). Get in touch with Dino at firstname.lastname@example.org. |
From the May 2001 issue of MSDN Magazine