Practical ASP.NET

Create Data-Bound Controls With ASP.NET 2.0

ASP.NET 2.0 lets you build a data-bound custom control that ensures your data is always displayed consistently.

Thanks to the changes in databinding in ASP.NET 2.0, creating your own data-bound custom control is a simple task. I'll give you the foundation for any data-bound control you want to build.

ASP.NET 2.0's GridView control is a powerful tool, but you must reformat it constantly every time you want to use it. If you want to make sure your data is always displayed the same way but don't need the power of the GridView, you can create your own data-bound control. This will let you display your data simply by dragging your control onto the next form that you create.

The mechanics of databinding and, as a result, the mechanics of creating a data-bound control have changed dramatically from ASP.NET 1.* to ASP.NET 2.0. The good news is that the process is simple and flexible, allowing you to intervene in the process easily to tailor the results. I'll show you how to handle complex databinding to retrieve records. You can create a control that's bound to multiple records in order to generate a display that shows all of the records. In this case, I'll show you how to create a display-only control that displays a single field from a DataSource and displays the data in a table (see Figure 1, which shows the page in design view, and Figure 2, which shows the result at run time).

You must inherit from the System.Web.UI.WebControls.DataBoundControl class in order to create a control that supports complex databinding. You need to provide a way for developers to set databinding information, such as which data source to use, the name(s) of the fields to use, and so on. Then you must retrieve the data and move the data to the page.

Start your ASP.NET 2.0 custom control project in Visual Studio 2005 by creating a class library project, deleting the default class added to the project, and adding a Web Custom Control class to the project. All data-bound controls must inherit from System.Web.UI.WebControls.DataBoundControl class, so you need to alter the Inherits line in the class. The default code and property provided in the class module are going to be of no help to you either, so you should delete them to give this as your starting point:

Imports System.ComponentModel
Imports System.Web.UI

<ToolboxData("<{0}:CustomerList _
	runat=server></{0}:CustomerList>")> _
	Public Class CustomerList
	Inherits System.Web.UI.WebControls.WebControl

End Class

Your next step is to decide how many databinding properties you want to implement in your control (the DataBoundControl specifies only three: DataSource, DataSourceId, and DataMember). You need to implement only the DataTextField if you're creating a control that displays a single field in a fixed format. The base DataTextField property lets the developer using your control pick the field to display:

Private strDTF As String

Public Property DataTextField() As String
	Get
		If strDTF Is Nothing Then
			Return String.Empty
		Else
			Return strDTF
		End If
	End Get

	Set(ByVal value As String)
		strDTF = value
	End Set

End Property

You should change the way your data is retrieved if any databinding-related property is changed at run time. There's no problem if the property is changed before data is retrieved. However, you'll want to retrieve the data again if the code on the page hosting the control changes the values of the databinding properties after the data is retrieved.

You can use two members of the DataBoundControl class to handle this scenario and trigger retrieving data: the Initialized property, which returns True if data has been retrieved (or if the databinding process has been started), and the OnDataPropertyChanged method, which you can call to signal that a property has changed. Taking advantage of these two members in the DataTextField property means rewriting the Set portion of the property like this:

Set(ByVal value As String)
	If Me.Initialized = True Then
		Me.OnDataPropertyChanged()
	End If
	strDTF = value
End Set

Retrieve Your Data
Retrieve the data for your control by overriding the DataBoundControl's PerformSelect method. In PerformSelect, before retrieving any data, you should check to see whether the IsBoundUsingDataSourceId property is True, which would indicate that the control has bound to the data specified in the DataSourceId property. If it isn't, you should call the OnDataBinding method to bind to the data source. However, you probably don't want to databind at all when your control is being used at design time, so this code checks for that condition before starting the databinding process:

Protected Overrides Sub PerformSelect()
Dim dsa As System.Web.UI.DataSourceSelectArguments
Dim dsv As System.Web.UI.DataSourceView

If Me.DesignMode = True Then
	If Me.IsBoundUsingDataSourceID = False Then
		Me.OnDataBinding(EventArgs.Empty)
	End If

Next, you need to retrieve the DataSourceView through the DataBoundControl's GetData method. Once you do this, you can call its Select method to initiate data retrieval. The Select method retrieves data from the data source asynchronously, allowing other processing to continue while the data is being retrieved. This is the first place where you can step in to tailor the data retrieval process by taking advantage of the Select method's two parameters.

The first parameter is a DataSourceSelectArguments object that you can use to tailor data retrieval settings in the DataSource that your control is bound to. The second parameter to the Select method is the routine to call when all the data has been retrieved. In this routine, you can perform any processing you want before the data is displayed and then call the DataBoundControl's PerformDataBinding method to actually move the data to the page.

You can ignore most of the properties on the DataSourceSelectArguments object in most scenarios, but you might want to take advantage of the SortExpression property. The string value that you set this property to will be used by the DataSource to sort the incoming data. For data sources tied to a relational database, this will be the same text that you would use in the Order By clause of a SQL statement. If you don't want to configure the Select method, pass the object returned by the CreateDataSourceSelectArguments method as the first parameter to the Select method.

In ASP.NET 2.0, the DataSource object that your control is bound to handles all of the data-related activities except for displaying the data (that's the responsibility of your custom control). However, not all DataSource objects support all data-related activities. You can use the DataSourceView to find out what the DataSource will (and won't) do for you. For instance, not all DataSource objects support sorting. You can check to determine if the DataSource that your control is bound to does support sorting by checking the CanSort property.

In this example, I use the SortExpression property of the DataSourceSelectArguments object to have the data sorted by whatever field has been set in the DataTextField property. The code then uses the GetData method to retrieve a DataSourceView and calls its Select method, passing the DataSourceSelectArguments object just created. The second parameter passed to the Select method specifies that a function called HandleRetrievedData is to handle the returned data:

dsa = New System.Web.UI.DataSourceSelectArguments
If dsv.CanSort = True Then
	dsa.SortExpression = Me.DataTextField
End If

dsv = Me.GetData
dsv.Select(dsa, AddressOf Me.HandleRetrievedData)

Once you retrieve all your data, set the RequiresDataBinding property to False to signal to ASP.NET that you won't be calling the PerformSelect method again. This lets ASP.NET manage data resources more efficiently. You should also save the databinding state by calling the MarkAsDataBound method and raise the OnDataBound event to notify the host page that databinding is complete. This code performs all three of these actions:

Me.RequiresDataBinding = False
Me.MarkAsDataBound()
Me.OnDataBound(EventArgs.Empty)

You can intervene in the databinding process again in the routine that you passed as the second parameter to the Select method. This routine must accept a single parameter (which must have the data type IEnumerable). This is the collection of all of the data objects retrieved by the DataSource. For a relational database, there will be one data object for each row that was retrieved.

You can do any processing in this routine on the data collection before you display it. The code to actually display the data goes in the DataBoundControl's PerformDataBinding method, so you should finish this routine by calling the PerformDataBinding method. Since I don't need to process any of the data, I can write the simplest possible version of the routine:

Private Sub HandleRetrievedData(ByVal data As _
	IEnumerable)
	PerformDataBinding(data)
End Sub 

Tailor Your Display
You must override the PerformDataBinding method in order to tailor the display of the data. You should always call the PerformDataBinding method of the DataBoundControl object to make sure that any utility code has a chance to execute. It's also probably smart to ensure that the data collection passed to the method actually has some data:

Protected Overrides Sub PerformDataBinding( _
	ByVal data As System.Collections.IEnumerable)
Dim objData As Object
Dim strFieldName as String

	MyBase.PerformDataBinding(data)

	If data IsNot Nothing Then

Display the data in this control by looping through all of the data objects in the data collection, and insert them into an ASP.NET control. In this example, I've inserted the data into a table. When your data is added to a control, add the control to your custom control's Controls collection and let ASP.NET's standard processing take care of rendering the controls to the page.

Use the GetProperty value method of ASP.NET's DataBinder object to extract the field you want from the data passed to the method. Simply pass the data object holding the data and the name of the field that you want to the GetProperty method. In this example, the value set in the DataTextField is used to determine which field to display:

If data IsNot Nothing Then
	Dim tbl As New System.Web.UI.WebControls.Table
	Dim tr As System.Web.UI.WebControls.TableRow
	Dim tc As System.Web.UI.WebControls.TableCell
	Dim trh As New _
		System.Web.UI.WebControls.TableHeaderRow
	Dim tch As New _
		System.Web.UI.WebControls.TableHeaderCell

	tch.Text = Me.DataTextField 
	trh.Cells.Add(tch)
	tbl.Rows.Add(trh)
	If Me.DataTextField > "" Then
		strFieldName = Me.DataTextField
		ctr = 0
		For Each objData In data
			tr = New _
				System.Web.UI.WebControls.TableRow
			tc = New _
				System.Web.UI.WebControls.TableCell
			tc.Text = _
				System.Web.UI.DataBinder.GetPropertyValue( _
				objData, strFieldName, _
				Nothing)

			tr.Cells.Add(tc)
			tbl.Rows.Add(tr)
		Next
		Me.Controls.Add(tbl)
	End If
End If

Now you're ready to test out your control. Testing is simple if you've placed your control project in the same solution as the Web site where you'll use the control. The next time you switch to a Web Form, you'll find your control sitting in the Visual Studio Toolbox, ready to be dragged onto your page.

However, after you drag your control onto your Web Form, you'll discover that your control has no design-time display. Fortunately, you can provide a display at design time by overriding the control's Render method. This version of the Render method displays a simple message at design time and calls the DataBoundControl's base Render method at run time to display the data:

Protected Overrides Sub Render(ByVal writer As _
	System.Web.UI.HtmlTextWriter)
	If Me.DesignMode = True Then
		writer.Write("<b>Displaying List of _
			" & Me.DataTextField & "s</b>")
	Else
		MyBase.Render(writer)
	End If
End Sub

You're probably already thinking of how you can extend this simple control. Displaying multiple fields from multiple records or supporting the standard Format properties found on a data-bound control are the obvious next steps. You can let users enter data by using textboxes to display the data. You could then update the data using the same techniques you would use from a Web Form. You also might want to investigate the properties on the DataSourceSelectArguments object, which supports paging so you don't have to display all your data at once.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube