Cutting Edge
Custom Data Control Fields
Dino Esposito
Code download available at:
CuttingEdge0601.exe
(131 KB)
Browse the Code Online

Contents
In ASP.NET 2.0, the GridView and DetailsView controls are designed to work together. They don't merely provide complementary services, they also share a number of helper classes and components. The output of the GridView control consists of a sequence of rows, each with a fixed number of columns. Each table column maps to a data column in the bound data source. The DetailsView control has a fixed number of rows (one for each column in the bound data source) and a constant number of columns (header and value).
GridView and DetailsView can be used together to form master/details pages, as I demonstrated in my last column (see
Cutting Edge: Flexible Custom Data Views). Both represent data columns in the bound data source. In fact, a GridView's column and a DetailsView's row are the same kind of object and are represented in ASP.NET 2.0 by the same class—DataControlField.
DataControlField is an abstract class and cannot be used for actual data binding. A bunch of child field classes are derived for binding data to GridView and DetailsView controls. In my last column I covered some of these field types. In particular, I showed how to use the TemplateField class to add validation and special editing user interface capabilities to bound fields. The TemplateField class works as a generic container of markup. It is ideal for rendering custom types of data and any combination of existing fields that you might need. The TemplateField type can also be used to render foreign-key columns through dropdown lists or dates through calendars.
But what if you wanted a specialized, more compact calendar field class—a CalendarField type that would render the bound data as a string in view mode and employ a more effective Calendar control in edit and insert mode? As I mentioned, you can achieve the same result using a TemplateField, but that would be like using late-bound versus early-bound objects. A data-bound page based on CalendarField will be easier to read and even a bit faster to process at run time.
I'll discuss the design and implementation of custom data field types and demonstrate how to actually code them using a few examples—CalendarField to render date values and DropDownField to render foreign key values defined on an external data source.
The DataControlField Class
The DataControlField class in ASP.NET 2.0 is a parallel for the DataGridColumn type used extensively with DataGrid controls in ASP.NET 1.x and is used only by the GridView and DetailsView controls. However, if you happen to write a custom data-bound control that requires binding to columns of data, you can use the DataControlField type to bind data columns to your control's user interface.
As I mentioned, DataControlField is an abstract class that inherits directly from System.Object. It implements the IStateManager interface. Figure 1 lists the methods and properties on the class.

Figure 1 Members of the DataControlField Class
| Property |
Description |
| AccessibleHeaderText |
Gets or sets text that is rendered as the AbbreviatedText property value in some controls for accessibility reasons. |
| ControlStyle |
Gets the style properties for any input control that the field uses in edit or insert mode. |
| FooterStyle |
Gets the style properties for the footer of the field. This is only applicable to the GridView control. |
| FooterText |
Indicates the text displayed in the footer of the field. This is only applicable to the GridView control. |
| HeaderImageUrl |
Gets or sets the URL reference to an image to display instead of text on the header of this field. |
| HeaderStyle |
Gets the style properties for the header of this field. |
| InsertVisible |
Indicates whether the field is displayed when the associated control is in Insert mode. This is only applicable to the DetailsView control. |
| ItemStyle |
Gets the style properties for an item of this field. |
| ShowHeader |
Indicates whether the field header is visible. This is only respected by the DetailsView control. |
| SortExpression |
Indicates the expression used when this field is used to sort the bound data source. This is only applicable to the GridView control. |
| Visible |
Indicates whether the field is visible. |
| Method |
Description |
| ExtractValuesFromCell |
Extracts the value from the control(s) in the field cell and adds it to the dictionary passed to this method, using a unique key name for this field. |
| Initialize |
Initializes the field. |
| InitializeCell |
Initializes a cell in the field. |
| ValidateSupportsCallback |
Indicates that the controls that are contained by the field support callbacks. |
As you can see, the class supports a few style properties—ItemStyle, HeaderStyle, FooterStyle, and ControlStyle. In ASP.NET, style properties are not persisted in the view state through the usual ViewState container, as most simple type properties are. The Style class from which all style properties derive implements IStateManager to take care of view state persistence. To utilize this specific capability, classes that expose style properties typically implement IStateManager themselves to delegate view state persistence to the methods defined on the objects that represent style properties. I'll return to this later.
Types derived from DataControlField are listed in
Figure 2. Although the DataControlField class is marked abstract, it contains a lot of code and many concrete members. The only abstract method is CreateField, defined as follows:
Protected MustOverride Function CreateField() As DataControlField

Figure 2 Data Control Field Classes
| Class |
Description |
| BoundField |
Renders a simple string-based output for each element. It uses a TextBox control to capture input in edit/insert mode. |
| ButtonField |
Renders a command button for each element. Not supported in edit/insert mode. |
| CheckBoxField |
Renders a checkbox control for each element. A CheckBox control is used both in view and edit/insert mode. |
| CommandField |
Renders command buttons to perform selecting, editing, inserting, or deleting operations. |
| HyperLinkField |
Renders a hyperlink control for each element. Not supported in edit/insert mode. |
| ImageField |
Renders an image for each element from the bound URL. The field is predisposed to display BLOB data containing images. In edit/insert mode, it uses a TextBox control to edit the image URL. |
| TemplateField |
Renders some custom UI for each element. It allows you to define a template for the edit mode and one for the insert mode. |
This method is expected to return an instance of the particular data control field object. Other methods should also be overridden in derived classes because their built-in implementation is empty (though not abstract). Those methods are ExtractValuesFromCell and InitializeCell. You must override them to create a significant new field type. Depending on what kind of new functionality you want to build into the field, overriding additional members might be required as well.
Designing the CalendarField Component
The CalendarField component inherits from DataControlField and adds three new properties—DataField, ReadOnly, and DataFormatString. DataField is a string property and indicates the name of the data source column that will be bound to the field. ReadOnly is a Boolean property to indicate whether the contents of the field are editable. To control whether the field is to be added in the insert mode UI, you can use the base InsertVisible Boolean property. Finally, DataFormatString indicates the desired format of the date. The contents of the DataFormatString property will be passed to the ToString method of the DateTime class, meaning that {0:d} and similar expressions are invalid. Here's an example of the CalendarField persistence format:
<msdn:CalendarField DataField="OrderDate"
HeaderText="Date" ShowHeader="true"
DataFormatString="dd MMM, yyyy" ReadOnly="False" />
The code that follows shows the implementation of the DataField property (keep in mind that the other custom properties follow the same implementation scheme):
Public Overridable Property DataField As String
Get
Dim o As Object = MyBase.ViewState("DataField")
If (Not o Is Nothing) Then Return CType(o, String)
Return String.Empty
End Get
Set(ByVal value As String)
MyBase.ViewState("DataField") = value
OnFieldChanged()
End Set
End Property
The OnFieldChanged method is a protected virtual method defined on the parent class DataControlField and meant to signal to the host control (the GridView) that the value of a property on the field has changed. Each control field component knows about its host control through the Control property defined on DataControlField. The Control property is protected and set when the host control initializes the field component.
The Initialize method (see
Figure 1) is called by the host control (GridView or DetailsView) to initialize the field. The initialization takes place before the field is added to the Fields collection of GridView and DetailsView. GridView and DetailsView call the Initialize method from within their CreateChildControls method. The Initialize method is given its basic behavior in the DataControlField base class and there's no real need for you to override it in a derived class:
Public Overridable Function Initialize( _
sortingEnabled As Boolean, ctl As Control) As Boolean
Initialize receives a Boolean value that indicates if sorting is supported. It also receives a reference to the host control (GridView or DetailsView) to be stored in the internal Control property. The return value tells the host control if a call to DataBind is required to complete the initialization of the field type.
Another method on DataControlField that you might want to override in some cases is ValidateSupportsCallback. The default implementation of the method in DataControlField simply throws an exception that the field type doesn't intend to support script callbacks. In a derived class you can override this method to prevent the exception from being thrown in some or all cases. For example, you can supply an empty method implementation to say that a field supports callbacks or throws an exception based on the value of a field-specific property:
Public Overrides Sub ValidateSupportsCallback()
If Not SupportsCallback Then
Throw New NotSupportedException("Callbacks Not Supported")
End If
End Sub
InitializeCell and ExtractValuesFromCell will need overrides in any significant control. InitializeCell lets you take control of the contents of cell where the field value is displayed. ExtractValuesFromCell extracts values from the cell to pursue updates or insert operations. Before taking the plunge into these methods and their overrides for the CalendarField component, let's have a look at Figure 3 where other protected overridable methods are listed.

Figure 3 Protected Overridable Members of DataControlField
| Overridable Method |
Description |
| CopyProperties |
Receives an object reference that represents a copy of the current field object and adds class-specific properties. |
| CreateField |
Abstract method, returns an instance of the field type. |
| LoadViewState |
Restores the class style properties from the view state. |
| OnFieldChanged |
Fires the FieldChanged event defined on the DataControlField class. |
| SaveViewState |
Saves the class style properties to the view state. |
| TrackViewState |
Enables view state tracking for the style properties. |
Implementing CalendarField
The CalendarField class provides the three aforementioned properties—DataField, ReadOnly, and DataFormatString—and overrides InitializeCell, ExtractValuesFromCell, CopyProperties, and CreateField.
CreateField follows a common scheme and gets the same form of implementation in all built-in field types:
Protected Overrides Function CreateField() As DataControlField
Return New CalendarField()
End Function
Overriding this method is mandatory if you derive from DataControlField; it is optional, but recommended, if you derive from existing field types.
In InitializeCell you begin by calling the base method to import much of the logic that deals with header and footer rendering. Next, you add some code to customize the current cell, as shown here:
Sub InitializeCell(cell As DataControlFieldCell, _
cellType As DataControlCellType, _
rowState As DataControlRowState, rowIndex As Integer)
' Call the base method
MyBase.InitializeCell(cell, cellType, rowState, rowIndex)
' Initialize the contents of the cell
If cellType = DataControlCellType.DataCell Then
InitializeDataCell(cell, rowState)
End If
End Sub
The InitializeCell method receives four arguments: a reference to the cell object, the type of the cell, and the state and index of the row being rendered. Most of the existing field components delegate their rendering tasks to another internal method that is usually named InitializeDataCell.
Figure 4 shows the implementation of InitializeDataCell for CalendarField.

Figure 4 Initializing the Cell for a CalendarField Component
Protected Overridable Sub InitializeDataCell( _
ByVal cell As DataControlFieldCell, _
ByVal rowState As DataControlRowState)
Dim ctrl As Control = Nothing
' If in edit/insert mode...
Dim state As DataControlRowState = rowState & DataControlRowState.Edit
If ((Not Me.ReadOnly And (state <> DataControlRowState.Normal)) Or _
rowState = DataControlRowState.Insert) Then
Dim cal As New Calendar
cal.ToolTip = HeaderText
cell.Controls.Add(cal)
' Save the control to use for binding (edit/insert mode)
If DataField.Length > 0 Then ctrl = cal
_inInsertMode = (rowState = DataControlRowState.Insert)
ElseIf (DataField.Length > 0) Then
' Save the control to use for binding (view mode)
ctrl = cell
End If
' If the column is visible, trigger the binding process
If Not ctrl Is Nothing And Visible Then
AddHandler ctrl.DataBinding, New EventHandler(OnBindingField)
End If
End Sub
Protected Overridable Sub OnBindingField( _
ByVal sender As Object, ByVal e As EventArgs)
Dim target As Control = CType(sender, Control)
' If in view mode ...
If TypeOf (target) Is TableCell Then
Dim t As TableCell = CType(target, TableCell)
t.Text = LookupValueForView(target.NamingContainer)
ElseIf TypeOf (target) Is Calendar Then
Dim cal As Calendar = CType(target, Calendar)
Dim dt As DateTime = LookupValueForEdit(target.NamingContainer)
cal.SelectedDate = dt
cal.VisibleDate = dt
End If
End Sub
At its core, the InitializeDataCell method determines which control to bind to data—the cell for view mode or a Calendar control for edit or insert modes. The control is then attached to a DataBinding event handler to actually retrieve and display data. In the handler, you determine the working mode, retrieve the value to display, and configure the control as appropriate.
In view mode, the LookupValueForView helper method determines the value to display. It takes a reference to the naming container of the control (table cell or calendar) that currently forms the field's UI. The naming container is passed to the DataBinder.GetDataItem method to obtain a reference to the row object:
Dim dataItem As Object = DataBinder.GetDataItem(container)
Dim dt As DateTime = _
CType(DataBinder.GetPropertyValue(dataItem, DataField), DateTime)
Next, DataBinder.GetPropertyValue extracts the requested field from the row object. For a CalendarField control, this value is a DateTime object and can be further formatted before display. In edit or insert mode, no significantly different behavior is required, except that you might want to distinguish between edit and insert and provide a default value in the latter case:
Protected Overridable Function LookupValueForEdit( _
container As Control) As DateTime
If Not _inInsertMode Then
Dim dataItem As Object = DataBinder.GetDataItem(container)
Dim value As Object = DataBinder.GetPropertyValue( _
dataItem, DataField)
Return CType(value, DateTime)
End If
Return DateTime.Now
End Function
GridView and DetailsView controls scan their own list of bound fields to collect input values when an update or insert command is selected. In doing so, they call the ExtractValuesFromCell method on the field types. Figure 5 details the implementation of ExtractValuesFromCell in the CalendarField component.

Figure 5 ExtractValuesFromCell Method
Public Overrides Sub ExtractValuesFromCell( _
ByVal dictionary As IOrderedDictionary, _
ByVal cell As DataControlFieldCell, _
ByVal rowState As DataControlRowState, _
ByVal includeReadOnly As Boolean)
Dim selectedValue As Object
If cell.Controls.Count > 0 Then
Dim cal As Calendar = CType(cell.Controls(0), Calendar)
If cal Is Nothing Then
Throw New InvalidOperationException( _
"CalendarField could not extract control.")
Else
selectedValue = cal.SelectedDate
End If
End If
' Add the value to the dictionary
If dictionary.Contains(DataField) Then
dictionary(DataField) = selectedValue
Else
dictionary.Add(DataField, selectedValue)
End If
End Sub
The ExtractValuesFromCell method is invoked when the field is in edit or insert mode. You locate the input control and capture any significant values it may contain. Based on the code shown in Figure 4, the CalendarField's input control is the first control in the Controls collection of the cell. (Note that the position of the input control is arbitrary and depends on the structure of the cell you create in InitializeCell.)
Once you hold the currently selected value of the input control, you add it to the dictionary object that was passed to the ExtractValuesFromCell method. The dictionary contains name/value pairs where the name entry is the name of the DataField property.
Using the CalendarField Component
A custom data control field must be registered with the page (or the application) as any other custom server control. To do this, use the @ Register directive like so:
<%@ Register Namespace="Samples.CustomFields"
TagPrefix="expo" Assembly="HelperFields" %>
Add the following code to the <fields> element of a DetailsView or GridView:
<msdn:CalendarField DataField="OrderDate" HeaderText="Date" />
Turn the page in design mode and work through the Visual Studio
® 2005 menus and windows to get what's shown in
Figure 6.
The sample DetailsView has two custom data fields: CalendarField, used for the Date row, and a DropDownField, used for the posted-by row. I'll say more about the DropDownField control shortly. As you can see, the two control fields have a slightly different user interface. This is due to the DataBinding event handler. Basically, you check the DesignMode property and output some markup if the output is going to be shown in Visual Studio 2005:
If DesignMode Then
Return "<select><option>Databound Date</option> </select>"
End If
Figure 6 DetailsView
DesignMode is a protected property of the DataControlField class. It wraps the DesignMode property of the host control. The code you just saw returns a dropdown-like user interface and characterizes the custom control field. Although the custom data control fields have to be manually coded in the editor, you have full visual support as far as properties editing is concerned. Smart tags and the Properties dialog box show custom fields correctly.
Figure 7 shows a sample master/detail page in action. The DetailsView control uses the CalendarField component to display the date of a data-bound order.
Figure 7 Custom Data Fields in Action—Edit Mode
Designing a DropDownField Component
The DropDownField data field is used with foreign-key fields, that is with data columns that contain indexes to data living in another table. Imagine a table of orders with columns to track the customer who made the order and the employee who physically placed the order. In the Orders table, you won't typically store the full name of employee and customer; rather, you'd store a pointer to another table. In the end, a row in the Orders table contains numbers instead of plain names. This might be great for database management systems, but it's not great for human consumers. When displaying data, you need to transform those numbers into readable names. This is only half the task that you might expect from a DropDownField component. When in edit or insert mode, in fact, the data field should be able to display a list of possible options—that is, all possible customers or employees.
I designed the DropDownField component to expose DataTextField and DataValueField properties. The former indicates the data column to use for display purposes; the latter indicates the data column to use for actual I/O data exchange. The DataTextField is useful only in one particular scenario: when your query contains joined columns. Consider the following SQL command:
SELECT o.*, e.lastname FROM orders o
INNER JOIN employees e on o.employeeid=e.employeeid
WHERE o.orderid=@id
A row from this query that is bound to a DetailsView control contains both the ID of the employee who placed the order (table Orders) and her last name (joined table Employees). In this case, you set DataValueField to employeeid and DataTextField to lastname. If you don't have joined columns, you just leave DataTextField blank.
The DropDownField component also counts three more properties. DataSourceIDForEdit indicates the data source control providing the options to list in edit/insert mode. DataValueFieldForEdit denotes the field in the data source used to determine the value of the items in the dropdown list. Finally, DataTextFieldForEdit denotes the field in the data source used to determine the display text of items in the dropdown list.
To a large extent, the code for DropDownField is the same as for CalendarField. A key difference exists when the data binding is made. To prepare the calendar control, you only need to set its SelectedDate property with the value you obtain from the bound row. With a DropDownField, you first need to populate the dropdown list and select the item that corresponds to the DataValueField's value in the bound row. The dropdown list control is bound to the list of feasible data items through the DataSourceID property. For data binding, this would work just fine. However, setting DataSourceID on a data-bound control doesn't automatically start the binding process. If you try to access the Items collection immediately after setting DataSourceID, you'll get a null reference exception. If you call DataBind to force binding, you get a stack overflow exception because you're calling DataBind from within a DataBinding event, which is triggered by a call to DataBind. The trick is to attach a DataBound event handler to the dropdown list control:
Dim dd As DropDownList = CType(target, DropDownList)
dataValue = LookupValueForEdit(target)
AddHandler dd.DataBound, AddressOf OnDropDownDataBound
dd.DataTextField = DataTextFieldForEdit
dd.DataValueField = DataValueFieldForEdit
dd.DataSourceID = DataSourceIDForEdit
The DataBound event is new in ASP.NET 2.0 for data-bound controls and is fired when the data binding process is complete. In this way, the event handler for DataBound fires when the Items collection is fully populated and you can select the item safely:
Sub OnDropDownDataBound(sender As Object sender, e As EventArgs)
Dim dd As DropDownList = CType(sender, DropDownList)
Dim li As ListItem = dd.Items.FindByValue(dataValue)
li.Selected = True
End Sub
A key goal of the DropDownField component is to be able to use the bound value, DataValueField, to find a related field, possibly in another data source. This operation implies a database query, an INNER JOIN operation, or an access to the ASP.NET cache where related data is cached. Running a real query against a database is simply out of the question for very good reasons: for performance and because it would break consistency and force you to inject connection string and SQL information in a data control field. The only reasonable approach entails retrieving the related field (for example, the last name of the employee knowing the ID) from the data source used to populate the dropdown list in edit mode (you populate the dropdown list when the data control field is initialized). To minimize the performance hit, the data source should be accessed only when you enter into edit/insert mode. Caching should be enabled on the data source control and a cache key dependency mechanism should be used to refresh the cached data in case of changes.
What if the row bound to the DetailsView contains a joined field? In this case, the required information is already available. That's why I defined an optional DataTextField property. If it is set, and the specified field really exists, the data source for edit is loaded only when the host control really enters in edit/insert mode. If DataTextField is not available, then the data source is accessed at initialization time, and the input dropdown list control is created earlier and made available to internal methods through a private property. Here's the markup code that binds a DropDownField field to the employeeid column of the Orders table and the lastname column in the Employees table:
<msdn:DropDownField
DataValueField="employeeid"
HeaderText="Posted by"
DataSourceIDForEdit="EmployeeDataSource"
DataValueFieldForEdit="employeeid"
DataTextFieldForEdit="lastname" />
EmployeeDataSource is defined in the following:
<asp:SqlDataSource ID="EmployeeDataSource" runat="server"
ConnectionString='<%$ ConnectionStrings:LocalNWind %>'
SelectCommand="SELECT employeeid, lastname FROM
employees" />
The full source code of DropDownField and CalendarField is available with the download for this issue of
MSDN®Magazine.
As a side note, remember that the DropDownField component that is discussed here is optimized for data-driven sources. However, the DropDownField component could easily be used to allow users to select any value out of a fixed list—for example, an enum type or any enumerable collection of data. To support these scenarios, you might want to add a DataSource property and bind its contents to the DataSource property of the dropdown list control programmatically.
Styling Control Fields
The DataControlField type defines a few style properties for header, footer, and data items. In addition, the ControlStyle property lets you style the input controls used by the field component:
<asp:BoundField DataField="CompanyName" HeaderText="Company">
<ControlStyle BackColor="red" />
</asp:BoundField>
The preceding code snippet generates a bound field to view or edit the CompanyName data column. The internal textbox of the field will have a red background. When writing custom data control fields, you can use the ControlStyle property to style input controls in a polymorphic way. Both the Calendar and the dropdown list controls, in fact, can be styled through the ControlStyle property. By assigning default values to the property, you can also set new default settings for the input controls.
Depending on characteristics and capabilities of the input controls, you may need to define custom style properties. A new style property can be of type TableItemStyle or a new custom type derived from TableItemStyle. Finally, any new style property you add must be properly persisted in the view state. You need to override LoadViewState, SaveViewState, and TrackViewState methods. Figure 8 shows how to proceed for a sample YourStyle property.

Figure 8 Handling ViewState
Protected Overrides Sub LoadViewState(savedState As Object)
If Not savedState Is Nothing Then
Dim state As Object() = CType(savedState, Object())
If Not state(0) Is Nothing Then
MyBase.LoadViewState(state(0))
End If
If Not state(1) Is Nothing Then
YourStyle.LoadViewState(state(1))
End If
End If
End Sub
Protected Overrides Sub TrackViewState()
MyBase.TrackViewState()
YourStyle.TrackViewState()
End Sub
For the sample CalendarField field, custom style properties can be defined for any of the calendar style you want to control from the page level. The ControlStyle property in this case is of little help because it can only address the style of the calendar core element. In order to style internal parts of the helper calendar control, you need to expose some of the internal styles of the Calendar through new properties.
Conclusion
DataControlField is the base class for data fields—helper components that GridView and DetailsView controls use to build their own data-bound user interface. DataControlField has several derived classes, ImageField, CheckBoxField, and the more popular TemplateField and BoundField, that can be used in pages and ASP.NET applications. In addition, you can create custom data fields. Two canonical examples are DropDownField for foreign keys and CalendarField for dates. In addition to the description of these two components given here, the documentation for DataControlField contains another sample field—the RadioButtonField. In the documentation code, the RadioButtonField inherits from CheckBoxField and provides an alternate user interface for Boolean choices. Look there if you want to explore this topic further.
Send your questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is a mentor at Solid Quality Learning and the author of
Programming Microsoft ASP.
NET 2.
0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch with Dino at
cutting@microsoft.com or join the blog at
weblogs.asp.net/despos.