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
|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
|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 Get 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
|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 EnsureItemsFilled(useDataSource) ' Create the outermost table Dim outerTable As New Table Controls.Add(outerTable) ' Create the header row CreateHeader(outerTable) ' Create all item rows If Items.Count = 0 Then CreateEmptyTable(outerTable) Else CreateView(outerTable) End If ' Create the footer row CreateFooter(outerTable) ' Create the pager bar If AllowPaging Then CreatePager(outerTable) End If ' Create the command bar CreateCommandBar(outerTable) 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 Return End If If data.Count = 0 Then Return End If ' Select the record to display Dim row As DataRowView = data(SelectedRecord) If (row Is Nothing) Then Return End If ' Extract the schema from the underlying table Items.Clear() 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(), _ col.DefaultValue) Items.Add(item) index += 1 Next 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) PrepareControlForRendering(writer) MyBase.RenderContents(writer) 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) outerTable.CopyBaseAttributes(Me) If (ControlStyleCreated) Then outerTable.ApplyStyle(ControlStyle) End If : Dim headerRow As TableRow = outerTable.Rows(0) If Not (_headerStyle Is Nothing) Then headerRow.ApplyStyle(_headerStyle) Else headerRow.ApplyStyle(ControlStyle) 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 CreateEmptyTable(outerTable) Else CreateView(outerTable) 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 CreateCustomView(t) Return End If For Each item As DetailsViewItem In Items CreateItem(t, item) Next 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 t.Rows.Add(row) ' Add the label Dim cellLabel As New TableCell cellLabel.Text = item.Text row.Cells.Add(cellLabel) ' 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 row.Cells.Add(cellValue) 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 Return End If cell.Controls.Add(customView)
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) Next customView.Items = displayData customView.Initialize()
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 t.Rows.Add(row) ' Add a cell Dim cell As New TableCell cell.ColumnSpan = 2 row.Cells.Add(cell) ' 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 Else _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 Else _nextRecLink.Enabled = False End If If SelectedRecord = RecordCount - 1 Then _nextRecLink.Enabled = False End If cell.Controls.Add(New LiteralControl(" ")) cell.Controls.Add(_prevRecLink) cell.Controls.Add(New LiteralControl(" ")) cell.Controls.Add(_nextRecLink) 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 DataBind() 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.