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

This walkthrough illustrates how to create a simple data-bound server control that exposes a bindable data source.

An ASP.NET data-bound server control provides a user interface (UI) for a data source representing a collection of records or items. The DataList and Repeater server controls are examples of data-bound server controls. For more information on data-bound controls provided with ASP.NET, see ASP.NET Data-Bound Web Server Controls Overview.

Note

The control illustrated in this walkthrough is compatible with ASP.NET version 1.1. For more information creating custom data-bound Web controls for ASP.NET 2.0, see Developing Custom Data-Bound Web Server Controls for ASP.NET 2.0.

An ASP.NET 1.1 data-bound server control can bind to any of the following types:

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

Tasks illustrated in this walkthrough include:

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

  • Creating a data-bound control class. The class must expose a property of type IListSource or IEnumerable, enabling a page developer to specify the location of the data to be bound. It must also override the base class's DataBind and CreateChildControls methods.

  • 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 in the Web site root. Classes in source files in App_Code can therefore be accessed from pages without being manually compiled into assemblies.

Note

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 described in the 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 5.0 and 6.0.

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

Creating the SimpleDataBoundControl Class

The next step is to create the control class.

To create the SimpleDataBoundControl class

  1. In the App_Code folder created in the previous procedure, create a class named SimpleDataBoundControl.cs or SimpleDataBoundControl.vb, depending on what programming language you want to use.

  2. Add the following code to your class file.

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.Web
    Imports System.Web.UI
    Imports System.Web.UI.WebControls
    
    Namespace Samples.ASPNet.VB.Controls
        <DefaultProperty("DataSource")> _
            Public Class SimpleDataBoundControl
            Inherits WebControl
    
            Private _dataSource As IEnumerable
            Private _label As Label
            Private _button As Button
            Private _table As Table
    
            <Category("Data"), DefaultValue(""), Description("An exposed data source: A public member of type IEnumerable to bind to such as an Array, ArrayList or Hashtable.")> _
            Public Overridable Property DataSource() As IEnumerable
                Get
                    Return _dataSource
                End Get
                Set(ByVal value As IEnumerable)
                    If TypeOf value Is IEnumerable OrElse value Is Nothing Then
                        _dataSource = value
                    Else
                        Throw New ArgumentException()
                    End If
                End Set
            End Property
    
            Protected Overridable Function GetDataSource() As IEnumerable
                If _dataSource Is Nothing Then
                    Return Nothing
                End If
    
                Dim resolvedDataSource As IEnumerable
                resolvedDataSource = _dataSource
    
                Return resolvedDataSource
    
            End Function 'GetDataSource
    
            Protected Overridable Sub CreateMyControlHeirarchy(ByVal useViewState As Boolean)
                Dim resolvedDataSource As IEnumerable = Nothing
                If useViewState Then
                    If Not (ViewState("RowCount") Is Nothing) Then
                        resolvedDataSource = New Object(Fix(ViewState("RowCount"))) {}
                    Else
                        Throw New Exception("Unable to retrieve expected data from ViewState")
                    End If
                Else
                    resolvedDataSource = GetDataSource()
                End If
    
                If Not (resolvedDataSource Is Nothing) Then
                    ' Create a label that will indicate form which source the data has been provided.
                    Dim s As String
                    If useViewState Then
                        s = "Data collection retrieved from ViewState:"
                    Else
                        s = "Data collection retrieved from bound data source:"
                    End If
                    _label = New Label()
                    Me.Controls.Add(Me._label)
                    _label.Text = s
    
                    _button = New Button()
                    Me.Controls.Add(Me._button)
                    _button.Text = "Test re-binding of ViewState"
    
                    _table = New Table()
                    Me.Controls.Add(Me._table)
    
                    Dim dataItem As Object
                    For Each dataItem In resolvedDataSource
                        Dim row As New TableRow()
                        _table.Rows.Add(row)
                        Dim cell As New TableCell()
                        If Not useViewState Then
                            cell.Text = dataItem.ToString()
                        End If
                        row.Cells.Add(cell)
                    Next dataItem
    
                    ViewState("RowCount") = _table.Rows.Count
                End If
    
            End Sub 'CreateMyControlHeirarchy
    
            Protected Overrides Sub CreateChildControls()
                Controls.Clear()
    
                If Not (ViewState("RowCount") Is Nothing) Then
                    Dim useViewState As Boolean = True
                    CreateMyControlHeirarchy(useViewState)
                End If
    
            End Sub 'CreateChildControls
    
            Public Overrides Sub DataBind()
                MyBase.OnDataBinding(EventArgs.Empty)
    
                Controls.Clear()
                ClearChildViewState()
                TrackViewState()
    
                Dim useViewState As Boolean = False
                CreateMyControlHeirarchy(useViewState)
    
                ChildControlsCreated = True
            End Sub 'DataBind
    
            Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
                Me._label.RenderControl(writer)
                Me._table.RenderControl(writer)
                Me._button.RenderControl(writer)
            End Sub 'RenderContents
        End Class 'SimpleDataBoundControl
    End Namespace
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    namespace Samples.AspNet.CS.Controls
    {
        [
            DefaultProperty("DataSource")
        ]
        public class SimpleDataBoundControl : WebControl
        {
            private IEnumerable _dataSource;
            private Label _label;
            private Button _button;
            private Table _table;
    
            [
            Category("Data"),
            DefaultValue(""),
            Description("An exposed data source: A public member of type IEnumerable to bind to such as an Array, ArrayList or Hashtable.")
            ]
            public virtual IEnumerable DataSource
            {
                get
                {
                    return _dataSource;
                }
                set
                {
                    if ((value is IEnumerable) || (value == null))
                    {
                        _dataSource = value;
                    }
                    else
                    {
                        throw new ArgumentException();
                    }
                }
            }
    
            protected virtual IEnumerable GetDataSource()
            {
                if (_dataSource == null)
                {
                    return null;
                }
    
                IEnumerable resolvedDataSource;
                resolvedDataSource = _dataSource as IEnumerable;
    
                return resolvedDataSource;
            }
    
            protected virtual void CreateMyControlHeirarchy(bool useViewState)
            {
                IEnumerable resolvedDataSource = null;
                if (useViewState)
                {
                    if (ViewState["RowCount"] != null)
                    {
                        resolvedDataSource = new object[(int)ViewState["RowCount"]];
                    }
                    else
                    {
                        throw new Exception("Unable to retrieve expected data from ViewState");
                    }
                }
                else
                {
                    resolvedDataSource = GetDataSource();
                }
    
                if (resolvedDataSource != null)
                {
                    // Create a label that will indicate form which source the data has been provided.
                    String s;
                    if (useViewState)
                    {
                        s = "Data collection retrieved from ViewState:";
                    }
                    else
                    {
                        s = "Data collection retrieved from bound data source:";
                    }
                    _label = new Label();
                    this.Controls.Add(this._label);
                    _label.Text = s;
    
                    _button = new Button();
                    this.Controls.Add(this._button);
                    _button.Text = "Test re-binding of ViewState";
    
                    _table = new Table();
                    this.Controls.Add(this._table);
    
                    foreach (object dataItem in resolvedDataSource)
                    {
                        TableRow row = new TableRow();
                        _table.Rows.Add(row);
                        TableCell cell = new TableCell();
                        if (!useViewState)
                        {
                            cell.Text = dataItem.ToString();
                        }
                        row.Cells.Add(cell);
                    }
    
                    ViewState["RowCount"] = _table.Rows.Count;
                }
            }
    
            protected override void CreateChildControls()
            {
                Controls.Clear();
    
                if (ViewState["RowCount"] != null)
                {
                    bool useViewState = true;
                    CreateMyControlHeirarchy(useViewState);
                }
            }
    
            public override void DataBind()
            {
                base.OnDataBinding(EventArgs.Empty);
    
                Controls.Clear();
    
                ClearChildViewState();
    
                TrackViewState();
    
                bool useViewState = false;
                CreateMyControlHeirarchy(useViewState);
    
                ChildControlsCreated = true;
            }
    
            protected override void RenderContents(HtmlTextWriter writer)
            {
                this._label.RenderControl(writer);
                this._table.RenderControl(writer);
                this._button.RenderControl(writer);
            }
        }
    }
    

Code Discussion

The SimpleDataBoundControl class renders an HTML table based on the data collection specified in its DataSource property. ASP.NET 1.1 data-bound Web server controls must expose a bindable DataSource property that can be set to either an IEnumerable or an IListSource type.

In the example code, the set accessor for the DataSource property verifies that the value to set is either null or to an object of type IEnumerable. Therefore, in this example the page developer can bind to any IEnumerable type, such as an Array, ArrayList, or Hashtable object. The developer can initially leave the DataSource set to its default null value and set the property in code.

The metadata attributes Category, DefaultValue, and Description provide information used by design tools, the ASP.NET page parser, and the common language runtime.

The CreateMyControlHeirarchy helper method keeps the creation of the control’s child control hierarchy on one common code path. It is called by both the overridden DataBind method and the overridden CreateChildControls method. The overridden DataBind method is required. It enumerates the object in the associated data source and creates the child controls. The overridden CreateChildControls method is required and recreates the child control hierarchy based on the data saved in view state.

The control renders by overriding the inherited RenderContents method. This enables the control to render within its own tags. The parameter passed into the RenderContents method is an object of type HtmlTextWriter, which is a utility class that has methods for rendering tags and other HTML (and HTML-variant) markup.

For more information on the required implementations of a data-bound Web server control, see Developing Custom Data-Bound Web Server Controls for ASP.NET 1.1.

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 an @ Register directive on each page that uses the custom control, as in the following example:

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

Note

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, note 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.

Note

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.

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.

    Note

    The tag prefix entry must be a child of the <controls> element, which must be under the <pages> section under <system.web>.

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

After you have specified the tag prefix mapping in the configuration file, you can use the SimpleDataBoundControl control declaratively as <aspSample:SimpleDataBoundControl /> 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 enables you to test the custom data-bound control.

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

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

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

    <%@ Page Language="VB" %>
    <%@ Register TagPrefix="aspSample" Namespace="Samples.AspNet.VB.Controls" %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <script runat="server">
        Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
            If Not IsPostBack Then
                Dim a As New ArrayList()
                a.Add("One")
                a.Add("Two")
                a.Add("Three")
                a.Add("Four")
    
                simpleDataBoundControl1.DataSource = a
                simpleDataBoundControl1.DataBind()
            End If
        End Sub 'Page_Load
    </script>
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" runat="server">
        <title>SimpleDataBoundControl test page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <aspSample:SimpleDataBoundControl runat="server" id="simpleDataBoundControl1" BorderStyle="Solid" ></aspSample:SimpleDataBoundControl>
        </div>
        </form>
    </body>
    </html>
    
    <%@ Page Language="C#" Trace="true" EnableViewState="true" %>
    <%@ Register TagPrefix="aspSample" Namespace="Samples.AspNet.CS.Controls" %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <script runat="server">
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                ArrayList a = new ArrayList();
                a.Add("One");
                a.Add("Two");
                a.Add("Three");
                a.Add("Four");
    
                simpleDataBoundControl1.DataSource = a;
                simpleDataBoundControl1.DataBind();
            }
        }
    </script>
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>SimpleDataBoundControl test page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <aspSample:SimpleDataBoundControl runat="server" id="simpleDataBoundControl1" BorderStyle="Solid" ></aspSample:SimpleDataBoundControl>
        </div>
        </form>
    </body>
    </html>
    
  3. Run the TestSimpleDataBoundControl.aspx page.

  4. 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.")
    
  5. Refresh the TestSimpleDataBoundControl.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 the source files you created earlier in this walkthrough.

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

    csc /t:library /out:Samples.AspNet.CS.Controls.dll /r:System.dll /r:System.Web.dll *.cs
    
    vbc /t:library /out:Samples.AspNet.VB.Controls.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.

    Note

    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 include the assembly-level System.Web.UI.TagPrefixAttribute attribute, which suggests a default tag prefix that a visual designer should use for your control. 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.

Note

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.CS.Controls", "aspSample")]
    
    Imports System
    Imports System.Web.UI
    <Assembly: TagPrefix("Samples.AspNet.VB.Controls", "aspSample")> 
    

    The tag prefix attribute creates a mapping between the namespace Samples.AspNet.CS.Controls or Samples.AspNet.VB.Controls 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 available to pages in the Web site.

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

  1. If your site does not already have one, create a Bin directory under the root of the Web site.

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

  3. Move the control's source file from the App_Code directory to another directory in your Web site.

    If you do not move the source files out of the App_Code directory, 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 walkthrough is 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 the Web.config file to specify your control's assembly name.

To modify the tag prefix mapping in the Web.config file

  • Edit the Web.config file to add an assembly attribute to the an <add tagPrefix=name> element:

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

The assembly attribute specifies the name of the assembly that the control is in. An <add tagPrefix=name> element maps a tag prefix to a namespace/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 assembly dynamically generated from the App_Code directory.

You can now test the control by viewing it in a page.

To view the page that uses the custom control

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

    https://localhost/ServerControlsTest/TestSimpleDataBoundControl.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.

Next Steps

This simple custom data-bound server control illustrates the fundamental steps used to create a custom control that provides the page developer with a standard way to bind it to an external data source. From this start, you can begin to explore how Visual Studio helps you create sophisticated custom server controls. Suggestions for more exploration include:

See Also

Tasks

Walkthrough: Developing and Using a Custom Web Server Control

Concepts

ASP.NET Data-Bound Web Server Controls Overview

Metadata Attributes for Custom Server Controls

ASP.NET Control Designers Overview

Reference

HierarchicalDataBoundControlDesigner

Other Resources

Developing Custom ASP.NET Server Controls