Chapter 22. Creating Custom Web Form Controls

In this chapter, you will learn how to extend the ASP.NET Framework with your own custom Web Form controls. You learn how to create the same type of controls that Microsoft developed for the ASP.NET Framework, like the TextBox and DataGrid controls.

By the end of this chapter, you’ll understand how to

  • Create non-composite Web Form controls that render any content that you please

  • Create composite controls built from combining existing Web Form controls

  • Add Designer support to your controls so that they will work well in a development environment, such as Visual Studio .NET

Custom Web Form Controls Versus Web User Controls

The ASP.NET Framework supports two methods of creating custom controls. You can create either Web User Controls or custom Web Form controls. In this section, we’ll examine the question of when it’s more appropriate to create one type of control rather than another.

We discussed Web User Controls in Chapter 5, “Creating Web User Controls.” The primary advantage of Web User Controls is that you create a Web User Control in exactly the same way as you create a normal Web Form Page. You create a Web User Control simply by dragging and dropping controls from the Toolbox.

On the other hand, creating a custom Web Form control takes a little more work. You can’t use the Visual Studio .NET Designer when building a custom Web Form control. Instead, you must programmatically specify the content or add each control that you want to display.

The primary disadvantage of Web User Controls is that they do not provide good Designer support. When you add a Web User Control to a Web Form Page, you get a gray blob on the Designer surface that represents the control (see Figure 22.1). You must take the additional step of building and browsing the page before you can see what the page will actually look like. Furthermore, because you cannot add a Web User Control to the Toolbox, you cannot easily reuse the same Web User Control in multiple projects.

Adding a Web User Control to the Designer.

Figure 22.1. Adding a Web User Control to the Designer.

Custom Web Form controls, on the other hand, offer excellent Designer support. You can add a custom Web Form control to the Toolbox. Furthermore, when you drag a custom Web Form control onto a page, it appears in the same way it will appear when it is displayed in a browser.

The one area in which Web User Controls and custom Web Form controls do not differ is performance. Although the two types of controls are compiled in different ways, they are both compiled. A custom Web Form control must be manually compiled before being used. A Web User Control is dynamically compiled when the page that contains it is first requested. So, at the end of the day, there are no performance differences.

Overview of Custom Web Form Controls

When building custom Web Form controls, there are two basic questions that you must answer:

  • What control should I derive from?

  • Should I create a non-composite or composite control?

Let’s start with the first question. When you create a new Web Form control, you must derive the new control from an existing control in the ASP.NET Framework. Typically, you derive a new control from either the Control (System.Web.UI.Control) class or the WebControl (System.Web.UI.WebControls.WebControl) class.

Note

You can create a custom Web control that derives from any existing control in the ASP.NET Framework. For example, you might want to derive a new control from the Label or DataGrid class when you want to automatically get all the functionality of the existing control.

All controls in the ASP.NET Framework, including both HTML and Web controls, ultimately derive from the Control class. Almost all of the controls in the System.Web.UI.WebControls namespace, such as the TextBox and DataGrid controls, derive from the WebControl class.

The difference between deriving from the Control and WebControl classes is support for formatting. You’ll need to derive your control from the WebControl class when you want to take advantage of all the common formatting properties of Web controls. For example, if you derive from the WebControl class, you automatically get support for specifying different fonts and background colors.

To make this discussion more concrete, consider the difference between two existing controls in the ASP.NET Framework—the Label control and the Literal control. Because the Label control derives from the base WebControl class, the Label control includes properties such as the AccessKey, BackColor, and Font properties. Because the Literal control derives from the base Control class and not the base WebControl class, the Literal control does not support these properties.

In most cases, you’ll derive a new custom control from the WebControl class to take advantage of these additional formatting properties. In the next section, you’ll be provided with examples of controls that derive both from the base Control and the base WebControl classes.

Another decision that you must make before building a new control is whether it makes more sense to create a non-composite or composite control. When you create a non-composite control, you are responsible for specifying all the content that the control renders. When you create a composite control, you build the control out of existing controls.

A good example of a composite control is an Address Form control. You can create a new Address Form composite control by combining existing TextBox and Validation controls. In that case, you can take advantage of all the existing functionality of these controls.

On the other hand, you’ll want to create a non-composite control when you need more control over what your control renders. For example, a Color Picker control that enables you to pick a color from a table of colors would be a good example of a non-composite control. We’ll build both types of controls in the following sections.

Creating a Simple Non-Composite Control

A Web Form Page is really nothing more than a collection of controls. More accurately, because some controls contain other controls, a Web Form Page is a control tree.

Tip

You can view a Web Form Page’s control tree by enabling Tracing for a page. To learn more about tracing, see Chapter 6, “Debugging Your Web Form Pages.”

When you request a Web Form Page, the page calls the RenderControl() method of each of its child controls. The RenderControl() method checks the Visible property of the control. If Visible has the value True, the RenderControl() method calls the control’s Render() method.

Consequently, if you want to control the content rendered by a control, you can override the control’s Render() method. For example, Listing 22.1 contains the code for a simple control that displays Hello World!.

Example 22.1.CS. HelloWorld.cs

using System;
using System.Web.UI;

namespace myControls
{
  public class HelloWorld : System.Web.UI.Control
  {
    override protected void Render(HtmlTextWriter writer)
    {
      writer.Write( "Hello World!" );
    }
  }
}

Example 22.1.VB. HelloWorld.vb

Imports System.Web.UI

Public Class HelloWorld
    Inherits System.Web.UI.Control

    Protected Overrides Sub Render(ByVal writer As HtmlTextWriter)
        writer.Write("Hello World!")
    End Sub
End Class

Notice that the HelloWorld control in Listing 22.1 derives from the System.Web.UI.Control class. Furthermore, the HelloWorld class overrides the Render() method and writes the value Hello World!.

The control in Listing 22.1 derives from the base Control class. This means that it does not automatically support formatting properties such as the Font and BackColor properties. If you need support for these formatting properties, you need to make two changes. You need to derive from the base WebControl class and you need to override the RenderContents() method instead of the Render() method.

The WebControl class Render() method does three things. First, the method creates an opening HTML tag; next it calls the RenderContents() method; and finally it creates the closing HTML tag. By overriding the RenderContents() method, you can place content in between the opening and closing tags created automatically by the Render() method.

The opening and closing HTML tags that are automatically created by the WebControl class Render() method add the necessary attributes to support formatting. If you override the Render() method instead of the RenderContents() method, all the automatic formatting is lost.

The control in Listing 22.2 contains the code for a simple control that derives from the base WebControl class.

Example 22.2.CS. HelloWorldWebControl.cs

using System;
using System.Web.UI;

namespace myControls
{
  public class HelloWorldWebControl : System.Web.UI.WebControls.WebControl
  {
    override protected void RenderContents(HtmlTextWriter writer)
    {
      writer.Write( "Hello World!" );
    }
  }
}

Example 22.2.VB. HelloWorldWebControl.vb

Imports System.Web.UI

Public Class HelloWorldWebControl
  Inherits System.Web.UI.WebControls.WebControl

  Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
    writer.Write("Hello World!")
  End Sub

End Class

When developing controls to work in the Visual Studio .NET environment, you should normally stick with controls that derive from WebControl rather than Control. Controls that derive from the base WebControl class provide better Designer support. For example, you can absolutely position a control that derives from WebControl on the Designer surface.

Using the HtmlTextWriter Class

In the previous section, we created a simple non-composite control named the HelloWorldWebControl that displays the text Hello World!. We created the control by overriding the RenderContents() method. You might have noticed that we did not use the Response.Write() method to output the text. Instead, we used something called the HtmlTextWriter() class.

Warning

You should never use the Response.Write() method inside a custom Web control. If you use the Response.Write() method to output a string of text, the text can appear either before or after all the page content. In other words, using Response.Write() ignores the page’s control tree.

The HtmlTextWriter class has several methods that make it easier to output HTML formatted content:

  • AddAttributeAdds an HTML attribute to the tag being rendered

  • AddStyleAttributeAdds a style attribute to the tag being rendered

  • RenderBeginTagRenders an opening HTML tag

  • RenderEndTagRenders a closing HTML tag that corresponds to the last opening HTML tag

  • WriteWrites arbitrary content

  • WriteLineWrites arbitrary content followed by a line terminator

For example, suppose that you need to output the text Hello World! using a green font. You could output this text using the following call to the Write method of the HtmlTextWriter class:

C#

writer.Write( "<font color="green">Hello World!</font>" );

VB.NET

writer.Write("<font color=""green"">Hello World!</font>")

However, this method of writing HTML content is messy and hard to read. Instead, you can take advantage of the methods of the HtmlTextWriter class to output the text like the following:

C#

writer.AddAttribute( "color", "green" );
writer.RenderBeginTag( "font" );
writer.Write( "Hello World!" );
writer.RenderEndTag();

VB.NET

writer.AddAttribute("color", "green")
writer.RenderBeginTag("font")
writer.Write("Hello World!")
writer.RenderEndTag()

This code is easier to read. Furthermore, when you use the methods of the HtmlTextWriter class, the rendered content is automatically indented. For example, if you render an HTML table with the HtmlTextWriter class, the table cells are correctly indented.

When calling the RenderBeginTag and AddAttribute methods in the previous code, we passed a string that represents the attribute or tag to the method. Instead of passing a string, you can pass a value from either the HtmlTextWriterTag or HtmlTextWriterAttribute enumerations.

The HtmlTextWriterTag enumeration contains common HTML tags and the HtmlTextWriterAttribute enumeration contains common HTML attributes. There is also an HtmlTextWriterStyle enumeration that contains common style attributes that can be used with the AddStyleAttribute method.

When you use the enumerations with the HtmlTextWriter methods, the HtmlTextWriter will render different content for downlevel browsers than uplevel browsers. Consider the following code:

writer.AddStyleAttribute( HtmlTextWriterStyle.BackgroundColor, "Yellow" );
writer.RenderBeginTag( HtmlTextWriterTag.Div);
writer.Write( "Hello World!" );
writer.RenderEndTag();

This code displays the text Hello World! within a <div> tag with a yellow background. When the HtmlTextWriter class renders this content to an uplevel browser, it renders the following content:

<div style="background-color:Yellow;">
    Hello World!
</div>

However, when this content is rendered to a downlevel browser, the following content is rendered:

<table cellpadding="0" cellspacing="0" border="0" width="100%"
bgcolor="Yellow"><tr><td>
    Hello World!
</td></tr></table>

Notice that the <div> tag has been automatically downgraded to a <table> tag, and the background-color attribute has been downgraded to a bgcolor attribute. When possible, you should use the enumerations so that your controls will render correctly in both uplevel and downlevel browsers.

Creating a Non-Composite Content Rotator Control

In this section, we’ll tackle a more realistic sample of a non-composite control. We’ll walk through each step of creating a Content Rotator control. Our Content Rotator control will randomly display one entry from an XML file named Content.xml.

Perform the following steps to create a new project for the Content Rotator control:

  1. Open the New Project dialog box by selecting New from the File menu, and pointing to Project.

  2. Select Web Control Library under Templates.

  3. Select Close Solution under Location.

  4. Name the new project myControls and click OK.

Next, we need to create the Content Rotator control:

Procedure 22.1. C# Steps

  1. Add references to the System.Data and System.XML assemblies to the myControls project by right-clicking the References folder and selecting Add Reference.

  2. Add a new class named ContentRotator.cs to the myControls project by selecting Add Class from the Project menu.

  3. Enter the following code for the ContentRotator.cs class:

    using System;
    using System.Data;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    namespace myControls
    {
      /// <summary>
      /// Summary description for ContentRotator.
      /// </summary>
      public class ContentRotator : WebControl
      {
        protected override void RenderContents(HtmlTextWriter writer)
        {
          // Don't retrieve content in Designer
          if (HttpContext.Current != null)
          {
            // Get path to content file
            string contentFile =
              HttpContext.Current.Server.MapPath( "Content.xml" );
    
            // Load content into dataset
            DataSet dstContent = new DataSet();
            dstContent.ReadXml( contentFile );
    
            // Get random entry
            Random objRan = new Random();
            DataTable dtblContent = dstContent.Tables[0];
            int intRan = objRan.Next( dtblContent.Rows.Count);
    
            // Render the results
            writer.Write( (string)dtblContent.Rows[intRan]["item_Text"]);
          }
          else
          {
            writer.Write( "Random Content" );
          }
        }
      }
    }
    
  4. Build the myControls project by select Build myControls from the Build menu.

Procedure 22.2. VB.NET Steps

  1. Add a new class named ContentRotator.vb to the myControls project by selecting Add Class from the Project menu.

  2. Enter the following code for the ContentRotator.vb class:

    Imports System
    Imports System.Data
    Imports System.Web
    Imports System.Web.UI
    Imports System.Web.UI.WebControls
    
    Public Class ContentRotator
      Inherits WebControl
    
      Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
        Dim contentFile As String
        Dim dstContent As DataSet
        Dim dtblContent As DataTable
        Dim intRan As Integer
    
        ' Don't retrieve content in Designer
        If Not HttpContext.Current Is Nothing Then
    
          ' Get path to content file
          contentFile = HttpContext.Current.Server.MapPath("Content.xml")
    
          ' Load content into dataset
          dstContent = New DataSet()
          dstContent.ReadXml(contentFile)
    
          ' Get random entry
          Dim objRan As New Random()
          dtblContent = dstContent.Tables(0)
          intRan = objRan.Next(dtblContent.Rows.Count)
    
          ' Render the results
          writer.Write(dtblContent.Rows(intRan)("item_Text"))
        Else
          writer.Write("Random Content")
        End If
      End Sub
    End Class
    
  3. Build the myControls project by select Build myControls from the Build menu.

Next, we need to create a separate project for testing our control:

  1. Open the New Project dialog box by selecting New from the File menu, and pointing to Project.

  2. Select ASP.NET Web Application under Templates.

  3. Select Add to Solution under Location.

  4. Name the new project TestControls and click OK.

Next, we need to add the Content Rotator control to the Toolbox:

  1. Add a new Web Form Page to your project named TestContentRotator.aspx.

  2. Right-click the Toolbox under the General tab and select Customize Toolbox.

  3. Select the tab labeled .NET Framework Components.

  4. Click the Browse button and browse to the following file:

    My DocumentsVisual Studio ProjectsmyControlsmyControlsin DebugmyControls.dll
    
  5. Click OK.

After you complete these steps, the Content Rotator control should appear under the General tab. Before we can test the control, we need to add a Content.xml file to the TestControls project.

  1. Open the Add New Item dialog box by right-clicking the TestControls project in the Solution Explorer window and selecting Add, Add New Item. Select XML File under Templates, name the file Content.xml and click Open.

  2. Enter the following content for the Content.xml file:

    <?xml version="1.0" encoding="utf-8" ?>
    <content>
    <item>Aliens attack!</item>
    <item>Life discovered on Mars!</item>
    <item>Moon explodes!</item>
    </content>
    
  3. Save the changes to the Content.xml file by clicking the Save Content.xml button.

The final step is to add the control to a page and test it:

  1. Drag the Content Rotator control onto the TestContentRotator.aspx page.

  2. Right-click the TestContentRotator.aspx file in the Solution Explorer window and select Build and Browse.

Every time you click the Refresh button on your browser, a new entry from the Content.xml file will be randomly selected and displayed in the TestControl.aspx page.

There’s one special thing that you should notice about the code for the Content Rotator control. The following conditional is used to check whether the Content Rotator is being displayed at design time (on the Designer surface) or at run time (when the TestControl.aspx page is executing):

C#

if (HttpContext.Current != null)
{
  ...
}

VB.NET

If Not HttpContext.Current Is Nothing Then
...
End If

When there is no HttpContext, the control is not actually being displayed in a browser. Therefore, you can use this check to skip actions that you don’t want performed when manipulating the control in the Designer.

Note

We should really implement caching for our Content Rotator control so that we can cache the Content.xml file in memory. The best way to implement the caching would be to create a file dependency on the Content.xml file. To learn more about creating file dependencies, see Chapter 14, “Improving Application Performance with Caching.”

Creating a Simple Composite Control

A composite control enables you to reuse existing controls in a new control. For example, you can create a new Address Form composite control by combining together exiting TextBox and Validation controls.

When we created our non-composite control in the previous section, we overrode the RenderContents() method. When you create a composite control, on the other hand, you typically override the CreateChildControls() method.

Every control in the ASP.NET Framework has a Controls collection. The Controls collection contains all of a control’s child controls. For example, the Controls collection of an Address Form control would include TextBox and Validation controls.

The CreateChildControls() method is responsible for creating all the controls contained in the Controls collection. When you override this method, you add each of a control’s child controls using logic that looks like the following:

C#

protected override void CreateChildControls()
{
  // Add Street TextBox
  TextBox txtStreet = new TextBox();
  txtStreet.ID = "txtStreet";
  Controls.Add( txtStreet );
}

VB.NET

Protected Overrides Sub CreateChildControls()
  ' Add Street TextBox
  Dim txtStreet As New TextBox()
  txtStreet.ID = "txtStreet"
  Controls.Add(txtStreet)
End Sub

This code adds a TextBox to the Controls collection. You can add any control you need—including Repeater, DropDownLists, and DataGrid controls—by tossing it into the Controls collection.

There is an important method that works with the CreateChildControls method called the EnsureChildControls() method. The EnsureChildControls() method calls the CreateChildControls() method. However, it is guaranteed to call the CreateChildControls() method only once. You can safely call the EnsureChildControls method over and over again without worrying about the Controls collection being re-created.

You need to call the EnsureChildControls() method within your control before accessing any child controls. For example, suppose that the Address Form control has a property named Street that returns the current value of the txtStreet text box:

C#

public string Street
{
  get
  {
    EnsureChildControls();
    return txtStreet.Text;
  }
}

VB.NET

Public ReadOnly Property Street() As String
  Get
    EnsureChildControls()
    Return txtStreet.Text
  End Get
End Property

If you neglect to include EnsureChildControls() in the Street property, you would receive a Null Reference Exception. You would receive a Null Reference Exception because the child txtStreet text box would not have been created yet.

The CreateChildControls() method is unique in that you don’t know exactly when it will be called. It could be called whenever someone happens to read a property of your control. If the CreateChildControls() method is not called before a control’s PreRender() method is called, the CreateChildControls() method is called automatically at that point.

Using the INamingContainer Interface

When implementing a composite control, you should always implement the INamingContainer interface. This interface is a marker interface. This means that it doesn’t actually have any methods that you must override.

Note

The INamingContainer interface is in the System.Web.UI namespace, so you’ll need to import this namespace when implementing the interface.

You can implement the INamingContainer interface when declaring a control like the following:

C#

public class AddressForm : WebControl, INamingContainer
{
  ...
}

VB.NET

Public Class AddressForm
    Inherits WebControl
    Implements INamingContainer
  ...
End Class

The INamingContainer interface creates a new namespace for your control. You need to implement this interface when developing composite controls to prevent ID naming collisions.

For example, if you don’t implement this interface in the case of the AddressForm control, the values of any TextBox controls contained within AddressForm control will not be retained across postbacks. The framework would not be able to assign the values to the TextBox controls because it would not be able to resolve the IDs of the TextBox controls.

Using Render with Composite Controls

When you create a composite control, you override the CreateChildControls() method. You use the CreateChildControls() method to specify all the child controls of the composite control.

However, typically, you’ll also want to override the Render() method of a composite control. You’ll use the Render() method to lay out the controls that you create in the CreateChildControls() method.

For example, when creating an AddressForm control, you’ll normally want to lay out the TextBox and Validation controls within an HTML table. That way, all the controls are not bunched together haphazardly on the page.

Listing 22.3 illustrates how you use both the CreateChildControls() and the Render() method for the AddressForm control.

Example 22.3.CS. Partial AddressForm.cs

private TextBox txtStreet;
private TextBox txtCity;

protected override void CreateChildControls()
{
  // Clear child controls
  Controls.Clear();

  // Add Street TextBox
  txtStreet = new TextBox();
  txtStreet.ID = "txtStreet";
  Controls.Add( txtStreet );
  // Add City TextBox
  txtCity = new TextBox();
  txtCity.ID = "txtState";
  Controls.Add( txtCity );
}

protected override void Render(HtmlTextWriter writer)
{
  // Add WebControl Formatting
  AddAttributesToRender(writer);

  // Open table
  writer.AddAttribute(HtmlTextWriterAttribute.Border, "1");
  writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4");
  writer.RenderBeginTag( HtmlTextWriterTag.Table);

  // Create first row
  writer.RenderBeginTag( HtmlTextWriterTag.Tr);
  writer.RenderBeginTag( HtmlTextWriterTag.Td);
  writer.Write( "Street:" );
  writer.RenderEndTag();
  writer.RenderBeginTag( HtmlTextWriterTag.Td);
  txtStreet.RenderControl(writer);
  writer.RenderEndTag();
  writer.RenderEndTag();

  // Create second row
  writer.RenderBeginTag( HtmlTextWriterTag.Tr);
  writer.RenderBeginTag( HtmlTextWriterTag.Td);
  writer.Write( "City:" );
  writer.RenderEndTag();
  writer.RenderBeginTag( HtmlTextWriterTag.Td);
  txtCity.RenderControl(writer);
  writer.RenderEndTag();
  writer.RenderEndTag();

  // Close table
  writer.RenderEndTag();
}

Example 22.3.VB. Partial AddressForm.vb

Dim txtStreet As TextBox
Dim txtCity As TextBox


Protected Overrides Sub CreateChildControls()
  ' Clear child controls
  Controls.Clear()

  ' Add Street TextBox
        txtStreet = New TextBox()
        txtStreet.ID = "txtStreet"
        Controls.Add(txtStreet)

        ' Add City TextBox
        txtCity = New TextBox()
        txtCity.ID = "txtState"
        Controls.Add(txtCity)
    End Sub

    Protected Overrides Sub Render(ByVal writer As HtmlTextWriter)

        ' Add WebControl Formatting
        AddAttributesToRender(writer)

        ' Open table
        writer.AddAttribute(HtmlTextWriterAttribute.Border, "1")
        writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4")
        writer.RenderBeginTag(HtmlTextWriterTag.Table)

        ' Create first row
        writer.RenderBeginTag(HtmlTextWriterTag.Tr)
        writer.RenderBeginTag(HtmlTextWriterTag.Td)
        writer.Write("Street:")
        writer.RenderEndTag()
        writer.RenderBeginTag(HtmlTextWriterTag.Td)
        txtStreet.RenderControl(writer)
        writer.RenderEndTag()
        writer.RenderEndTag()

        ' Create second row
        writer.RenderBeginTag(HtmlTextWriterTag.Tr)
        writer.RenderBeginTag(HtmlTextWriterTag.Td)
        writer.Write("City:")
        writer.RenderEndTag()
        writer.RenderBeginTag(HtmlTextWriterTag.Td)
        txtCity.RenderControl(writer)
        writer.RenderEndTag()
        writer.RenderEndTag()

        ' Close table
        writer.RenderEndTag()
    End Sub

Notice that the txtStreet and txtCity TextBox controls are created in the CreateChildControls() method. However, they are actually rendered within the Render() method by calling txtStreet.RenderControl(writer) and txtCity.RenderControl(writer).

You don’t need to include a Render() method in a composite control. If you leave it out, each of the controls created in the CreateChildControls() method will be automatically rendered. However, the Render() method provides you with access to the HtmlTextWriter class that makes it easier to layout the controls contained in a composite control.

Composite Controls and Designer Support

When you create a non-composite control and add the control to the Visual Studio .NET Designer, you see exactly what will be displayed by the control when the control is displayed in a Web Form Page. In other words, whatever is rendered by a non-composite control at design time is exactly the same as whatever is rendered by the control at runtime.

By default, this is not true in the case of a composite control. You have to perform some additional work to get a composite control to look the same at design time as it does at runtime. I’m giving you this warning now so that you are not surprised when the composite control that we create in the next section does not appear correctly in the Designer.

The extra work involves creating something called a ControlDesigner. You can use a ControlDesigner with both composite and non-composite controls to specify how a control will appear at design time. We’ll discuss the ControlDesigner designer class later in this chapter in the “Using the ControlDesigner Class” section.

Creating a Composite Address Form Control

We’ve discussed pieces of the AddressForm control. In this section, we are going to put everything together and build the AddressForm composite control from start to finish (see Figure 22.2).

The AddressForm control.

Figure 22.2. The AddressForm control.

Let’s start by creating a new Control Library project:

  1. Open the New Project dialog box by selecting New from the File menu and pointing to Project.

  2. Select Web Control Library under Templates.

  3. Select Close Solution under Location.

  4. Name the new project myCompositeControls and click OK.

Next, we need to create the AddressForm control:

Procedure 22.3. C# Steps

  1. Add a new class named AddressForm.cs to the myCompositeControls project by selecting Add Class from the Project menu.

  2. Enter the following code for the AddressForm.cs class:

    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    namespace myCompositeControls
    {
         public class AddressForm : WebControl, INamingContainer
         {
              private TextBox txtStreet;
              private TextBox txtCity;
              private TextBox txtState;
              private TextBox txtZip;
              private RequiredFieldValidator valStreet;
              private RequiredFieldValidator valCity;
              private RequiredFieldValidator valState;
              private RequiredFieldValidator valZip;
    
              public string Street
              {
                   get
                   {
                        EnsureChildControls();
                        return txtStreet.Text;
                   }
              }
    
              public string City
              {
                   get
                   {
                        EnsureChildControls();
                        return txtCity.Text;
                   }
              }
              public string State
              {
                   get
                   {
                        EnsureChildControls();
                        return txtState.Text;
                   }
              }
              public string Zip
              {
                   get
                   {
                        EnsureChildControls();
                        return txtZip.Text;
                   }
              }
    
              public override ControlCollection Controls
              {
                   get
                   {
                        EnsureChildControls();
                        return base.Controls;
                   }
              }
    
              protected override void CreateChildControls()
              {
                   // Clear child controls
                   Controls.Clear();
    
                   // Add Street TextBox
                   txtStreet = new TextBox();
                   txtStreet.ID = "txtStreet";
                   Controls.Add( txtStreet );
    
                   // Add Street Validation
                   valStreet = new RequiredFieldValidator();
                   valStreet.ControlToValidate = "txtStreet";
                   valStreet.Text = "*";
                   Controls.Add( valStreet );
    
                   // Add City TextBox
                   txtCity = new TextBox();
                   txtCity.ID = "txtCity";
                   Controls.Add( txtCity );
    
                   // Add City Validation
                   valCity = new RequiredFieldValidator();
                   valCity.ControlToValidate = "txtCity";
                   valCity.Text = "*";
                   Controls.Add( valCity );
    
                   // Add State TextBox
                   txtState = new TextBox();
                   txtState.ID = "txtState";
                   Controls.Add( txtState );
    
                   // Add State Validation
                   valState = new RequiredFieldValidator();
                   valState.ControlToValidate = "txtState";
                   valState.Text = "*";
                   Controls.Add( valState );
    
                   // Add Zip TextBox
                   txtZip = new TextBox();
                   txtZip.ID = "txtZip";
                   Controls.Add( txtZip );
    
                   // Add State Validation
                   valZip = new RequiredFieldValidator();
                   valZip.ControlToValidate = "txtZip";
                   valZip.Text = "*";
                   Controls.Add( valZip );
              }
    
              protected override void Render(HtmlTextWriter writer)
              {
                   // Add WebControl Formatting
                   AddAttributesToRender(writer);
    
                   // Open table
                   writer.AddAttribute(HtmlTextWriterAttribute.Border, "1");
                   writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4");
                   writer.RenderBeginTag( HtmlTextWriterTag.Table);
          
                   // Create first row
                   writer.RenderBeginTag( HtmlTextWriterTag.Tr);
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   writer.Write( "Street:" );
                   writer.RenderEndTag();
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   txtStreet.RenderControl(writer);
                   valStreet.RenderControl(writer);
                   writer.RenderEndTag();
                   writer.RenderEndTag();
    
                   // Create second row
                   writer.RenderBeginTag( HtmlTextWriterTag.Tr);
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   writer.Write( "City:" );
                   writer.RenderEndTag();
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   txtCity.RenderControl(writer);
                   valCity.RenderControl(writer);
                   writer.RenderEndTag();
                   writer.RenderEndTag();
    
                   // Create third row
                   writer.RenderBeginTag( HtmlTextWriterTag.Tr);
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   writer.Write( "State:" );
                   writer.RenderEndTag();
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   txtState.RenderControl(writer);
                   valState.RenderControl(writer);
                   writer.RenderEndTag();
                   writer.RenderEndTag();
    
                   // Create fourth row
                   writer.RenderBeginTag( HtmlTextWriterTag.Tr);
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   writer.Write( "ZIP:" );
                   writer.RenderEndTag();
                   writer.RenderBeginTag( HtmlTextWriterTag.Td);
                   txtZip.RenderControl(writer);
                   valZip.RenderControl(writer);
                   writer.RenderEndTag();
                   writer.RenderEndTag();
    
                   // Close table
                   writer.RenderEndTag();
    
              }
         }
    }
    
  3. Build the myCompositeControls project by selecting Build myCompositeControls from the Build menu.

Procedure 22.4. VB.NET Steps

  1. Add a new class named AddressForm.vb to the myCompositeControls project by selecting Add Class from the Project menu.

  2. Enter the following code for the AddressForm.vb class:

    Imports System
    Imports System.Web.UI
    Imports System.Web.UI.WebControls
    
    Public Class AddressForm
        Inherits WebControl
        Implements INamingContainer
    
        Private txtStreet As TextBox
        Private txtCity As TextBox
        Private txtState As TextBox
        Private txtZip As TextBox
        Private valStreet As RequiredFieldValidator
        Private valCity As RequiredFieldValidator
        Private valState As RequiredFieldValidator
        Private valZip As RequiredFieldValidator
    
        Public ReadOnly Property Street() As String
            Get
                EnsureChildControls()
                Return txtStreet.Text
            End Get
        End Property
    
        Public ReadOnly Property City() As String
    
            Get
                EnsureChildControls()
                Return txtCity.Text
            End Get
        End Property
    
        Public ReadOnly Property State() As String
    
            Get
    
                EnsureChildControls()
                Return txtState.Text
            End Get
        End Property
    
        Public ReadOnly Property Zip() As String
            Get
    
                EnsureChildControls()
                Return txtZip.Text
            End Get
        End Property
    
        Public Overrides ReadOnly Property Controls() As ControlCollection
            Get
                EnsureChildControls()
                Return MyBase.Controls
            End Get
        End Property
    
        Protected Overrides Sub CreateChildControls()
            ' Clear child controls
            Controls.Clear()
    
            ' Add Street TextBox
            txtStreet = New TextBox()
            txtStreet.ID = "txtStreet"
            Controls.Add(txtStreet)
    
            ' Add Street Validation
            valStreet = New RequiredFieldValidator()
            valStreet.ControlToValidate = "txtStreet"
            valStreet.Text = "*"
            Controls.Add(valStreet)
    
            ' Add City TextBox
            txtCity = New TextBox()
            txtCity.ID = "txtCity"
            Controls.Add(txtCity)
    
            ' Add City Validation
            valCity = New RequiredFieldValidator()
            valCity.ControlToValidate = "txtCity"
            valCity.Text = "*"
            Controls.Add(valCity)
    
            ' Add State TextBox
            txtState = New TextBox()
            txtState.ID = "txtState"
            Controls.Add(txtState)
    
            ' Add State Validation
            valState = New RequiredFieldValidator()
            valState.ControlToValidate = "txtState"
            valState.Text = "*"
            Controls.Add(valState)
    
            ' Add Zip TextBox
            txtZip = New TextBox()
            txtZip.ID = "txtZip"
            Controls.Add(txtZip)
    
            ' Add State Validation
            valZip = New RequiredFieldValidator()
            valZip.ControlToValidate = "txtZip"
            valZip.Text = "*"
            Controls.Add(valZip)
        End Sub
    
        Protected Overrides Sub Render(ByVal writer As HtmlTextWriter)
            ' Add WebControl Formatting
            AddAttributesToRender(writer)
    
            ' Open table
            writer.AddAttribute(HtmlTextWriterAttribute.Border, "1")
            writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4")
            writer.RenderBeginTag(HtmlTextWriterTag.Table)
    
            ' Create first row
            writer.RenderBeginTag(HtmlTextWriterTag.Tr)
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            writer.Writer("Street:")
            writer.RenderEndTag()
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            txtStreet.RenderControl(writer)
            valStreet.RenderControl(writer)
            writer.RenderEndTag()
            writer.RenderEndTag()
    
            ' Create second row
            writer.RenderBeginTag(HtmlTextWriterTag.Tr)
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            writer.Write("City:")
            writer.RenderEndTag()
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            txtCity.RenderControl(writer)
            valCity.RenderControl(writer)
            writer.RenderEndTag()
            writer.RenderEndTag()
    
            ' Create third row
            writer.RenderBeginTag(HtmlTextWriterTag.Tr)
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            writer.Write("State:")
            writer.RenderEndTag()
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            txtState.RenderControl(writer)
            valState.RenderControl(writer)
            writer.RenderEndTag()
            writer.RenderEndTag()
    
            ' Create fourth row
            writer.RenderBeginTag(HtmlTextWriterTag.Tr)
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            writer.Write("ZIP:")
            writer.RenderEndTag()
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            txtZip.RenderControl(writer)
            valZip.RenderControl(writer)
            writer.RenderEndTag()
            writer.RenderEndTag()
    
            ' Close table
            writer.RenderEndTag()
        End Sub
    End Class
    
  3. Build the myCompositeControls project by selecting Build myCompositeControls from the Build menu.

Next, we need to create a separate project for testing our control:

  1. Open the New Project dialog box by selecting New from the File menu and pointing to Project.

  2. Select ASP.NET Web Application under Templates.

  3. Select Add to Solution under Location.

  4. Name the new project TestCompositeControls and click OK.

Next, we need to add the Address Form control to the Toolbox:

  1. Add a new Web Form Page to your project named TestAddressForm.aspx.

  2. Right-click the Toolbox under the General tab and select Customize Toolbox.

  3. Select the .NET Framework Components tab.

  4. Click the Browse button and browse to the following file:

    My DocumentsVisual Studio ProjectsmyCompositeControls myCompositeControlsinDebug
    VB.NET StepsmyCompositeControls.dll
    
  5. Click OK.

After you complete these steps, the Address Form control should appear under the General tab. We are finally ready to test out the control:

  1. Drag the Address Form control onto the TestAddressForm.aspx page.

  2. Right-click the TestAddressForm.aspx page in the Solution Explorer window and select Build and Browse.

When the TestAddressForm.aspx page opens, you can enter address information into the control. Notice that each text box is validated with a RequiredFieldValidator. You can retrieve the entries in the Address Form control by using the Street, City, State, and Zip properties of the control.

Note

Notice that you can format the Address Form control from the Properties window. For example, you can modify its background color. We get these formatting properties automatically because the Address Form control derives from the base WebControl class.

Adding Designer Support to a Custom Web Form Control

The primary advantage of creating custom Web Form controls instead of Web User Controls is that custom Web Form controls provide better Designer support. You can add custom Web Form controls to the Toolbox, you can modify the properties of custom Web Form controls in the Properties window, and you can see the content rendered by custom Web Form controls during design time.

In this section, you’ll learn how to take control over the appearance of custom controls in the Visual Studio .NET environment. First, we’ll look at the special attributes that you can apply to a custom control. Next, we’ll look at how you can customize the appearance of a custom control in the Toolbox. Finally, you’ll learn how to take advantage of ControlDesigners to take control over the appearance of your control on the Designer surface.

Using Design-Time Attributes

There are a number of special design-time attributes that you can apply to your control. For example, you can use the DefaultProperty attribute to specify the property that is selected by default in the Properties window when you click a control. The following is a list of the most important design attributes that you can apply at the class, property, or event level:

  • BindableContains a Boolean value that indicates whether a property can be used for binding

  • BrowsableContains a Boolean value that determines whether a property or event appears in the Properties window

  • CategoryContains a string value that determines the category associated with a property or event

  • DefaultEventContains a string value that specifies the default event associated with a control

  • DefaultPropertyContains a string value that represents the property that is selected by default in the Properties window

  • DefaultValueContains an object that represents the default value for a property

  • DescriptionContains a string value that determines the help text associated with a property or event at the bottom of the Properties window

  • EditorContains the name and type of the editor used for editing the value of a property in the Designer

  • ToolboxDataContains a string that specifies the tag generated for a control when the control is dragged onto the Designer surface

  • TypeConverterContains a string or type that specifies a type converter used for converting the value of a property into a form that can be persisted

For example, the control in Listing 22.4 uses several of these attributes to determine its appearance in the Visual Studio .NET Designer.

Example 22.4.CS. myControl.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;

namespace myControls
{
  [DefaultProperty("Text"),
  ToolboxData("<{0}:myControl runat=server></{0}:myControl>")]
  public class myControl : System.Web.UI.WebControls.WebControl
  {
    private string text;

    [Bindable(true),
    Category("Appearance"),
    Description("The text this control displays")]
    public string Text
    {
      get
      {
        return text;
      }
      
      set
      {
        text = value;
      }
    }

    protected override void Render(HtmlTextWriter output)
    {
      output.Write(Text);
    }
  }
}

Example 22.4.VB. myControl.vb

Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.ComponentModel

<DefaultProperty("Text"), _
ToolboxData("<0}:myControl runat=server></0}:myControl>")> _
Public Class myControl
    Inherits System.Web.UI.WebControls.WebControl

    Private _text As String

    <Bindable(True), _
    Category("Appearance"), _
    Description("The text this control displays")> _
      Public Property Text() As String
        Get

          Return _text
        End Get

        Set(ByVal Value As String)

          _text = Value
        End Set
    End Property

    Protected Overrides Sub Render(ByVal output As HtmlTextWriter)
        output.Write(_text)
    End Sub
End Class

There is another design-time attribute that applies at the assembly level instead of the class, property, or event level. The TagPrefix attribute enables you to specify the tag that appears when your control is declared on a page.

By default, the first control added to a page appears with the tag prefix cc1, the second cc2, and so on. If you want to provide a more descriptive tag prefix, you need to assign a value to the TagPrefix attribute.

When you create a new Web Control Library project, a file is automatically added to your project named AssemblyInfo.cs or AssemblyInfo.vb. You need to add the TagPrefix attribute to this file. You can add the following attribute anywhere in this file:

C#

[assembly: System.Web.UI.TagPrefix("myControls","Super")]

VB.NET

<Assembly: System.Web.UI.TagPrefix("myControls", "Super")>

The first parameter, myControls, refers to the namespace. Typically, this will be the same name as your project. The second parameter, Super, is the tag prefix generated when you add a control to a page.

Controlling the Appearance of a Custom Control in the Toolbox

When you add a custom Web control to the Toolbox, it appears with an icon of a gear by default. You can create your own 16×16 pixel bitmap image to customize this icon.

  1. Open the Image Editor by right-clicking the project that contains your custom control in the Solution Explorer window and selecting Add New Item from the Add menu. Select the Bitmap File template. Make sure that you named the Bitmap image with the same name as your control and click Open.

  2. In the Properties window, set the Width and Height properties to the value 16.

  3. Use the Image Editor tool to draw an appropriate image for your control.

  4. Click the Save button to save your image.

  5. In the Solution Explorer window, select your image.

  6. In the Properties window, assign the value Embedded Resource to the Build Action property.

  7. Build your project by selecting Build Project Name from the Build menu.

After you complete these steps, you might need to remove and re-add the control to the Toolbox for the changes to appear.

Using the ControlDesigner Class

If you want your control to appear differently in the Visual Studio .NET Designer than it appears at runtime, you can use the ControlDesigner class to specify a design-time appearance for your control.

The ControlDesigner class has a method called GetDesignTimeHtml() that you can override to specify the design-time appearance of a control. This method simply returns a string that is used for rendering the control in the Designer. After you create a ControlDesigner, you associate it with a control by using the Designer attribute.

For example, the ControlDesigner in Listing 22.5 renders the text Hello World!.

Example 22.5.CS. myControlDesigner.cs

using System;
using System.Web.UI.Design;

namespace myControls
{
  public class myControlDesigner : ControlDesigner
  {
    public override string GetDesignTimeHtml()
    {
      return "Hello World!";
    }
  }
}

Example 22.5.VB. myControlDesigner.vb

Imports System.Web.UI.Design

Public Class myControlDesigner
    Inherits ControlDesigner

    Public Overrides Function GetDesignTimeHTML() As String
      Return "Hello World!"
    End Function
End Class

You can associate the ControlDesigner in Listing 22.5 with a control by adding the following attribute to the declaration of a control class:

C#

[System.ComponentModel.Designer(typeof(myControlDesigner))]
public class myControl : WebControl, INamingContainer
{
  ...
}

VB.NET

<System.ComponentModel.Designer(GetType(myControlDesigner))> _
Public Class myControl
  ...
End Class

Warning

The ControlDesigner class is part of the System.Web.UI.Design namespace. This namespace is located in an assembly that is not one of the default assemblies referenced in a project. You must add a reference to the System.Design.dll assembly before using the ControlDesigner class.

You can create a ControlDesigner for any type of control. However, you’ll almost always want to associate a ControlDesigner class with a composite control because a composite control does not appear correctly in the Designer.

For example, earlier in this chapter, we created an Address Form composite control. To get the Address Form control to appear correctly in the Designer, do the following:

Procedure 22.5. C# Steps

  1. Add a new class to the myCompositeControls project named AddressFormDesigner.cs.

  2. Add a new reference to the System.Design assembly to your project by right-clicking the References folder, selecting Add Reference, and selecting System.Design.dll.

  3. Add a new class to your project named AddressFormDesigner.

  4. Enter the following code for the AddressFormDesigner class:

    using System;
    using System.Web.UI;
    using System.Web.UI.Design;
    
    namespace myControls
    {
      public class AddressFormDesigner : ControlDesigner
      {
        public override string GetDesignTimeHtml()
        {
        ControlCollection AddressFormControls = ((Control)Component).Controls;
        return base.GetDesignTimeHtml();
        }
      }
    }
    
  5. Modify the AddressForm control by adding the following attribute before the class declaration:

    [System.ComponentModel.Designer(typeof(AddressFormDesigner))]
    
  6. Rebuild the Solution by selecting Build Solution from the Build menu.

Procedure 22.6. VB.NET Steps

  1. Add a new class to the myCompositeControls project named AddressFormDesigner.vb.

  2. Add a new reference to the System.Design assembly to your project by right-clicking the References folder, selecting Add Reference, and selecting System.Design.dll.

  3. Add a new class to your project named AddressFormDesigner.

  4. Enter the following code for the AddressFormDesigner class:

    Imports System.Web.UI.Design
    Imports System.Web.UI
    
    Public Class AddressFormDesigner
        Inherits ControlDesigner
    
        Public Overrides Function GetDesignTimeHTML() As String
            Dim AddressFormControls As ControlCollection
    
            AddressFormControls = CType(Component, Control).Controls
            Return MyBase.GetDesignTimeHtml()
        End Function
    End Class
    
  5. Modify the AddressForm control by adding the following attribute before the class declaration:

    <System.ComponentModel.Designer(GetType(AddressFormDesigner))> _
    
  6. Rebuild the Solution by selecting Build Solution from the Build menu.

The important part of the AddressFormDesigner is the following line of code:

C#

ControlCollection AddressFormControls = ((Control)Component).Controls;

VB.NET

AddressFormControls = CType(Component, Control).Controls

The ControlDesigner class automatically creates the Component variable. It represents the AddressForm control. Consequently, this line of code retrieves the Controls collection from the AddressForm control.

When we created the AddressForm control, we overrode the Controls property to include a call to the EnsureChildControls() method. Therefore, because the AddressFormDesigner accesses the Controls collection, the AddressForm control is forced to create its child controls and render correctly in the Visual Studio .NET Designer.

Summary

In this chapter, you learned how to create custom Web Form controls that work in the Visual Studio .NET environment. In the first section, you learned how to create non-composite controls. We created a simple Content Rotator control that randomly retrieves content items from an XML file.

Next, we examined the topic of composite controls. You learn how to use composite controls to bundle together the functionality of existing controls in a new control. We create a simple Address Form composite control.

Finally, we looked at methods for controlling the appearance of a custom control in the Visual Studio .NET environment. You learned how to use design-time attributes, modify the appearance of a control in the Toolbox, and create a custom ControlDesigner.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.40.4