It s fairly common for Web applications to work on a single record at a time. For example, displaying the contents of a single record of data is a necessary practice when you build master/detail views. In ASP.NET 2.0, the DetailsView control renders a single record at a time from its associated data source, optionally providing paging buttons to navigate between records and Create/Read/Update/Delete (CRUD) capabilities. You can customize the user interface of the DetailsView control to a large extent. You can choose the most appropriate combination of data fields to render each piece of information conveniently. The default set of data fields includes templated, image checkboxes, and hyperlink fields. For more information on the DetailsView control, see "Getting Under the DetailViews Control. "

The DetailsView control is not limited to exposing its data to users in an appropriate and friendly way. The control also provides interactive buttons to navigate data and edit and insert new records. These features are provided out-of-the-box and are not configurable by programmers. Or, at least, not declaratively and, maybe, not easily. In this column, I ll first answer a specific question posed by a reader, then move on to putting the solution in perspective and extending other features of the DetailsView control.

 

The Problem

Let s say you need a Web page that collects data and populates a database. By using the DetailsView control you save yourself a bunch of user interface stuff. The control provides a free table-based interface with one column for labels and one column for input fields. You set the working mode of the DetailsView to Insert and go:

<asp:DetailsView ID="DetailsView1" runat="server"

   AutoGenerateInsertButton="true"

   DefaultMode="Insert"

   DataSourceID="SqlDataSource1">

</asp:DetailsView>

Figure 1 shows the output you get. Filling out the form and inserting a record into the database works well. Next, you can use a GridView control on another, or the same, page to retrieve any just-inserted records. As you can see in Figure 1, input fields are empty and don t suggest any values to users. As the reader pointed out, this is going to be a problem if you have to enter the same subset of data for, say, fifteen records. Looking at Figure 1, wouldn t it be reasonable to automatically set the title field with Mr. or whatever else is the most likely value? Especially for pages used for data entry, this little trick might greatly improve the user s experience.


Figure 1: The DetailsView control working in insert mode.

The DetailsView control lacks a property to indicate the default value of an input field. So how would you do that? You might think that the DetailsView control fires an event that, properly handled, gives you a chance to set the default value of input fields. This is not entirely correct. More precisely, the DetailsView control doesn t fire a specific event that would let you do so. However, in the long list of events fired by the DetailsView control, there s one that you can leverage to implement a little piece of logic to set default input values.

 

The ItemCreated Event

The DetailsView control exposes several events that enable the developer to execute custom code at various times in the lifecycle. The table in Figure 2 details the supported events.

Event

Description

ItemCommand

Occurs when any buttons in the user interface are clicked.

ItemCreated

Occurs whenever a new DetailsView item is created.

ItemDeleting, ItemDeleted

Events fire before and after the record is deleted, respectively.

ItemInserting, ItemInserted

Events fire before and after the record is inserted, respectively.

ItemUpdating, ItemUpdated

Events fire before and after the record is updated, respectively.

ModeChanging, ModeChanged

Events fire before and after the working mode of the control changes, respectively.

PageIndexChanging, PageIndexChanged

Events fire before and after a new record is displayed, respectively.

Figure 2: Events of the DetailsView control.

It is interesting to note that the ItemCommand event doesn t fire if the user clicks on the Edit or Insert buttons or any other standard buttons. Clicking these buttons is handled internally to ensure the expected behavior. So the ItemCommand event, although promising, is not the right handle. Reading through Figure 2, it s easy to see that another event can be taken into account the ItemCreated event.

The ItemCreated event is fired immediately after a DetailsView item is created. But what s a DetailsView item? They are header, footer, command row, and page bar. All data rows are considered a single item. In other words, you won t receive an ItemCreated event for each field of the displayed record, but only one when all labels and input fields have been created. Let s consider the following code snippet:

protected void DetailsView1_ItemCreated(object sender,

 EventArgs e)

{

  if (DetailsView1.FooterRow == null)

      return;

   :

}

The FooterRow property returns an object that represents the footer of the DetailsView. The footer is the item that gets generated after all data rows. When FooterRow is not null, you can be sure that all data rows exist and, as such, can be set to custom values. To set default values only when in insert mode, you modify the preceding code as follows:

protected void DetailsView1_ItemCreated(object sender,

 EventArgs e)

{

   if (DetailsView1.FooterRow == null)

       return;

   if (DetailsView1.CurrentMode == DetailsViewMode.Insert)

   {

       SetDefaultValue(3, "USA");

       SetDefaultValue(4, "Mr.");

   }

   :

}

How can you retrieve the control instance of the textboxes you see in Figure 1? That s a bit tricky. To start off, you must know the index of the field you want to set. In Figure 1, the title field is the fifth row in the table and has an index (0-based) of 4. The Rows property of the DetailsView control returns a collection of DetailsViewRow objects, each of which represents a data row in the control. A data row maintains a collection of cells at least two (for the label and the input control). Each cell is represented by an object of type DataControlFieldCell. Let s consider the following code:

protected void SetDefaultValue(int rowIndex, string msg)

{

   const int DataCellIndex = 1;

   DetailsViewRow row = DetailsView1.Rows[rowIndex];

   DataControlFieldCell cell =

    (DataControlFieldCell)row.Cells[DataCellIndex];

   TextBox txt = (cell.Controls[0] as TextBox);

   if (txt != null)

       txt.Text = msg;

}

The field cell is simply the container of any control used to render the user interface for the input, be it a textbox, a checkbox, or perhaps a calendar. To find the textbox, or to add a validator on the fly, you need to access the Controls collection of the cell. The effect of the SetDefaultValue method is shown in Figure 3.


Figure 3: Predefined values show up in DetailsView when in insert mode.

 

Persisting Values

In the sample code presented a moment ago, the value for each initialized field is fixed. According to the code snippet, the country field, for instance, will always be set to USA. What if you want to persist the last typed data until the user changes it? This might be a valuable feature that obviates retyping and saves time especially in scenarios where many records must be entered.

In Figure 2 you see a pair of pre/post events for the insertion task. The ItemInserted event handler is invoked immediately after the record has been processed by the bound data source object. This is a great time to persist any inserted data that you want to reiterate. Where should you persist it? It s data specific to the ongoing session; hence, it should go to the session state.

Figure 4 shows the updated code; Figure 5 demonstrates the feature in action. Once you add a record where country is, say, Italy , you ll retrieve that value as the default value for the country field for the next records, as well.

protected void SetDefaultValue(int rowIndex,

 string defaultText)

{

   const int DataCellIndex = 1;

   DetailsViewRow row = DetailsView1.Rows[rowIndex];

   DataControlFieldCell cell =

    (DataControlFieldCell)row.Cells[DataCellIndex];

   TextBox txt = (cell.Controls[0] as TextBox);

   if (txt != null)

   {

       string key = GetDataEntryKey(rowIndex);

       object o = Session[key];

       if (o == null)

           txt.Text = defaultText;

       else

           txt.Text = (string) o;

   }

}

protected void DetailsView1_ItemInserted(object sender,

 DetailsViewInsertedEventArgs e)

{

   string key = "";

   string title = (string) e.Values["title"];

   key = GetDataEntryKey(3);

   Session[key] = title;

   string country = (string) e.Values["country"];

   key = GetDataEntryKey(4);

   Session[key] = country;

}

private string GetDataEntryKey(int rowIndex)

{

   return String.Format("DataEntry_{0}", rowIndex);

}

Figure 4: Persisting inserted data to reiterate in subsequent insertions.


Figure 5: The user enters a new record and some of its data are cached for subsequent entries.

In Figure 4 I use a custom schema to name the key of session state slots used to persist data. Obviously, this schema is totally arbitrary and may be changed in your own implementation. I just found it easier if a hint remains in the key name that points to the index of the field. Note that in the handler of the ItemInserted event you retrieve the entered value through the Values collection of event data structure the DetailsViewInsertedEventArgs class. The collection returns objects whose real types depend on the input field used. It would be a date, for example, if you have a calendar rather than a textbox.

 

Further Enhancements

The DetailsView control lends itself very well to arrange quick data entry pages. Like most semi-automatic tools, though, it may lack key features, such as validation and confirmation. To take full control of the editing process you can use templated fields or perhaps opt for the much more flexible and fully customizable FormView control. A further nice touch to the piece of code we crafted thus far would be adding a client, JavaScript-powered confirm message box before critical operations start. For example, you might want to request confirmation before deleting a record or saving changes. To do so, you must walk your way through the DetailsView control tree and locate the instance of the button control that triggers the delete or save operation. You extend the ItemCreated handler as shown in Figure 6.

protected void DetailsView1_ItemCreated(object sender,

 EventArgs e)

{

   if (DetailsView1.FooterRow == null)

       return;

   if (DetailsView1.CurrentMode == DetailsViewMode.Insert)

   {

       SetDefaultValue(3, "USA");

       SetDefaultValue(4, "Mr.");

   }

   if (DetailsView1.CurrentMode == DetailsViewMode.ReadOnly)

       AddConfirmToDeleteButton();

   if (DetailsView1.CurrentMode != DetailsViewMode.ReadOnly)

       AddConfirmToUpdateButton();

}

Figure 6: Extending the ItemCreated handler.

The Delete button shows up in read-only mode, whereas the Insert and Update buttons appear only when you re in Insert and Edit mode, respectively. The helper methods are detailed in Figure 7.

protected void AddConfirmToDeleteButton()

{

   int commandRowIndex = DetailsView1.Rows.Count - 1;

   DetailsViewRow commandRow =

     DetailsView1.Rows[commandRowIndex];

   DataControlFieldCell cell =

     (DataControlFieldCell)commandRow.Controls[0];

   foreach (Control ctl in cell.Controls)

   {

       LinkButton link = ctl as LinkButton;

       if (link != null)

       {

           if (link.CommandName == "Delete")

           {

               link.ToolTip = "Click here to delete";

               link.OnClientClick = "return confirm('Do

                you really want to delete this record?');";

           }

           else if (link.CommandName == "New")

               link.ToolTip = "Click here to add

                a new record";

           else if (link.CommandName == "Edit")

               link.ToolTip = "Click here to edit

                 the current record";

       }

   }

}

protected void AddConfirmToUpdateButton()

{

   int commandRowIndex = DetailsView1.Rows.Count - 1;

   DetailsViewRow commandRow =

     DetailsView1.Rows[commandRowIndex];

   DataControlFieldCell cell =

     (DataControlFieldCell)commandRow.Controls[0];

   foreach (Control ctl in cell.Controls)

   {

       LinkButton link = ctl as LinkButton;

       if (link != null)

       {

           if (link.CommandName == "Update" ||

             link.CommandName == "Insert")

           {

               link.ToolTip = "Click here to save changes";

               link.OnClientClick = "return confirm('Do you

                 really want to save changes?');";

           }

           else if (link.CommandName == "Cancel")

               link.ToolTip = "Click here to

                 cancel editing";

       }

   }

}

Figure 7: Adding client message boxes to the DetailsView.

The command row is the last row in the Rows collection and contains exactly one cell. You walk your way through the child controls of the cell until you find link buttons. You recognize link buttons through the CommandName property and add a tooltip and client click handler on the fly.

 

Conclusion

In ASP.NET 2.0, the developer s toolbox for databinding operations is definitely rich and complete. Now you have a new, and radically revised, grid control, as well as two controls to manage views of a single record: DetailsView and FormView. In this article, I explored the internal structure of a DetailsView control and provided a working solution to a couple of real-world issues.

The source code for this article is available for download.