Cutting Edge: Building Editing Capabilities into the SqlDataNavigator ASP.NET Control
.gif)
n last month's column I began an ambitious project: building a SQL Server™-specific DataNavigator control that supports two-way data binding. The control I'll present in this column, SqlDataNavigator, is just an extension of last month's DataNavigator. The SqlDataNavigator ASP.NET control described here is meant to be the Microsoft® .NET counterpart of the Data control—an old Visual Basic® control that caused its share of headaches. The control moves from one record to the next according to a given order and displays each data row using a dynamically generated template. Last month I focused on the DataNavigator control's architecture and tackled some programming issues related to connectivity and data display. This month, I'll add editing capabilities to the control, making SqlDataNavigator actually support the "writing" channel of .NET data binding. Paging Record by Record The SqlDataNavigator control has a pager bar, similar to the MoveNext and MovePrevious methods exposed by the Data control, that you can use to move through records sequentially. There's a significant difference between the Data control, which was tailored for the ADO Recordset object, and the SqlDataNavigator control, which has been designed with ADO.NET and ASP.NET in mind. The SqlDataNavigator control (see Figure 1) has no underlying open connection or server-side cursor to easily move you to the next record. Right now, ADO.NET does not support server cursors. In addition, in this implementation of the SqlDataNavigator control, I have deliberately chosen to build all the logic necessary for data access into the control class. What I need is a flexible tool that can quickly set up an attractive, efficient user interface for SQL Server tables. Furthermore, once full support for two-way data binding has been implemented, you'll have a rather powerful tool for creating edit interfaces for virtually any SQL Server table. But how does the control get the current record, and what kind of logic moves it from one record to the next?
Setting the SearchKeyField property allows you to specify the field by which to order. You also need to indicate a primary key field to make sure that records with duplicates in the sort field are not mistaken for one another. Since the SqlDataNavigator control works with a page size of 1, the following T-SQL statements retrieves the record in the fifth position, sorting by lastname:
Notice that there are items in the code that represent parametric information such as the table name, sorting field, the key, and the record number to retrieve. If you need to retrieve the record in position N with a page size of 1, then you must discard the first N-1 records. Unfortunately, there's no way to write a command like this using SQL parameters. The reason is that the TOP and the ORDER BY clauses do not accept variable parameters. So I resorted to the following code for formatting placeholders:
The SQL Server-based .NET data provider deals with this code, and any other T-SQL code, in a relatively efficient manner. The SQL code is transmitted through the sp_executesql system procedure. For programming ease, you might want to consider using format placeholders in your code. Adapting the Control's Structure At the foundation of the SqlDataNavigator user interface there is a highly customized DataGrid Web control. The DataGrid is already predisposed toward in-place editing, so making this feature show off the SqlDataNavigator control should not be really hard. To set a DataGrid to edit mode you normally add a special breed of column—the EditCommandColumn column type—and handle the events it fires upon clicking. The EditCommandColumn object allows you to specify the text for the links that will edit the row and then save or cancel any changes. Note that you don't strictly need such a column to set a grid to edit mode. What really matters is that you run a piece of code that properly sets the DataGrid's EditItemIndex property. Edit ImplementationThe structure of the handler that takes the edit command is pretty straightforward. The code sets up the control's interface to reflect the new working mode and then orders a data refresh:
The feasible working modes for the control are defined in a custom enum object called WorkingMode:
When the control is not in view mode (in other words, it's editing or inserting), the button bar is hidden from view to avoid abruptly halting ongoing operations. At the same time, the EditItemIndex must be 0 (which refers to the first item in the grid page) and the EditCommandColumn must be visible:
Figure 5 shows the SqlDataNavigator control while editing a record. As you can see, the button bar is hidden while the EditCommandColumn is visible. This column features the Save and the Cancel buttons. Clicking on either of these two buttons would fire the standard pair of events—UpdateCommand and CancelCommand—for you to persist or cancel changes.
The class DataNavigatorEditItemTemplate inherits from ITemplate and binds to the data using editable controls like textboxes, dropdown lists, and checkboxes. In Figure 6 you see the outline of the code for the template class. The edit template creates a placeholder control and then handles its DataBinding event. When the placeholder gets bound, the class dynamically creates and renders an HTML table. The procedure looks similar to what happens for display, but with a few significant differences.
Prior to refreshing the grid, you make sure that the template classes (both item and edit item templates) have been filled with all the configuration information they need:
Carrying schema information in the body of the edit template class makes it easy for you to implement some cool features such as marking the field as required if it does not accept nulls, preventing changes on read-only fields, or using multiline controls if the text or the column size can exceed a certain length. In Figure 5 you can see some of these features in action. For example, asterisks mark fields where nulls are not allowed and the employeeid field, which is an identity column, is disabled. Creating a Column Binding ContextTo improve the user's edit experience, you might want to configure each column individually. For example, you may want to pick up the value for that column from a lookup table or render a certain piece of content as a Boolean value. In such cases, the textbox is no longer the most suitable control for editing. You might also want to keep fields as read-only in your application or format them in a special way. For this purpose, I created a new data structure called DataBoundField (see Figure 7). This class describes how a field should be rendered for display and edit. The class represents the binding context for the column and indirectly adds a great deal of flexibility to the overall interface of the SqlDataNavigator control. For example, you can control the label text and the tooltip of the field and decide whether you want it to be displayed with a dropdown list or a checkbox. (More in a moment.) The SqlDataNavigator control exposes a DataBindings property that is an instance of the ListDictionary class:
The contents of the property is persisted across multiple page requests. ASP.NET does not know, though, how to serialize the contents of the DataBoundField. If you want to be served the default way, just mark the class with the [Serializable] attribute. Beware, though, that this approach is not necessarily optimal and could lead to too much code being persisted. Check the MSDN documentation to explore alternative approaches, such as writing a type converter for the class.
First, you create a new DataBoundField object and set some of its properties. Then, add the object to the DataBindings collection using a key value that matches the field name. The control internally locates the item using the Contains method of the ListDictionary class and passes the name the column just read off the schema in a string. Since the Contains method is case-sensitive, you must pay attention to how you write the column name. A better approach would be to derive a custom dictionary object from DictionaryBase and make it work irrespective of the key case.
The alternate class constructor defines a lookup table for the field. You specify the name of the field to be mapped, the lookup table, and the fields to use to populate the dropdown list control for text and value. There's a bit of redundancy here as the field name appears both as the key of the collection item and as the BoundField member of the DataBoundField object. Figure 9 shows the user interface of the record being edited in this way; Figure 10 shows the ad hoc formatting when in view mode.
If the data to render lends itself to representation as a binary type of information (yes/no, on/off, true/false), you can use a checkbox control instead of textboxes or lists. For example, in the Employees table, the ReportsTo column contains the ID of the boss. The column allows for nulls, meaning that the given employee does not report to anyone. Although not strictly Boolean, this piece of information can be adapted to display through a checkbox that answers the question: does he or she report to anyone? If the value of the column is greater than 0, the employee has a boss; otherwise, he or she does not report to anyone.
In view mode, the SqlDataNavigator control shows Yes/No text. In edit mode you have a checkbox whose text is always the true string. The value of the column is converted to a Boolean and the result determines whether or not the checkbox is checked. The control's code also ensures that any null values encountered are rendered as false:
However, this is not necessarily a good approach and potentially leads to some data inconsistency. The SqlDataNavigator assumes that you use a checkbox-based representation of the data either if you have truly Boolean data or if you want to abstract over the data. In the latter case, though, you won't allow for editing. In situations in which you must handle buttons with three possible states (true, false, or nothing) you are better off adding a radio button list rather than using checkboxes. Handling Null Values In effect, the optional presence of null values in some fields poses a few design issues that can be summarized in the following question: how do you let users set null values? In the SqlDataNavigator control I assume that if you edit through textboxes, you are going to enter non-empty strings. So if the textbox turns out to be empty at save time, the control sets that column to null. Columns rendered as checkboxes handle the null value as false, but what about dropdown lists? In this case, the control gets slightly smarter and recognizes the nullity as a special case. For example, the ReportsTo column contains the ID of the boss or null. In view mode, employees without bosses can simply be rendered with an empty label. What happens if you need to update the ReportsTo field to hold the value null?
If the field accepts nulls then you create a new row and add it to the source table of the dropdown list. To enhance the user interface, you set the Text property of the list item with any text that means NULL. Figure 12 shows how gracefully the SqlDataNavigator control handles the contents of the ReportsTo column. Inserting and Deleting RecordsThe edit mechanism is by far the most complex part of the navigator, and it is the core engine of the editing capabilities of SqlDataNavigator. When you click to insert a new record, the control switches in insert mode and refreshes the grid. There are only a few minor differences between the edit and the insert mode:
Actually, both the Insert and the Edit buttons trigger the same engine—the in-place editing feature of the underlying DataGrid. The idea is that the insertion acts as the update of the record currently displayed. When it comes to this, the SqlDataNavigator control detects the insert mode and adapts the user interface. For example, it clears out all the textboxes, sets a few of them to default values—the DefaultValue field of the DataBoundField class—and, more importantly, runs a different procedure to save the data. Figure 13 shows the typical insertion mask with some default values and automatic handling of auto-increment columns.
Upon creation of the Delete button, you just add an onclick item to the button's Attributes collection. Finalizing the UpdateWhen the user chooses to save the changes he has made, he clicks on the Save button and the following code performs a number of possible operations:
Depending on the working mode, the control updates the current record or inserts a new one. Both the INSERT and the UPDATE statements are built by concatenating text into a StringBuilder object. The values are extracted from the page using the Page.Request.Form collection and the textbox control's unique ID. If you snoop through the source code, you see that a lot of facilities such as the automatic duplication of single quotes are already implemented. |
||||
Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Building Web Solutions with ASP.NET and ADO.NET (Microsoft Press, 2002), he now spends most of his time teaching classes on ASP.NET and ADO.NET for Wintellect (https://www.wintellect.com). Get in touch with Dino at dinoe@wintellect.com. From the May 2002 issue of MSDN Magazine |
.gif)
.gif)
.gif)
.gif)
.gif)
.gif)