Learning Resources

Consuming Data with the DetailsView Control

In ASP.NET 2.0, DetailsView is a data-bound control that renders a single record at a time from its associated data source. It can optionally provide paging buttons to navigate between records, and a command bar to execute basic operations on the current record (Insert, Update, Delete). DetailsView generates a user interface similar to the Form View of a Microsoft Access database, and is typically used for updating/deleting any currently displayed record or for inserting new records. Figure 1 compares the Access and ASP.NET typical single-record views.


Figure 1. Comparing the Form view of an Access database and the output of a DetailsView control in an ASP.NET 2.0 page

The DetailsView control is also often used in a master-details scenario where the selected record of the master control (a GridView, for example) determines the record to display in the detail view. 

What are the key aspects of a DetailsView control? The new control needs to:

  • Be a composite control and act as a naming container.
  • Be data-bindable to enumerable data sources.
  • Support some style properties.
  • Provide a navigation bar (pager).
  • Support replaceable views of the record fields.
  • Provide a command bar for common operations.

The Starting Point

The DetailsView control is declared as follows:

Public Class DetailsView 
Inherits WebControl  
Implements InamingContainer

As in Figure 1, the user interface of the control consists of a HTML table with two columns—field name and field value. The table may optionally have header, footer, navigation bar, and command bar. The record can display in three ways—read-only, edit, or insert.

The control can be given as many properties as needed to fulfill the level of customization you wish. The properties listed in the table below represent the minimum set of properties to make this control usable in realistic projects.

TABLE 1. Layout properties for the DetailsView control

Property Description
AllowEdit Indicates whether the Edit button should be displayed in the command bar. False by default.
AllowInsert Indicates whether the Insert button should be displayed in the command bar. False by default.
AllowPaging Indicates whether the control should display the navigation bar. False by default.
CurrentMode Indicates the current working mode of the control. Feasible values come from the DetailsViewMode enum type: ReadOnly, Edit, Insert.
EmptyText The text to display should the data source be empty.
FooterText The text to display on the footer of the control.
HeaderText The text to display atop the control.
ViewTemplateUrl Indicates the URL for the ASCX user control to use to provide a customized view of the record. (More on this later.)

In addition to these layout properties, the DetailsView control is also assigned a few style properties, as detailed in Table 2.

TABLE 2. Style properties for the DetailsView control

Property Description
AlternatingItemStyle Style of every other item in the displayed list. For this control, a displayed item represents a field on the current record.
FooterStyle Style of the control's footer.
HeaderStyle Style of the control's header.
ItemStyle Style of all items. If an alternating-item style is specified, this property affects only items with an odd index (first, third, and so on.)

All style properties represent an instance of a class derived from TableItemStyle. In this particular case, all style properties are designed to be TableItemStyle objects, because there's no need to add extra style attributes. Below is the typical implementation of a style property in a composite control. Note that _itemStyle is a control's private member that holds the actual value of the style property. The call to the style object's TrackViewState method instructs the base class (TableItemStyle) to track changes to its view state so that styles can be correctly restored from viewstate on postbacks.

Public ReadOnly Property ItemStyle() As TableItemStyle
     If _itemStyle Is Nothing Then
        _itemStyle = New TableItemStyle
     End If

     If IsTrackingViewState Then
        CType(_itemStyle, IStateManager).TrackViewState()
     End If

     Return _itemStyle
   End Get
End Property

In addition to viewstate built-in support, the class TableItemStyle also provides for great design-time support nearly identical to what you get for style properties of ASP.NET native controls like the DataGrid control. The PersistenceMode attribute instructs the DetailsView control to persist the style settings (as modified by RAD designers) as an inner tag in the ASPX source.

The DetailsView control is obviously a data-bound control and features a few data-related properties. See Table 3 for details.

TABLE 3. Data-binding properties of the DetailsView control

Property Description
DataSource Indicates the data source object associated with the control. In this example, the object must either be a DataTable or a DataView object. More generally, in the setter of this property, you can check and accept types at leisure.
RecordCount Read-only property, indicates the number of records in the currently bound data source. If paging is enabled, the navigation bar in the control's user interface lets you move back and forth through records.
SelectedRecord Gets and sets the 0-based index of the currently displayed record. The index relates to the size of the data source bound to the control. This property is automatically updated when users navigate through records, but can also be programmatically set. The new record displays after the control's binding is refreshed.

You can bind any number of records to the control. As you'll see later, if more than one record is bound, the control displays a navigation bar with buttons to move around.

In addition, the DetailsView control exposes a Items collection object to hold key information about the table rows that form the control's user interface. The collection is a custom collection that groups DetailsViewItem objects. The Items collection is rebuilt from the viewstate or populated from the data source depending on the nature of the request. Typically, in postback requests, the collection is restored from viewstate; otherwise, it is filled with data read out of the data source.

The DetailsViewItem class (see companion source code) is a relatively simple class with four public read-only properties: Text, Value, DataType, DefaultValue. The first three properties are strings; the last is an object. These properties are set with data coming from the viewstate or from the data source. Once set, their values are used to build the control's hierarchy.

The control's hierarchy is generated care of a protected over-ridable method of the Control base class. The method is CreateControlHierarchy and is triggered by any call made to the DataBind method of the control.

Protected Overridable Sub CreateControlHierarchy( _
       ByVal useDataSource As Boolean)
   ' Fill the Items collection from the viewstate/data source

   ' Create the outermost table
   Dim outerTable As New Table

   ' Create the header row

   ' Create all item rows
   If Items.Count = 0 Then
   End If

   ' Create the footer row

   ' Create the pager bar
   If AllowPaging Then
   End If

   ' Create the command bar
End Sub

The method first ensures that the Items collection is filled and usable; next it proceeds with the building of the control's user interface. The useDataSource parameter indicates whether the collection should be filled from the data source (true) or the viewstate (false). The control's infrastructure sets that parameter for you. You are only requested to write a custom method that reads the data source and populates the Items collection with record information. Here's an example:

Private Sub LoadItemsFromDataSource()
    Dim data As DataView = ResolveDataSource()
    If data Is Nothing Then
    End If
    If data.Count = 0 Then
    End If

    ' Select the record to display
    Dim row As DataRowView = data(SelectedRecord)
    If (row Is Nothing) Then
    End If

    ' Extract the schema from the underlying table
    Dim index As Integer = 0
    For Each col As DataColumn In data.Table.Columns
       Dim item As New DetailsViewItem( _
                   col.ColumnName, _
                   row(index).ToString(), _
                   col.DataType.ToString(), _
       index += 1
    RecordCount = data.Count
End Sub

The ResolveDataSource method normalizes the data source object to a DataView object. (Bear in mind that here the data source can only be a DataView or DataTable object by design.)

The code selects the current record and fills the Items collection with as many elements as there are columns in the record. Each added element is completed with a label (column name), a string for the type, and the default value. All this data is excerpted from the table behind the DataView object.

The CreateControlHierarchy method also builds the HTML table that encapsulates the control's markup. First, it builds a new Table control and adds it to the Controls collection of the DetailsView control. Next, it starts adding rows, each of which represents a different item on the control—items, alternating items, header, footer, and so forth. All constituent items are styled just before rendering. No style information is processed while building the hierarchy of the final control. Here's a possible and likely implementation of the Render method:

Protected Overrides Sub Render(ByVal writer As HtmlTextWriter)
End Sub

The PrepareControlForRendering method applies styles to the rows of the constituent table. The following code snippet illustrates the core code behind control styling. The table is the first control in the DetailsView's Controls collection; it is applied a mix of base style attributes and user-defined attributes. The header is identified as first row in the table and is styled using the custom HeaderStyle object, if defined, or the built-in ControlStyle object.

Dim outerTable As Table = Controls(0)
If (ControlStyleCreated) Then
End If
Dim headerRow As TableRow = outerTable.Rows(0)
If Not (_headerStyle Is Nothing) Then
End If

The record view provided by the DetailsView control is set up in the CreateView method.

' Create all item rows
If Items.Count = 0 Then
End If

The CreateView method is invoked if there's at least one record to display, and generates a relatively simple and spartan user interface—the left column shows the field name and right column shows the field value. This default view can be customized at will, as you'll see in a moment.

The View Engine

The CreateView method works by integrating a custom view object in the control's body or, alternatively, generates the classic layout you can see in Figure 1.

Private Sub CreateView(ByVal t As Table)
    ' Any custom view specified?
    If ViewTemplateUrl <> "" Then
    End If
    For Each item As DetailsViewItem In Items
         CreateItem(t, item)
End Sub

A custom view object is an external ASCX user control whose URL can be set through the ViewTemplateUrl property. If this property is empty or not set the method defaults to the standard rendering algorithm; otherwise, it loads and initializes the user control.

The classic user interface is generated invoking the CreateItem method for each element in the Items collection.

Private Sub CreateItem(ByVal t As Table, ByVal item As DetailsViewItem)
   Dim row As New TableRow

   ' Add the label 
   Dim cellLabel As New TableCell
   cellLabel.Text = item.Text

   ' Add the value 
   Dim cellValue As TableCell
   Select Case CurrentMode
      Case DetailsViewMode.ReadOnly
           cellValue = CreateItemReadOnly(item)
      Case DetailsViewMode.Edit
           cellValue = CreateItemEdit(item)
      Case DetailsViewMode.Insert
           cellValue = CreateItemInsert(item)
   End Select
End Sub

CreateItem first adds a new row to the table and then splits it in two cells—field name and value. The field name is rendered as plain text; the rendering of the field value depends on the current mode instead. If the mode is ReadOnly, it is rendered as plain text as well. If the mode is Edit, it is rendered with an editable textbox. Finally, if the control is in Insert mode, the field value changes to the item's default value and is rendered with a textbox. When in Edit or Insert mode, the control is going to update the current record or to insert a new one. The buttons to finalize these operations will be created in the control's command bar. Figure 2 offers a preview of the control in action.


Figure 2. The DetailsView control in action

To let a custom control support a customizable view, you can choose between two possible techniques: templates or user controls. In the aforementioned article, you find the template-based technique fully demonstrated. There's not much to add—templates are common amongst server controls, even though not very practical because any change requires editing and compiling the whole page. On the other hand, templates can take advantage of the ASP.NET data-binding model and can consume the current data item object. Writing a custom control that support templates requires a little bit more attention and care, but is overall a relatively simple technique to implement with several examples available as a guide.

Integrating a user control in a custom control decouples control and view objects and allows you to modify at will the user control without affecting the page.

Dim customView As DetailsViewLayout = Page.LoadControl(ViewTemplateUrl)
If customView Is Nothing Then
End If

The preceding code snippet shows how to load a user control into a table's cell. But what's that DetailsViewLayout class that show up in the listing? It is a class that inherits from UserControl and represents the base class for user controls to be used within a DetailsView control. Why on earth should you use a class like this?

The user control is meant to provide an alternative view of the record fields; so you need to pass the record data down to the user control. You can temporarily park data to Cache or Session (if it is not out-of-process), and agree on the name of the entry to use. Personally, I find another approach more elegant and neat. I set the requirement that not any user control can be used as the view object of a DetailsView control; only those which inherit from DetailsViewLayout can.

Public Class DetailsViewLayout : Inherits UserControl
   ' The control receives the data through this member 
   Public Items As Hashtable

   ' Used to initialize the UI reading from Items
   Public Overridable Sub Initialize()
   End Sub
End Class

The class adds a Items property (a hashtable object) and a virtual (overridable) method named Initialize.

After inserting the user control in the hierarchy, the CreateView method prepares a hashtable and fills it with the contents of the DetailsView's Items collection. The hashtable is then associated with the Items property of the user control.

' Retrieve data using a Items("customerid") syntax
Dim displayData As New Hashtable
For Each item As DetailsViewItem In Items
     displayData.Add(item.Text, item)
customView.Items = displayData

The Initialize method on the user control will bind data to local controls. Since the hashtable is a weakly typed object, you need to cast any object in the hashtable to the DetailsViewItem class. I've chosen this approach because it is the simplest (not only) way to use an intuitive syntax like the one below.

Public Overrides Sub Initialize()
   ' Initialize local controls
   customerid.Text = CType(Items("CustomerID"), DetailsViewItem).Value
   companyname.Text = CType(Items("CompanyName"), DetailsViewItem).Value
   contactname.Text = CType(Items("ContactName"), DetailsViewItem).Value
   Address.Text = sb.ToString()
End Sub

Figure 3 shows the DetailsView control with a custom view object.


Figure 3. A user control is used to provide the view of the DetailsView control.

The following markup is needed to set the DetailsView to use a given user control.


Navigating Through Records

A DetailsView control would be almost meaningless if it lacked a navigation bar, that is an additional row with buttons to move back and forth between bound records. The navigation bar is created as part of the control's hierarchy and typically supplies two buttons: Previous and Next record.

Private Sub CreatePager(ByVal t As Table)
   ' Create the row
   Dim row As New TableRow

   ' Add a cell
   Dim cell As New TableCell
   cell.ColumnSpan = 2 

   ' Add the link button to the PREVIOUS record 
   _prevRecLink = New LinkButton
   _prevRecLink.Font.Name = "webdings"
   _prevRecLink.Font.Size = FontUnit.Point(10)
   _prevRecLink.Text = "7"
   If RecordCount > 1 Then
       AddHandler _prevRecLink.Click, AddressOf GotoPrevRecord
       _prevRecLink.Enabled = False
   End If
   If SelectedRecord = 0 Then
       _prevRecLink.Enabled = False
   End If

   ' Add the link button to the NEXT record 
   _nextRecLink = New LinkButton
   _nextRecLink.Font.Name = "webdings"
   _nextRecLink.Font.Size = FontUnit.Point(10)
   _nextRecLink.Text = "8"
   If RecordCount > 1 Then
      AddHandler _nextRecLink.Click, AddressOf GotoNextRecord
      _nextRecLink.Enabled = False
   End If
   If SelectedRecord = RecordCount - 1 Then
      _nextRecLink.Enabled = False
   End If

   cell.Controls.Add(New LiteralControl(" "))
   cell.Controls.Add(New LiteralControl(" "))
End Sub

The navigation row spans over the two columns that form the typical output of the control and contains a couple of link buttons. Navigation buttons are disabled if the data source counts a single record. Previous and Next buttons are also individually disabled if the currently selected record is the first or the last in the bound data source.

The controls are added a click handler to actually select the next record the user can view. The listing below illustrates the code that moves the pointer back to the previous record. It is structurally identical to the code called to move the pointer ahead.

Protected Sub GotoPrevRecord(ByVal sender As Object, ByVal e As EventArgs)
    Dim pos As Integer = SelectedRecord
    If pos > 0 Then
        SelectedRecord = pos - 1
    End If
End Sub

The handler works by updating the SelectedRecord property, which determines the data source 0-based index of the record to view. Next, it calls DataBind to trigger the process that ultimately updates the user interface of the control. The enabled/disabled state of the buttons is handled within the CreatePager method when the buttons are created and inserted in the hierarchy.