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. In ASP.NET 2.0, a new 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 ASP.NET 2.0 data source model also continues to support binding directly to a data collection object. For more information about developing custom data-bound controls for the ASP.NET 2.0 data model, see Developing Custom Data-Bound Web Server Controls for ASP.NET 2.0.

In this walkthrough, you will create a data-bound control that can bind to a data source control or 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 with dynamic compilation.

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

    • An override of the PerformSelect method of the base data-bound control class. Within this method tasks are performed to initiate data retrieval.

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

    • An override of the PerformDataBinding method. Within this method the retrieved data is enumerated 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 placed in the App_Code directory under an ASP.NET Web site's root. Classes in source files in App_Code can thus be accessed from pages without being manually compiled into assemblies.

NoteNote

The App_Code directory is a new feature that was not available in ASP.NET 1.0 and 1.1. Using the App_Code directory for initial control testing is optional. The main steps in building a server control are the same as in earlier versions, as you will see in the next section, "Compiling the Control into an Assembly."

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 creating and configuring an IIS virtual directory, see How to: Create and Configure Virtual Directories in IIS.

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

Creating the SimpleDataBoundColumn Class

To create the SimpleDataBoundColumn class

  1. In the App_Code folder you already created in the previous step, 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 data-binding class DataBoundControl. Deriving from this base class provides the exposed DataSourceID, DataSource, and DataMember data-binding properties. These exposed 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 additional 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. It 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 creates logic to enumerate the object in the associated data source and creates the child controls. The following tasks are required by ASP.NET 2.0 data-bound controls to be performed within the overridden DataBind method, as demonstrated 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, then 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 callback method receives the retrieved data. This callback method must accept a single parameter of the type IEnumerable. Any data processing should occur here if the custom control requires it. This custom control uses the data as it is and so no additional data processing occurs in this example. As a last step, the PerformDataBinding method is called to start the data binding process.

Within an override of the PerformDataBinding method, all child controls are created that will represent the data. The data collection is enumerated and a new TableCell for each enumerated data item is created. If the DataTextField property is set, then it is used to determine which field of data will be bound to the cell's Text property; otherwise, the first field 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 while 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 for ASP.NET 2.0.

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 needs a tag prefix that is mapped to your control's namespace. A page developer can provide a tag prefix/namespace mapping by adding a @ 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"%>
NoteNote

The @ Register directive in ASP.NET 2.0 is the same as that in ASP.NET 1.0 and ASP.NET 1.1. If you are familiar with the @ Register directive in earlier versions of ASP.NET, you might notice that the assembly attribute that specifies the name of the control assembly is missing in the preceding @ Register directive. 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 your Web site does not already have one, create a file named Web.config in the Web site's root folder.

  2. If you created a new (empty) 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.

    NoteNote

    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">
            </add>
          </controls>
        </pages>
      </system.web>
    </configuration>
    
    <?xml version="1.0"?>
    <configuration>
      <system.web>
        <pages>
          <controls>
            <add tagPrefix="aspSample"
              namespace="Samples.AspNet.Controls.VB">
            </add>
          </controls>
        </pages>
      </system.web>
    </configuration>
    

    The highlighted section shows a tag prefix entry, which 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.

NoteNote

A configuration entry for the tag prefix is new feature of ASP.NET 2.0. In ASP.NET 1.0 and 1.1 the tag prefix mapping was specified in the @ Register directive in each page that used the custom control.

Creating a Page to Use the Custom Data-Bound Control

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

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

  1. Create a file named TestSimpleDataBoundColumn.aspx in your Web site.

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

<%@ 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"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://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>
  1. Run the SimpleDataBoundColumnTest.aspx page.

  2. Make some change to the source code for the custom control. For example, write an additional string by adding this line of code at the end of the RenderContents method:

    writer.Write("<br />Testing how the App_Code directory works.");
    
    writer.Write("<br />Testing how the App_Code directory works.")
    
  3. Refresh the SimpleDataBoundColumnTest.aspx page in your browser.

    You will see that changes in the control are reflected in the page even though you did not compile the control.

Compiling the Control into an Assembly

Although the App_Code directory enables you to test your control without compiling it, 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. Set the Windows environment PATH variable of your computer to include the path to your .NET Framework installation by following these steps:

    1. In Windows, right-click My Computer, select Properties, click the Advanced tab, and click the Environment Variables button.

    2. In the System variables list, double-click the Path variable.

    3. In the Variable value text box, add a semicolon (;) to the end of the existing values in the text box, and then type the path of your .NET Framework installation. The .NET Framework is generally installed in the Windows installation directory at \Microsoft.NET\Framework\versionNumber.

    4. Click OK to close each dialog box.

  2. Run the following command from the App_Code directory containing your source files created in a previous procedure of this walkthrough. This will generate an assembly named Samples.AspNet.Controls.CS.dll or Samples.AspNet.Controls.VB.dll in the same App_Code directory.

    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
    

    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.

    NoteNote

    To keep the example self-contained, this walkthrough asks you to create an assembly with a single control. In general, 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 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 a @ 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.

If you decide to use the TagPrefixAttribute attribute, you can specify it in a separate file that is compiled with your controls. By convention, the file is named AssemblyInfo.languageExtension, such as AssemblyInfo.cs or AssembyInfo.vb. The following procedure describes how to specify the TagPrefixAttribute metadata.

NoteNote

If you do not specify the TagPrefixAttribute in the control's assembly, and the page developer does not specify the tag prefix/namespace mapping in the page or in the Web.config file, the visual designer might create a default tag prefix. For example, Visual Studio 2005 will create its own tag (such as cc1) for your control when the control is dragged from the toolbox.

To add a tag prefix mapping using the TagPrefixAttribute

  1. Create a file named AssemblyInfo.cs or AssemblyInfo.vb in your source code directory 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 namespace Samples.AspNet.Controls.CS or Samples.AspNet.Controls.VB and the prefix aspSample.

  2. Recompile all the source files using the compilation command you used earlier (with or without the embedded resource).

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 accessible to pages in the Web site.

To make your control's assembly accessible to the Web site

  1. Create a Bin directory under the root 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 loading your control and any page in which the control is used will generate a compiler error.

The assembly that you created in this how-to is called a private assembly because it must be placed 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 you create a suite of controls that an ISP makes available to all its customers, you might need 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.

Next, you must modify the tag prefix mapping you created in Web.config to specify your control's assembly name.

To modify the tag prefix mapping in Web.config

  • Edit the Web.config file to add an assembly attribute to the an add tagPrefix element:

    <controls>
      <add tagPrefix="aspSample"
        namespace="Samples.AspNet.Controls.CS" 
        assembly="Samples.AspNet.Controls.CS">
      </add>
    </controls>
    
    <controls>
      <add tagPrefix="aspSample"   
        namespace="Samples.AspNet.Controls.VB" 
        assembly="Samples.AspNet.Controls.VB">
      </add>
    </controls>
    

The assembly attribute specifies the name of the assembly the control is in. An add tagPrefix element 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 necessary. When the assembly attribute is not used, ASP.NET loads the control's type from the assemblies dynamically generated from the App_Code directory.

To view the page that uses the custom control

  • Display the SimpleDataBoundColumnTest.aspx page in your browser by entering the following URL in the address bar:

    https://localhost/ServerControlsTest/ SimpleDataBoundColumnTest.aspx
    

If you use your control in a visual designer such as Visual Studio 2005, 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 2005, 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.

NoteNote

In many visual designers, you can add custom controls to the designer's toolbox. For details, consult the documentation for your designer.

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 Server Control

Reference

HierarchicalDataBoundControlDesigner

Concepts

Developing Custom Data-Bound Web Server Controls for ASP.NET 2.0
ASP.NET Data-Bound Web Server Controls Overview
Metadata Attributes for Custom Server Controls
ASP.NET Control Designers Overview

Other Resources

Developing Custom ASP.NET Server Controls