Walkthrough: Creating a Custom Data-Bound ASP.NET Web Control for ASP.NET 2.0

This walkthrough illustrates how to create a simple data-bound Web server control in ASP.NET version 2.0 and later versions. The data source model enables you to bind data-bound controls to data source controls, permitting common data operations such as paging, sorting, and deleting to be moved out of the data-bound control itself. This data model yields a more flexible data-bound control for page developers and increases the level of reusability. The data source model also supports binding directly to a data collection object. For more information about how to develop custom data-bound controls, see Developing Custom Data-Bound Web Server Controls.

In this walkthrough, you will create a data-bound control that can bind to a data source control or to any object that implements the IEnumerable interface.

Tasks illustrated in this walkthrough include the following:

  • Creating a Web site to test the custom data-bound control.

  • Creating a data-bound control class that extends the DataBoundControl base class. This class displays a table column that represents the bound data. The following provides an overview of what the data-bound control class must include:

    • An override of the PerformSelect method of the base data-bound control class. The code in this method initiates data retrieval.

    • A method with a single parameter of type IEnumerable to receive the returned data. Any data processing that might be required is performed by the code in this method. As a last step, the PerformDataBinding method is called to initiate data binding.

    • An override of the PerformDataBinding method. Code in this method enumerates the retrieved data and child controls are added to represent the data.

  • Registering the control in the Web.config file.

  • Testing the control in an ASP.NET Web page.

  • Compiling the control so that you can distribute it as binary code.

  • Testing the compiled custom data-bound server control.

Creating a Web Site to Test the Control

You can use ASP.NET dynamic compilation to test your control in a page without compiling the control into an assembly. ASP.NET dynamically compiles code that is in the App_Code directory under an ASP.NET Web site's root. Classes in source files in App_Code can therefore be accessed from pages without being manually compiled into assemblies.

To create a Web site to test custom data-bound controls

  1. Create a Web site named ServerControlsTest.

    You can create the site in IIS as a virtual directory named ServerControlsTest. For details about how to create and configure an IIS virtual directory, see How to: Create and Configure Virtual Directories in IIS 5.0 and 6.0.

  2. Create an App_Code directory directly under the root directory of your Web site (the Web application root).

Creating the SimpleDataBoundColumn Class

In this section, you will create the data-bound control class that extends the DataBoundControl class. The new control displays the bound data in a one-column table.

To create the SimpleDataBoundColumn class

  1. In the App_Code folder, create a class named SimpleDataBoundColumn.cs or SimpleDataBoundColumn.vb.

  2. Add the following code to your class file:

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.Security.Permissions
    Imports System.Web
    Imports System.Web.UI
    Imports System.Web.UI.WebControls
    
    Namespace Samples.AspNet.Controls.VB
    
        <AspNetHostingPermission(SecurityAction.Demand, _
            Level:=AspNetHostingPermissionLevel.Minimal), _
            AspNetHostingPermission(SecurityAction.InheritanceDemand, _
            Level:=AspNetHostingPermissionLevel.Minimal)> _
        Public Class SimpleDataBoundColumn
            Inherits DataBoundControl
    
            Public Property DataTextField() As String 
                Get 
                    Dim o As Object = ViewState("DataTextField")
                    If o Is Nothing Then 
                        Return String.Empty
                    Else 
                        Return CStr(o)
                    End If 
                End Get 
                Set(ByVal value As String)
                    ViewState("DataTextField") = value
                    If (Initialized) Then
                        OnDataPropertyChanged()
                    End If 
                End Set 
            End Property 
    
            Protected Overrides Sub PerformSelect()
                ' Call OnDataBinding here if bound to a data source using the 
                ' DataSource property (instead of a DataSourceID), because the 
                ' databinding statement is evaluated before the call to GetData.        
                If Not IsBoundUsingDataSourceID Then
                    OnDataBinding(EventArgs.Empty)
                End If 
    
                ' The GetData method retrieves the DataSourceView object from   
                ' the IDataSource associated with the data-bound control.            
                GetData().Select(CreateDataSourceSelectArguments(), _
                    AddressOf OnDataSourceViewSelectCallback)
    
                ' The PerformDataBinding method has completed.
                RequiresDataBinding = False
                MarkAsDataBound()
    
                ' Raise the DataBound event.
                OnDataBound(EventArgs.Empty)
    
            End Sub 
    
            Private Sub OnDataSourceViewSelectCallback(ByVal retrievedData As IEnumerable)
                ' Call OnDataBinding only if it has not already been  
                ' called in the PerformSelect method. 
                If IsBoundUsingDataSourceID Then
                    OnDataBinding(EventArgs.Empty)
                End If 
                ' The PerformDataBinding method binds the data in the   
                ' retrievedData collection to elements of the data-bound control.
                PerformDataBinding(retrievedData)
    
            End Sub 
    
            Protected Overrides Sub PerformDataBinding(ByVal retrievedData As IEnumerable)
                MyBase.PerformDataBinding(retrievedData)
    
                ' Verify data exists. 
                If Not (retrievedData Is Nothing) Then 
                    Dim tbl As New Table()
                    Dim row As TableRow
                    Dim cell As TableCell
                    Dim dataStr As String = String.Empty
    
                    Dim dataItem As Object 
                    For Each dataItem In retrievedData
                        ' If the DataTextField was specified get the data 
                        ' from that field, otherwise get the data from the first field.  
                        If DataTextField.Length > 0 Then
                            dataStr = DataBinder.GetPropertyValue(dataItem, DataTextField, Nothing)
                        Else 
                            Dim props As PropertyDescriptorCollection = TypeDescriptor.GetProperties(dataItem)
                            If props.Count >= 1 Then 
                                If Nothing <> props(0).GetValue(dataItem) Then
                                    dataStr = props(0).GetValue(dataItem).ToString()
                                End If 
                            End If 
                        End If
    
                        row = New TableRow()
                        tbl.Rows.Add(row)
                        cell = New TableCell()
                        cell.Text = dataStr
                        row.Cells.Add(cell)
                    Next dataItem
    
                    Controls.Add(tbl)
                End If 
    
            End Sub 
        End Class 
    End Namespace
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.Security.Permissions;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    namespace Samples.AspNet.Controls.CS
    {
        [AspNetHostingPermission(SecurityAction.Demand,
           Level = AspNetHostingPermissionLevel.Minimal)]
        [AspNetHostingPermission(SecurityAction.InheritanceDemand,
            Level = AspNetHostingPermissionLevel.Minimal)]
        public class SimpleDataBoundColumn : DataBoundControl
        {
            public string DataTextField
            {
                get
                {
                    object o = ViewState["DataTextField"];
                    return ((o == null) ? string.Empty : (string)o);
                }
                set
                {
                    ViewState["DataTextField"] = value;
                    if (Initialized)
                    {
                        OnDataPropertyChanged();
                    }
                }
            }
    
            protected override void PerformSelect()
            {
                // Call OnDataBinding here if bound to a data source using the 
                // DataSource property (instead of a DataSourceID), because the 
                // databinding statement is evaluated before the call to GetData.        
                if (!IsBoundUsingDataSourceID)
                {
                    this.OnDataBinding(EventArgs.Empty);
                }
    
                // The GetData method retrieves the DataSourceView object from   
                // the IDataSource associated with the data-bound control.            
                GetData().Select(CreateDataSourceSelectArguments(),
                    this.OnDataSourceViewSelectCallback);
    
                // The PerformDataBinding method has completed.
                RequiresDataBinding = false;
                MarkAsDataBound();
    
                // Raise the DataBound event.
                OnDataBound(EventArgs.Empty);
            }
    
            private void OnDataSourceViewSelectCallback(IEnumerable retrievedData)
            {
                // Call OnDataBinding only if it has not already been  
                // called in the PerformSelect method. 
                if (IsBoundUsingDataSourceID)
                {
                    OnDataBinding(EventArgs.Empty);
                }
                // The PerformDataBinding method binds the data in the   
                // retrievedData collection to elements of the data-bound control.
                PerformDataBinding(retrievedData);
            }
    
            protected override void PerformDataBinding(IEnumerable retrievedData)
            {
                base.PerformDataBinding(retrievedData);
    
                // Verify data exists. 
                if (retrievedData != null)
                {
                    Table tbl = new Table();
                    TableRow row;
                    TableCell cell;
                    string dataStr = String.Empty;
    
                    foreach (object dataItem in retrievedData)
                    {
                        // If the DataTextField was specified get the data 
                        // from that field, otherwise get the data from the first field.  
                        if (DataTextField.Length > 0)
                        {
                            dataStr = DataBinder.GetPropertyValue(dataItem,
                                DataTextField, null);
                        }
                        else
                        {
                            PropertyDescriptorCollection props =
                                    TypeDescriptor.GetProperties(dataItem);
                            if (props.Count >= 1)
                            {
                                if (null != props[0].GetValue(dataItem))
                                {
                                    dataStr = props[0].GetValue(dataItem).ToString();
                                }
                            }
                        }
    
                        row = new TableRow();
                        tbl.Rows.Add(row);
                        cell = new TableCell();
                        cell.Text = dataStr;
                        row.Cells.Add(cell);
                    }
    
                    this.Controls.Add(tbl); 
                }
            }
        }
    }
    

Code Discussion

The SimpleDataBoundColumn class derives from the base DataBoundControl data-binding class. Deriving from this base class provides the DataSourceID, DataSource, and DataMember data-binding properties. These properties enable a page developer to specify the data source and a specific data member to bind to this custom control.

To illustrate how to add custom data-binding properties, a DataTextField property has been added to the SimpleDataBoundColumn class. When the page developer sets the DataTextField property, the new value is stored in view state. The code in the property definition also calls the OnDataPropertyChanged method if the control has already been initialized. This forces the data-bound control to re-bind the data so that the control can use the new data-binding property setting.

The overridden DataBind method is required, and it includes logic to enumerate the object in the associated data source and create the child controls. The following tasks must be performed in ASP.NET data-bound controls in the overridden DataBind method, as shown in the SimpleDataBoundColumn class:

  • The IsBoundUsingDataSourceID property is checked for a value of false to determine whether the data source is specified in the DataSource property.

  • If the data to be bound is specified in the DataSource property, the OnDataBinding method is called to bind the data member that is specified in the DataSource property.

  • The GetData method is called to retrieve the DataSourceView object that is associated with the data-bound control.

  • The Select method of the retrieved DataSourceView object is called to initiate data retrieval and specify the OnDataSourceViewSelectCallback callback method that will handle the retrieved data.

  • To indicate that the data-retrieval tasks of the PerformSelect method are complete, the RequiresDataBinding property is set to false and then the MarkAsDataBound method is called.

  • The OnDataBound event is raised.

The OnDataSourceViewSelectCallback method receives the retrieved data. This callback method must accept a single parameter of type IEnumerable. Any data processing should occur here if the custom control requires it. In this walkthrough, the custom control uses the data as it is. Therefore, no additional data processing occurs in this example. As a last step, the PerformDataBinding method is called to start the data-binding process.

In the override of the PerformDataBinding method, all child controls are created that will represent the data. In this example, the data collection is enumerated and a new TableCell object is created for each data item. If the DataTextField property is set, the property value is used to determine which data field is bound to the cell's Text property. Otherwise, the first field of the data item is used.

The parent Table control is added to the custom SimpleDataBoundColumn control's Controls collection. Any control that is added to the control's Controls collection is automatically rendered when the inherited Render method executes.

For more information about the required implementations of a data-bound Web server control, see Developing Custom Data-Bound Web Server Controls.

Creating a Tag Prefix

A tag prefix is the prefix, such as "asp" in <asp:Table />, that appears before a control's type name when the control is created declaratively in a page. To enable your control to be used declaratively in a page, ASP.NET requires a tag prefix that is mapped to your control's namespace. A page developer can provide a tag prefix/namespace mapping by adding an @ Register directive on each page that uses the custom control, as in the following example:

<%@ Register TagPrefix="aspSample" 
    Namespace="Samples.AspNet.Controls.CS"%>
<%@ Register TagPrefix="aspSample" 
    Namespace="Samples.AspNet.Controls.VB"%>

Note

In this example, the @ Register directive does not contain an assembly attribute that specifies the name of the control assembly. When the assembly attribute is missing, ASP.NET infers that the assembly is dynamically compiled from source files in the App_Code directory.

As an alternative to using the @ Register directive in each .aspx page, the page developer can specify the tag prefix and namespace mapping in the Web.config file. This is useful if the custom control will be used in multiple pages in a Web application. The following procedure describes how to specify the tag prefix mapping in the Web.config file.

To add a tag prefix mapping in the Web.config file

  1. If the site does not already contain a Web.config file, create one in the Web site's root folder.

  2. If you created a new Web.config file, copy the following XML markup to the file and save the file. If your site already had a Web.config file, add the following highlighted element to it.

    Note

    The tag prefix entry must be a child of the controls section, which must be under the pages section, which in turn must be a child of system.web.

    <?xml version="1.0"?>
    <configuration>
      <system.web>
        <pages>
          <controls>
    <add tagPrefix="aspSample" namespace="Samples.AspNet.Controls.cs" />
          </controls>
        </pages>
      </system.web>
    </configuration>
    
    <?xml version="1.0"?>
    <configuration>
      <system.web>
        <pages>
          <controls>
    <add tagPrefix="aspSample" namespace="Samples.AspNet.Controls.vb" />
          </controls>
        </pages>
      </system.web>
    </configuration>
    

    The highlighted section shows a tag prefix entry that maps the tag prefix "aspSample" to the namespace Samples.AspNet.Controls.CS or Samples.AspNet.Controls.VB.

After you have specified the tag prefix mapping in the configuration file, you can use the SimpleDataBoundColumn control declaratively (as <aspSample:SimpleDataBoundColumn />) in any page in the Web site.

Creating a Page to Use the Custom Data-Bound Control

In this section of the walkthrough you will create page markup that lets you test the custom data-bound control.

To create a page that uses the custom data-bound control

  1. In the Web site, create a file named TestSimpleDataBoundColumn.aspx.

  2. Copy the following markup into the TestSimpleDataBoundColumn.aspx file and save the file.

    <%@ Page Language="VB" Trace="true"%>
    <%@ Register TagPrefix="aspSample" Namespace="Samples.AspNet.Controls.VB" %>
    <%@ Import Namespace="System.Data" %>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="https://www.w3.org/1999/xhtml">
    <script runat="server">
             Function CreateDataSource() As ICollection 
    
             ' Create sample data for the DataList control.
             Dim dt As DataTable = New DataTable()
             dim dr As DataRow
    
             ' Define the columns of the table.
             dt.Columns.Add(New DataColumn("IntegerValue", GetType(Int32)))
             dt.Columns.Add(New DataColumn("StringValue", GetType(String)))
             dt.Columns.Add(New DataColumn("CurrencyValue", GetType(Double)))
             dt.Columns.Add(New DataColumn("ImageValue", GetType(String)))
    
             ' Populate the table with sample values.
             Dim i As Integer
    
             For i = 0 To 8 
    
                dr = dt.NewRow()
    
                dr(0) = i
                dr(1) = "Description for item " & i.ToString()
                dr(2) = 1.23 * (i + 1)
                dr(3) = "Image" & i.ToString() & ".jpg"
    
                dt.Rows.Add(dr)
    
             Next i
    
             Dim dv As DataView = New DataView(dt)
             Return dv
    
          End Function
    
          Sub Page_Load(sender As Object, e As EventArgs) 
    
             ' Load sample data only once, when the page is first loaded.
             If Not IsPostBack Then 
    
                simpleDataBoundColumn1.DataSource = CreateDataSource()
                simpleDataBoundColumn1.DataBind()
    
             End If
    
          End Sub
    
    </script>
    
    <head runat="server">
        <title>SimpleDataBoundColumn test page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <aspSample:SimpleDataBoundColumn runat="server" id="simpleDataBoundColumn1" DataTextField="CurrencyValue" BorderStyle="Solid"></aspSample:SimpleDataBoundColumn>
        </div>
        </form>
    </body>
    </html>
    
    <%@ Page Language="C#" Trace="true"%>
    <%@ Register TagPrefix="aspSample" Namespace="Samples.AspNet.Controls.CS" %>
    <%@ Import Namespace="System.Data" %>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="https://www.w3.org/1999/xhtml">
    <script runat="server">
    
          ICollection CreateDataSource() 
          {
             DataTable dt = new DataTable();
             DataRow dr;
    
             dt.Columns.Add(new DataColumn("IntegerValue", typeof(Int32)));
             dt.Columns.Add(new DataColumn("StringValue", typeof(string)));
             dt.Columns.Add(new DataColumn("CurrencyValue", typeof(double)));
    
             for (int i = 0; i < 9; i++) 
             {
                dr = dt.NewRow();
    
                dr[0] = i;
                dr[1] = "Item " + i.ToString();
                dr[2] = 1.23 * (i + 1);
    
                dt.Rows.Add(dr);
             }
    
             DataView dv = new DataView(dt);
             return dv;
          }
    
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                simpleDataBoundColumn1.DataSource = CreateDataSource();
                simpleDataBoundColumn1.DataBind();
            }
        }
    </script>
    
    <head runat="server">
        <title>SimpleDataBoundColumn test page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <aspSample:SimpleDataBoundColumn runat="server" id="simpleDataBoundColumn1" DataTextField="CurrencyValue" BorderStyle="Solid"></aspSample:SimpleDataBoundColumn>
        </div>
        </form>
    </body>
    </html>
    
  3. Run the SimpleDataBoundColumnTest.aspx page.

    The data is displayed by the custom control.

Compiling the Control into an Assembly

The App_Code directory enables you to test your control without compiling it. However, if you want to distribute your control as object code to other developers, you must compile it. In addition, a control cannot be added to the toolbox of a visual designer unless it is compiled into an assembly.

To compile the control into an assembly

  1. Open the Visual Studio Command Prompt window. For more information, see Command-line Building With csc.exe.

  2. In the Command window, browse to the App_Code directory for your Web site.

  3. Run the following command:

    csc /t:library /out:Samples.AspNet.Controls.CS.dll /r:System.dll /r:System.Web.dll *.cs
    
    vbc /t:library /out:Samples.AspNet.Controls.VB.dll /r:System.dll /r:System.Web.dll *.vb
    

    This command generates an assembly named Samples.AspNet.Controls.CS.dll or Samples.AspNet.Controls.VB.dll in the App_Code directory.

    The /t:library compiler option tells the compiler to create a library instead of an executable assembly. The /out option provides a name for the assembly and the /r option lists the assemblies that are linked to your assembly.

    Note

    To keep the example self-contained, this walkthrough asks you to create an assembly with a single control. The .NET Framework design guidelines recommend that you do not create assemblies that contain only a few classes. For ease of deployment, you should create as few assemblies as possible.

Using the TagPrefixAttribute to Provide a Tag Prefix/Namespace Mapping

Earlier you saw how a page developer can specify a tag prefix in the page or in the Web.config file. When you compile a control, you can optionally suggest a default tag prefix that a visual designer should use for your control. You do this by including the assembly-level System.Web.UI.TagPrefixAttribute attribute. The TagPrefixAttribute attribute is useful because it provides a tag prefix for a visual designer to use if the designer does not find a tag prefix mapping in the Web.config file or in an @ Register directive in the page. The tag prefix is registered with the page the first time the control is double-clicked in the toolbox or dragged from the toolbox onto the page.

To add a tag prefix mapping using the TagPrefixAttribute

  1. Create an empty text file named AssemblyInfo.cs or AssemblyInfo.vb in the root folder of the Web site and add the following code to the file.

    using System;
    using System.Web.UI;
    [assembly: TagPrefix("Samples.AspNet.Controls.CS", "aspSample")]
    
    Imports System
    Imports System.Web.UI
    <Assembly: TagPrefix("Samples.AspNet.Controls.VB", "aspSample")> 
    

    The tag prefix attribute creates a mapping between the prefix "aspSample" and the namespace Samples.AspNet.Controls.CS or Samples.AspNet.Controls.VB.

  2. Recompile all the source files by using the compilation command you used earlier.

Using the Compiled Custom Data-Bound Server Control in an ASP.NET Page

To test the compiled version of your custom control, you must make your control's assembly available to pages in the Web site.

To make the control's assembly available to the Web site

  1. Create a Bin directory under the root folder of the Web site.

  2. Drag the control assembly (Samples.AspNet.Controls.CS.dll or Samples.AspNet.Controls.VB.dll) from the App_Code directory to the Bin directory.

  3. Delete the control's source file from the App_Code directory.

    If you do not delete the source files, your control's type will exist in both the compiled assembly and in the dynamically generated assembly created by ASP.NET. This will create an ambiguous reference when the page loads the control, and any page in which the control is used will generate a compiler error.

The assembly that you created in this walkthrough is referred to as a private assembly because it must be put in an ASP.NET Web site's Bin directory to enable pages in the Web site to use your control. The assembly cannot be accessed from other applications unless a copy is also installed with those applications. If you are creating controls for shared Web hosting applications, you will typically package your controls in a private assembly. However, if you create controls for use in a dedicated hosting environment, or if you create a suite of controls that an ISP makes available to all its customers, you might have to package your controls in a shared (strongly named) assembly that is installed in the global assembly cache. For more information, see Working with Assemblies and the Global Assembly Cache.

The next step is to change the tag prefix mapping that you created in the Web.config file so that the mapping specifies your control's assembly name.

To modify the tag prefix mapping

  1. If you registered the tag prefix in the Web page, add the assembly attribute to the @ Register directive, as shown in the following example:

    [C#]

    <%@ Register TagPrefix="aspSample" 
        Namespace="Samples.AspNet.Controls.CS" 
        assembly="Samples.AspNet.Controls.CS" %>
    
    <%@ Register TagPrefix="aspSample" 
        Namespace="Samples.AspNet.Controls.VB" 
        assembly="Samples.AspNet.Controls.VB" %>
    
  2. If you registered the tag prefix in the Web.config file, add an assembly attribute to the add element for tagPrefix, as shown in the following example:

    <controls>
      <add tagPrefix="aspSample" namespace="Samples.AspNet.Controls.CS" assembly="Samples.AspNet.Controls.CS" />
    </controls>
    
    <controls>
      <add tagPrefix="aspSample" namespace="Samples.AspNet.Controls.VB" assembly="Samples.AspNet.Controls.VB" />
    </controls>
    
  3. Run the SimpleDataBoundColumnTest.aspx page.

The assembly attribute specifies the name of the assembly that the control is in. An add element fortagPrefix maps a tag prefix to a namespace and assembly combination. When the assembly is dynamically generated by ASP.NET from source files in the App_Code directory, the assembly attribute is not required. When the assembly attribute is not used, ASP.NET loads the control's type from the assemblies dynamically generated from the App_Code directory.

If you use your control in a visual designer such as Visual Studio, you will be able to add your control to the toolbox, drag it from the toolbox to the design surface, and access properties and events in the property browser. In addition, in Visual Studio, your control has full IntelliSense support in Source view of the page designer and in the code editor. This includes statement completion in a script block as well as property browser support when a page developer clicks the control's tag.

Next Steps

This simple custom data-bound server control illustrates the fundamental steps used to create a custom control that gives a page developer a standard, efficient, and flexible way to bind it to an external data source. From here, you can begin to explore the framework provided in Visual Studio to help you create sophisticated, custom server controls. Suggestions for more exploration include the following:

See Also

Tasks

Walkthrough: Developing and Using a Custom Web Server Control

Reference

HierarchicalDataBoundControlDesigner

Concepts

Developing Custom Data-Bound Web Server Controls

Data-Bound Web Server Controls

Metadata Attributes for Custom Server Controls

ASP.NET Control Designers Overview

Other Resources

Developing Custom ASP.NET Server Controls