Creating DataBound ControlsYou can bind several of the standard ASP.NET controls to a data source. For example, you can bind a Repeater control to data sources, such as DataReaders , ArrayLists , and DataViews . You too can create a control that supports binding to a data source. To create a DataBound control, you must do the following:
You examine the DataSource property and OnDataBinding subroutine in the following sections. NOTE Classes that support the IEnumerable interface are required to have a method named GetEnumerator . The GetEnumerator method returns an enumerator that can be used to iterate through all the items in the underlying collection. Implementing Different Data SourcesIn most cases, a control's DataSource property should implement the IEnumerable interface, which is implemented by such classes as DataReader , DataView , and ArrayList . The IEnumerable interface exposes a method named GetEnumerator that enables you to walk through all the elements of a collection. If you simply want to display a list of items from a data source, you should implement this interface. In some situations, however, you need to implement the ICollection interface, which enables you to work with the contents of a data source as a whole. For example, it includes a Count property, which enables you to get a total count of the number of items in the collection. The ICollection interface also has a CopyTo method, which copies the contents of a data source to an array. If you're implementing a control, such as a content rotator control that enables you to randomly select an item from a data source, you need to use the ICollection interface. Otherwise, you cannot randomly select an item from the whole data source. Standard ASP.NET controls ”such as Repeater , DataList , and DataGrid ”also support the IListSource interface. They support this interface so that they can be bound directly to a DataSet . For example, you can specify the data source for a Repeater control like this: rptRepeater.DataSource = dstDataSet rptRepeater.DataMember = "Products" The first statement binds the Repeater control to a DataSet . The second statement selects a particular table in the DataSet to bind to. Implementing a Simple DataBound ControlFor this example, you can start by creating a simple DataBound control that binds to an ArrayList . The control in Listing 29.7 has a DataSource property that implements the IEnumerable interface. Listing 29.7 SimpleDataBound.vbImports System Imports System.Web Imports System.Web.UI Imports System.Collections Namespace myControls Public Class SimpleDataBound Inherits Control Private _dataSource As IEnumerable Public Property DataSource As IEnumerable Get Return _dataSource End Get Set _dataSource = Value End Set End Property Protected Overrides Sub OnDataBinding( e As EventArgs ) Dim DataEnum As IEnumerator Dim ltlLiteral As LiteralControl If Not DataSource Is Nothing Controls.Clear() DataEnum = DataSource.GetEnumerator() While ( DataEnum.MoveNext() ) ltlLiteral = New LiteralControl( DataEnum.Current.ToString() ) Controls.Add( ltlLiteral ) Controls.Add( New LiteralControl( "<br>" ) ) End While End If End Sub End Class End Namespace The C# version of this code can be found on the CD-ROM. The control in Listing 29.7, named SimpleDataBound , has a single public property named DataSource . This property reads and sets a private variable named _dataSource . The control also has an OnDataBinding subroutine, which is executed whenever the control's DataBind() method is called. Within the OnDataBinding subroutine, an enumerator named DataEnum is retrieved from the data source by calling the GetEnumerator method. The MoveNext method loops through all the values contained in the data source. Each item from the data source is transformed into a LiteralControl and added to the control's Controls collection. When the control is rendered, all the items from the data source are rendered. The page in Listing 29.8 illustrates how you can bind a data source to this control. Listing 29.8 DisplaySimpleDataBound.aspx<%@ Register TagPrefix="myControls" Namespace="myControls" Assembly="SimpleDataBound"%> <Script Runat="Server"> Sub Page_Load Dim colArrayList As ArrayList colArrayList = New ArrayList colArrayList.Add( "Seattle" ) colArrayList.Add( "Paris" ) colArrayList.Add( "Banjarmasin" ) colArrayList.Add( "Tokyo" ) ctrlSimpleDataBound.DataSource = colArrayList ctrlSimpleDataBound.DataBind() End Sub </Script> <html> <head><title>DisplaySimpleDataBound.aspx</title></head> <body> <form Runat="Server"> <myControls:SimpleDataBound id="ctrlSimpleDataBound" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. In Listing 29.8, an ArrayList is bound to the custom control in the Page_Load subroutine. This ArrayList contains a list of cities. When the DataBind method is called, the OnDataBinding method of the SimpleDataBound control is executed, and the items from the ArrayList are added to the Controls collection of the custom control. Using DataBound Controls and TemplatesTypically, when you create DataBound controls, you use the control with a template. By implementing a template, you can format each of the data source items displayed by the control. The control in Listing 29.9 contains a template named ItemTemplate . Listing 29.9 DataBoundTemplate.vbImports System Imports System.Web Imports System.Web.UI Imports System.Collections Namespace myControls <ParseChildren(true)> Public Class DataBoundTemplate Inherits Control Private _dataSource As IEnumerable Private _itemTemplate As ITemplate Public Property DataSource As IEnumerable Get Return _dataSource End Get Set _dataSource = Value End Set End Property Protected Overrides Sub OnDataBinding( e As EventArgs ) Dim objDataEnum As IEnumerator Dim objItem As DataBoundTemplateItem If Not DataSource Is Nothing Controls.Clear() objDataEnum = DataSource.GetEnumerator() While ( objDataEnum.MoveNext() ) objItem = New DataBoundTemplateItem( objDataEnum.Current ) ItemTemplate.InstantiateIn( objItem ) Controls.Add( objItem ) End While End If End Sub <TemplateContainer(GetType(DataBoundTemplateItem))> _ Public Property ItemTemplate As ITemplate Get Return _itemTemplate End Get Set _itemTemplate = value End Set End Property End Class Public Class DataBoundTemplateItem Inherits Control Implements INamingContainer Private _DataItem As Object Public Sub New( DataItem As Object ) MyBase.New() _dataItem = DataItem End Sub Public ReadOnly Property DataItem As Object Get Return _dataItem End Get End Property End Class End Namespace The C# version of this code can be found on the CD-ROM. In Listing 29.9, a control named DataBoundTemplate is declared. This control has a single template named ItemTemplate . When you call the DataBind method on the DataBoundTemplate control, its OnDataBinding subroutine executes. This subroutine iterates through the items in the data source. For each item, a new control named DataBoundTemplateItem is created to represent the item. Each element of the ItemTemplate is instantiated in the DataBoundTemplateItem control with the InstantiateIn method. The DataBoundTemplateItem control is then added to the Controls collection of the DataBoundTemplate control. The ASP.NET page in Listing 29.10 demonstrates how you can bind the DataBoundTemplate control to a database table named Authors. Listing 29.10 DisplayDataBoundTemplate.aspx<%@ Register TagPrefix="myControls" Namespace="myControls" Assembly="DataBoundTemplate"%> <%@ Import Namespace="System.Data.SqlClient" %> <Script Runat="Server"> Sub Page_Load Dim conPubs As SqlConnection Dim cmdSelect As SqlCommand conPubs = New SqlConnection( "Server=LocalHost;UID=sa;PWD=secret;Database=Pubs" ) cmdSelect = New SqlCommand( "Select * From Authors", conPubs ) conPubs.Open() ctrlDataBoundTemplate.DataSource = cmdSelect.ExecuteReader() ctrlDataBoundTemplate.DataBind() conPubs.Close() End Sub </Script> <html> <head><title>DisplayDataBoundTemplate.aspx</title></head> <body> <form Runat="Server"> <myControls:DataBoundTemplate ID="ctrlDataBoundTemplate" Runat="Server"> <ItemTemplate> <li> <i> <%# Container.DataItem( "au_lname" ) %> </i> </ItemTemplate> </myControls:DataBoundTemplate> </form> </body> </html> The C# version of this code can be found on the CD-ROM. In the Page_Load subroutine in Listing 29.10, a SqlDataReader is created to represent all the records from the Authors table. This SqlDataReader is bound to the DataBoundTemplate control. The DataBoundTemplate control is declared with a template named ItemTemplate . This template formats each item in italics. Using DataBound Controls and StateWhen you bind a data source to the standard ASP.NET controls, you typically need to bind the data source only when the page is first loaded. If the page is posted back to the server, the control maintains its state. You can maintain the state of a control by taking advantage of a control's view state. Items added to view state are automatically preserved between postbacks in a hidden __VIEWSTATE form field. The control in Listing 29.11 demonstrates how you can store the items from a data source in a control's view state. Listing 29.11 DataBoundState.vbImports System Imports System.Web Imports System.Web.UI Imports System.Collections Namespace myControls <ParseChildren(true)> _ Public Class DataBoundState Inherits Control Private _dataSource As IEnumerable Private _itemTemplate As ITemplate Public Property DataSource As IEnumerable Get Return _dataSource End Get Set _dataSource = Value End Set End Property Protected Overrides Sub OnDataBinding( e As EventArgs ) Dim objDataEnum As IEnumerator Dim objItem As DataBoundStateItem Dim intCounter As Integer If Not DataSource Is Nothing Controls.Clear() ClearChildViewState() objDataEnum = DataSource.GetEnumerator() While ( objDataEnum.MoveNext() ) objItem = New DataBoundStateItem( objDataEnum.Current ) ItemTemplate.InstantiateIn( objItem ) Controls.Add( objItem ) intCounter += 1 End While ViewState( "NumItems" ) = intCounter ChildControlsCreated = True End If End Sub Protected Overrides Sub CreateChildControls() Dim objNumItems As Object Dim intItemCount As Integer Dim intCounter As Integer Dim objItem As DataBoundStateItem objNumItems = ViewState( "NumItems" ) If Not objNumItems = Nothing Then Controls.Clear() intItemCount = CInt( objNumItems) For intCounter = 0 To intItemCount - 1 objItem = New DataBoundStateItem( Nothing ) ItemTemplate.InstantiateIn( objItem ) Controls.Add( objItem ) Next End If End Sub <TemplateContainer(GetType(DataBoundStateItem))> _ Public Property ItemTemplate As ITemplate Get Return _itemTemplate End Get Set _itemTemplate = value End Set End Property End Class Public Class DataBoundStateItem Inherits Control Implements INamingContainer Private _DataItem As Object Public Sub New( DataItem As Object ) MyBase.New() _DataItem = DataItem End Sub Public ReadOnly Property DataItem As Object Get Return _DataItem End Get End Property End Class End Namespace The C# version of this code can be found on the CD-ROM. In the OnDataBinding subroutine in Listing 29.11, a count of the number of items from the data source is added to the control's view state. A new item, named NumItems , is assigned to the ViewState property. If NumItems can be retrieved from the control's ViewState property, the value of NumItems is used to determine the number of child controls to add to the Controls collection. When each DataBoundStateItem control is added to the Controls collection, notice that each DataBoundStateItem is initialized with the value Nothing . The ASP.NET framework automatically retrieves the previous value of each control. You need only to instantiate each control. Here's one final thing you should notice about Listing 29.11. At the end of the OnDataBinding subroutine, the ChildControlsCreated property is set to the value True . You set this value to prevent the CreateChildControls subroutine from executing and overwriting the controls added by the OnDataBinding subroutine. The page in Listing 29.12 illustrates how you can use the DataBoundState custom control. Listing 29.12 DisplayDataBoundState.aspx<%@ Register TagPrefix="myControls" Namespace="myControls" Assembly="DataBoundState"%> <%@ Import Namespace="System.Data.SqlClient" %> <Script Runat="Server"> Sub Page_Load If Not IsPostBack Then Dim conPubs As SqlConnection Dim cmdSelect As SqlCommand conPubs = New SqlConnection( "Server=LocalHost;UID=sa;PWD=secret;Database=Pubs" ) cmdSelect = New SqlCommand( "Select * From Authors", conPubs ) conPubs.Open() ctrlDataBoundState.DataSource = cmdSelect.ExecuteReader() ctrlDataBoundState.DataBind() conPubs.Close() End If End Sub </Script> <html> <head><title>DisplayDataBoundState.aspx</title></head> <body> <form Runat="Server"> <myControls:DataBoundState ID="ctrlDataBoundState" Runat="Server"> <ItemTemplate> <li> <i> <%# Container.DataItem( "au_lname" ) %> </ItemTemplate> </myControls:DataBoundState> <p> <asp:Button Text="Reload Page!" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The IsPostBack property in the Page_Load subroutine in Listing 29.12 detects whether this is the first time the page has been loaded. The data source is bound to the control only when IsPostBack has the value False . If you click the Reload Page! button, the page is reloaded. However, DataBoundState continues to display all the data from the data source. The values of all the items are preserved in view state. Binding a Custom Control to a DataSetYou can bind all the standard ASP.NET controls to a number of different types of data sources. For example, you can bind a Repeater control to an ArrayList like this: colArrayList = New ArrayList colArrayList.Add( "Hello World!" ) rptRepeater.DataSource = colArrayList rptRepeater.DataBind() You also can bind a Repeater to a DataReader like this: rptRepeater.DataSource = dtrDataReader rptRepeater.DataBind() You can bind a Repeater to a DataView like this: rptRepeater.DataSource = dstDataSet.Tables( "Products" ).DefaultView() rptRepeater.DataBind() You can bind a Repeater directly to a DataSet like this: rptRepeater.DataSource = dstDataSet rptRepeater.DataBind() If a DataSet contains multiple tables, you can select a particular table to bind to like this: rptRepeater.DataSource = dstDataSet rptRepeater.DataMember = "titles" rptRepeater.DataBind() To create a custom control that works like the standard ASP.NET controls, you need to support all these very different data-binding options. In this section, you learn how to add a function to a custom control that enables it to bind to any standard data source. The custom control, named DataBoundDataSet , is contained in Listing 29.13. Listing 29.13 DataBoundDataSet.aspxImports System Imports System.Web Imports System.Web.UI Imports System.Collections Imports System.Data Namespace myControls <ParseChildren(true)> _ Public Class DataBoundDataSet Inherits Control Private _dataSource As Object Private _dataMember As String = String.Empty Private _itemTemplate As ITemplate Public Property DataSource As Object Get Return _dataSource End Get Set _dataSource = Value End Set End Property Public Property DataMember As String Get Return _dataMember End Get Set _dataMember = Value End Set End Property Private Function GetDataSource( DataSource, DataMember ) As IEnumerable If TypeOf DataSource Is IEnumerable Then Return DataSource Else If TypeOf DataSource Is DataSet Then If DataMember <> String.Empty Then Return DataSource.Tables( DataMember ).DefaultView Else Return DataSource.Tables( 0 ).DefaultView End If Else Throw New ArgumentException( "Invalid data source!" ) End If End Function Protected Overrides Sub OnDataBinding( e As EventArgs ) Dim objDataEnum As IEnumerator Dim objItem As DataBoundDataSetItem Dim intCounter As Integer If Not DataSource Is Nothing Controls.Clear() ClearChildViewState() objDataEnum = GetDataSource( _dataSource, _dataMember ).GetEnumerator() While ( objDataEnum.MoveNext() ) objItem = New DataBoundDataSetItem( objDataEnum.Current ) ItemTemplate.InstantiateIn( objItem ) Controls.Add( objItem ) intCounter += 1 End While ViewState( "NumItems" ) = intCounter ChildControlsCreated = True End If End Sub Protected Overrides Sub CreateChildControls() Dim objNumItems As Object Dim intItemCount As Integer Dim intCounter As Integer Dim objItem As DataBoundDataSetItem objNumItems = ViewState( "NumItems" ) If Not objNumItems = Nothing Then Controls.Clear() intItemCount = CInt( objNumItems) For intCounter = 0 To intItemCount - 1 objItem = New DataBoundDataSetItem( Nothing ) ItemTemplate.InstantiateIn( objItem ) Controls.Add( objItem ) Next End If End Sub <TemplateContainer(GetType(DataBoundDataSetItem))> _ Public Property ItemTemplate As ITemplate Get Return _itemTemplate End Get Set _itemTemplate = value End Set End Property End Class Public Class DataBoundDataSetItem Inherits Control Implements INamingContainer Private _DataItem As Object Public Sub New( DataItem As Object ) MyBase.New() _DataItem = DataItem End Sub Public ReadOnly Property DataItem As Object Get Return _DataItem End Get End Property End Class End Namespace The C# version of this code can be found on the CD-ROM. Listing 29.13 contains a private function named GetDataSource that has two parameters: the data source and a data member. The function does the best it can to return an instance of a data source that supports the IEnumerable interface based on these parameters. If you pass an ArrayList or DataView to the function, the function simply returns the data source cast as IEnumerable . If you pass a DataSet to the function, the function does its best to return a DataTable from the DataSet . For example, if you pass a DataSet without providing a data member parameter, the function returns the first DataTable in the DataSet . NOTE The standard Microsoft data binding controls, such as the Repeater and DataGrid controls, use a private, undocumented function named GetResolvedDataSource . This function is a member of the private DataSourceHelper class. Microsoft's GetResolvedDataSource function is slightly more sophisticated than the GetDataSource function. It uses methods of the IListSource interface to retrieve an enumerator from a DataSet . Since the GetDataSource function explicitly refers to the DataSet class, you need to compile the control with the following statement: vbc /t:library /r:System.dll,System.Web.dll,System.Data.dll,System.Xml.dll DataBoundDataSet.vb You need to add a reference to both the System.Data.dll and System.Xml.dll assemblies when working with DataSets in a control. The page in Listing 29.14 illustrates how you can bind a number of different types of data sources to a custom control that uses the GetDataSource function (see Figure 29.2). Listing 29.14 DisplayDataBoundDataSet.aspx<%@ Register TagPrefix="myControls" Namespace="myControls" Assembly="DataBoundDataSet"%> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <Script Runat="Server"> Sub Page_Load If Not IsPostBack Then Dim conPubs As SqlConnection Dim dadAdapter As SqlDataAdapter Dim dstDataSet As DataSet Dim cmdSelect As SqlCommand Dim colArrayList As ArrayList conPubs = New SqlConnection( "Server=LocalHost;UID=sa;PWD=secret;Database=Pubs" ) conPubs.Open() ' Create a dataset with 2 DataTables dadAdapter = New SqlDataAdapter( "Select top 3 * From Titles", conPubs ) dstDataSet = New DataSet() dadAdapter.Fill( dstDataSet, "Titles" ) dadAdapter.SelectCommand = New SqlCommand( "Select top 3 * From Authors", conPubs ) dadAdapter.Fill( dstDataSet, "Authors" ) ' Bind with DataSet and no DataMember ctrlDataBound1.DataSource = dstDataSet ctrlDataBound1.DataBind() ' Bind with DataSet and explicit DataMember ctrlDataBound2.DataSource = dstDataSet ctrlDataBound2.DataMember = "Authors" ctrlDataBound2.DataBind() ' Bind to DataReader cmdSelect = New SqlCommand( "Select top 3 * From Titles", conPubs ) ctrlDataBound3.DataSource = cmdSelect.ExecuteReader() ctrlDataBound3.DataBind() ' Bind to ArrayList colArrayList = New ArrayList colArrayList.Add( "Milk" ) colArrayList.Add( "Toast" ) ctrlDataBound4.DataSource = colArrayList ctrlDataBound4.DataBind() conPubs.Close End If End Sub </Script> <html> <head><title>DisplayDataBoundDataSet.aspx</title></head> <body> <form Runat="Server"> <h3>DataSet and no DataMember</h2> <myControls:DataBoundDataSet ID="ctrlDataBound1" Runat="Server"> <ItemTemplate> <li> <i><%# Container.DataItem( "Title" ) %></i> </ItemTemplate> </myControls:DataBoundDataSet> <h3>DataSet with DataMember</h2> <myControls:DataBoundDataSet ID="ctrlDataBound2" Runat="Server"> <ItemTemplate> <li> <i><%# Container.DataItem( "au_lname" ) %></i> </ItemTemplate> </myControls:DataBoundDataSet> <h3>DataReader</h2> <myControls:DataBoundDataSet ID="ctrlDataBound3" Runat="Server"> <ItemTemplate> <li> <i><%# Container.DataItem( "Title" ) %></i> </ItemTemplate> </myControls:DataBoundDataSet> <h3>ArrayList</h2> <myControls:DataBoundDataSet ID="ctrlDataBound4" Runat="Server"> <ItemTemplate> <li> <i><%# Container.DataItem %></i> </ItemTemplate> </myControls:DataBoundDataSet> </form> </body> </html> The C# version of this code can be found on the CD-ROM. Figure 29.2. Binding to multiple data sources.
In the Page_Load subroutine in Listing 29.14, the DataBoundDataSet control is bound to four different types of data sources. The control is bound to a DataSet , a DataSet and DataMember , a DataReader , and an ArrayList . |