Building Templated Custom ASP.NET Server Controls

 

Scott Mitchell
4GuysFromRolla.com

February 2004

Applies to:
    Microsoft® ASP.NET

Summary: Learn how to build templated custom ASP.NET server controls. Templates allow for a custom mix of HTML elements, Web controls, and databound values. Also learn how to build a non-databound templated control that displays the statistics for a fictional online forum site. (17 printed pages)

Download TemplatedControls.msi.

Contents

Introduction
Rendered Controls and Composite Controls
Building a Non-Databound Templated Server Control
Conclusion

Introduction

The basic building blocks of Microsoft® ASP.NET Web Forms are Web controls (such as the Label, TextBox, and DataGrid. Web controls) placed on an ASP.NET Web Form by a declarative syntax not unlike traditional HTML elements. Underneath the covers, Web controls are actually classes within the Microsoft .NET Framework, and each Web control placed on an ASP.NET Web Form becomes an instance of its respective class when the Web Form is visited. This model provides two great benefits:

  • ASP.NET Web Form developers can easily add, move, and reposition Web controls on an ASP.NET page by simply editing the declarative syntax (or, with editors like Microsoft Visual Studio® .NET, by dragging and dropping Web controls from the Toolbox onto the Design tab.)
  • Since Web controls are merely classes, the functionality of existing Web controls can be extended by creating new classes that derive from existing Web controls. Also, brand new Web controls can be formed by extending them from the base Web control class (System.Web.UI.WebControls.WebControl).

For information on using inheritance to extend the functionality of existing Web controls, check out my article Easily Adding Functionality to ASP.NET Server Controls or Marcie Robillard's Creating Custom Columns for the ASP.NET DataGrid. For information on building custom ASP.NET server controls from scratch, I highly recommend Nikhil Kothari and Vandana Datye's book, Developing Microsoft ASP.NET Server Controls and Components.

In this article we will examine how to build custom server controls that utilize templates. Templates allow for customization of a Web control's rendered output by providing a means for a page developer to specify HTML elements, Web controls, and databound statements. If you've worked with the ASP.NET data Web controls—the Repeater, DataList, or DataGrid—then you are already familiar with templates. The example below shows a DataList with its ItemTemplate and HeaderTemplate specified.

<asp:DataList runat="server" id="dlEmployees">
  <HeaderTemplate>
    <b>EMPLOYEE LISTING</b>
  </HeaderTemplate>
  <ItemTemplate>
    Name: <%# DataBinder.Eval(Container.DataItem, "Name") %><br>
    SSN: <%# DataBinder.Eval(Container.DataItem, "SSN") %><br>
    <asp:LinkButton runat="server" Text="Delete" Command="Delete" />
  </ItemTemplate>
</asp:DataList>

As the above Web control syntax illustrates, templates can contain:

  • HTML markup—examples include the HeaderTemplate's contents and the text and <br> tags in the ItemTemplate
  • Web controls—the LinkButton in the ItemTemplate
  • Databound statements—the <%# ... %> syntax in the ItemTemplate.

The DataList is an example of a databound templated control. That is, page developers use the DataList by first setting its DataSource property to some enumerable data, such as a DataReader, DataSet, ArrayList, or any object that implements either IEnumerable or IListSouce interfaces. After setting the DataSource property, the page developer then calls the DataBind() method. The DataBind() method then enumerates through the specified DataSource and for each row creates a new "item" which contains the contents of the ItemTemplate, with the Web controls rendered as HTML and the databound statements resolved to the values in the current row being enumerated through in the DataSource.

Databound templated controls are a more complex, albeit more useful, form of templated controls. Templates can also be used with non-databound controls. For example, a control could use a template to allow for a high degree of customization of the HTML emitted by the control, as shown in the following example:

<skm:DisplayStats runat="server" TotalPostCount="15000">
  <StatsTemplate>
    <i>There have been <%# Container.TotalPostCount %> posts...</i>
  </StatsTemplate>
</skm:DisplayStats>

In this article we'll see how to add template support for non-databound custom ASP.NET server controls. Specifically, we'll look at creating a non-databound templated control, one that displays statistics for a fictional online messageboard site. In a future article, we'll examine a databound templated control that displays the latest entries to a blog.

Adding template support is one of the more advanced features in server control building, so readers of this article should be familiar with the basics of creating custom server controls.

Rendered Controls and Composite Controls

Realize that whenever an ASP.NET Web Form is requested, each of the Web control instances on the page have their public RenderControl() method called, with an HtmlTextWriter instance passed in. The RenderControl() method simply delegates rendering responsibilities to the protected, overridable Render() method. For Web controls, which are designed to always be rendered using a specified HTML element, the Render() method performs the following steps:

  1. The RenderBeginTag() method is called, which generates the beginning tag of the Web control's specified HTML element.
  2. The RenderContents() method is called, which renders the content of the Web control.
  3. The RenderEndTag() method is called, which generates the ending HTML element tag.

All three of these methods are passed the same HtmlTextWriter instance that was initially passed into the RenderControl() method. The job for these methods is to add the appropriate HTML markup to the HtmlTextWriter. After these methods complete, the ASP.NET engine that has handled the page returns the markup in the HtmlTextWriter to the browser that requested the page.

One way to render a custom server control is to override the control's RenderContents() method. There, you can squirt out the appropriate HTML markup to the HtmlTextWriter. Typically, the output sent out would be some stylistic HTML markup along with the values of the control's properties. Web controls that emit their output via the RenderContents() method are said to be rendered controls.

In addition to rendered controls there exists another class of controls referred to as composite controls. Realize that every Web control has a Controls collection which can contain an arbitrary number of other Web controls. Since each control in this Controls collection has, itself, a Controls collection, this collection can be thought of as a hierarchy, as illustrated in Figure 1.

Aa478964.aspnet-buildtemplatedsrvrcntrls-01(en-us,MSDN.10).gif

Figure 1. The Controls Collection Enables a "Control Hierarchy."

When a control is rendered, all of the controls in its Controls collection are also rendered. Therefore, it is possible to render a control by not overriding its RenderContents() method, but by programmatically adding Web controls to its Controls collection.

When working with composite controls it is important to be familiar with a number of properties and methods, as well as with the INamingContainer interface. All composite controls should implement the INamingContainer interface. Controls that implement this interface do not need to add any methods or properties; rather, the implemented interface merely indicates that the control is being used as a composite control. The effect is that child controls—that is, controls in the composite control's Controls collection—are rendered so that their ID is prefixed with the ID of the control's naming container. This ensures that all the child controls will have unique ID values, even if there are multiple instances of the parent control on a Form. The WebControl class has a NamingContainer property that returns the control's parent.

The CreateChildControls() method is a protected method that needs to be overridden for composite controls. It is in this method that we'll add controls to the Controls collection. The ChildControlsCreated property is a Boolean and indicates whether or not CreateChildControls() has yet been called.

The EnsureChildControls() method invokes the CreateChildControls() method if it has not already been invoked. It is important to call this method before allowing a page developer to programmatically alter the Controls collection. Failure to make this check can result in a corrupted ViewState.

A concern when working with composite controls is how to respond to events that are raised by child controls. The method that is commonly used is called event bubbling. With event bubbling, an event that fires in a child control is bubbled up to its parent. The parent, then, can continue to bubble up the event to its parent, or it can handle the event.

For an example of event bubbling, consider a DataList with an ItemTemplate that contains a Button Web control with its Command property set. When the Button is clicked, it raises its Command event. This event is bubbled up to the DataListItem. The DataListItem stops this bubbling, creates a custom event—ItemCommand—and then bubbles up this event to the DataList. Event bubbling is made possible by two methods: OnBubbleEvent() and RaiseBubbleEvent().

When a child control raises an event, it is automatically bubbled up through the control hierarchy. To handle this event, you can override the OnBubbleEvent() method. In this method you'll want to determine if you want to handle this event or not. Handling an event might mean raising a custom event. If you handle an event, this method should return true, which will stop the event from being further bubbled up the hierarchy. If you don't want to handle this event, return false, and the event will continue on its merry way.

The RaiseBubbleEvent() method can be called to start passing an event up the control hierarchy. This is typically called to start bubbling up a custom event, such as the DataListItem does when it catches a bubbled-up Command event.

Realize that templated controls are created using the composite control technique, so it is important to be aware of the issues surrounding development of composite controls. We will not deal with event bubbling in the non-databound control. However, in a future article, when we examine building databound template controls, we will use event bubbling.

Building a Non-Databound Templated Server Control

To demonstrate the use of templates in non-databound controls, let's build a custom server control that will provide a template for customization of output. Specifically, the Web control will be used for displaying statistics for a fictional online forum site, like the ASP.NET Forums. This control, DisplayStats, will have three properties for specifying its appearance:

  • Title—specifies the title to display for the statistics report. This might be something like, "Forum Statistics."
  • TotalPostCount—an integer specifying the total number of posts in the forums.
  • TotalUserCount—an integer specifying the total number of registered users.

By default, DisplayStats will display these three properties using the following HTML markup:

TITLE<br>
TOTAL POST COUNT<br>
TOTAL USER COUNT<br>

Where TITLE, TOTAL POST COUNT, and TOTAL USER COUNT has the value of the Title, TotalPostCount, and TotalUserCount properties. To generate this output, the page developer need add the following declarative syntax to the ASP.NET Web Form:

<skm:DisplayStats runat="server" Title="Statistics"
                  TotalPostCount="14500" TotalUserCount="7500" />

We can allow for customization of the rendered output by adding a template. Specifically, the DisplayStats Web control will have a StatsTemplate. The page developer can use this template to customize the statistics report, as shown below:

<skm:DisplayStats runat="server" Title="Statistics" 
                  TotalPostCount="14500" TotalUserCount="7500">
   <StatsTemplate>
     <table border="0" bgcolor="#eeeeee">
       <tr><th><%# Container.Title %></th></tr>
       <tr>
         <td>
            <b>Posts:</b> <%# Container.TotalPostCount %>
         </td>
       </tr><tr>
          <td>
             <b>Users:</b> <%# Container.TotalUserCount %>
          </td>
       </tr>
     </table>
   </StatsTemplate>
</skm:DisplayStats>

Note that to reference one of the DisplayStat properties in the template, the syntax used is: <%# Container.PropertyName %>.

Now that we have examined the functionality of the DisplayStats control, let's get down to examining how to create the code to implement this non-databound templated control!

Creating the Control and Adding the Custom Properties

Before we even concern ourselves with adding template support to the DisplayStats Web control, let's first look at the steps necessary to build a template-less control. We start by creating a new class that derives from the System.Web.UI.WebControls.WebControl class. This process is made easy by Visual Studio .NET. Simply opt to create a new project, choose your programming language of choice—I'll be using C# in this article—and then select a project type of Web Control Library. This will create a new project for you with a class file. The class file will contain the essential namespaces and a class skeleton for your custom server control. The class, as you'll see, is derived from the WebControl class. Make sure to rename the class to DisplayStats, or whatever you want to name this custom control.

The skeleton class contains a single property, Text, which you can delete since we don't need a Text property for our control. Rather, we want to create three new properties for our custom control's Title, TotalPostCount, and TotalUserCount properties. The following code shows the code for the custom server control class after the three properties have been added. Also note that the Controls collection has been overridden so that the EnsureChildControls() method is called prior to allowing the page developer programmatic access to this collection.

public class DisplayStats : WebControl, INamingContainer
{
   [Bindable(true),
   Category("Appearance"),
   DefaultValue("Site Statistics")]
   public string Title
   {
      get
      {
         object o = ViewState["DisplayStateTitle"];
         if (o == null)
            return "Site Statistics";   // return the default value
         else
            return (string) o;
      }
      set
      {
         ViewState["DisplayStateTitle"] = value;
      }
   }

   [Bindable(true),
   Category("Appearance"),
   DefaultValue("0")]
   public int TotalPostCount
   {
      get
      {
         object o = ViewState["DisplayStateTotalPostCount"];
         if (o == null)
            return 0;   // return the default value
         else
            return (int) o;
      }
      set
      {
         if (value < 0)
            throw new ArgumentException("The total number 
               of posts cannot be less than zero.");
         else
            ViewState["DisplayStateTotalPostCount"] = 
               value;
      }
   }

   [Bindable(true),
   Category("Appearance"),
   DefaultValue("0")]
   public int TotalUserCount
   {
      get
      {
         object o = ViewState["DisplayStateTotalUserCount"];
         if (o == null)
            return 0;   // return the default value
         else
            return (int) o;
      }
      set
      {
         if (value < 0)
            throw new ArgumentException("The total number 
               of users cannot be less than zero.");
         else
            ViewState["DisplayStateTotalUserCount"] = 
               value;
      }
   }


   public override ControlCollection Controls
   {
      get
      {
         this.EnsureChildControls();
         return base.Controls;
      }
   }
}

The important thing to note here is that the property statements' get accessors check the control's ViewState for the property's value. If the value is not found, a default value is returned. Similarly, in the set accessors, the appropriate ViewState item is assigned the specified value. Saving this information in the ViewState ensures that the data is maintained across postbacks. If we didn't store this information in the control's ViewState then in each page load the value of the property would be the property specified in the control's declarative syntax.

Note A thorough discussion of ViewState management is a bit beyond the scope of this article. For a more in-depth discussion, refer back to a previous article of mine, Building an ASP.NET Menu Server Control. Alternatively, consider picking up a copy of Developing Microsoft ASP.NET Server Controls and Components, which discusses Web control state management exhaustively.

Also note that this control implements the INamingContainer interface. This is essential since this control will be a composite control. Too, DisplayStats contains an overridden Controls property statement that calls EnsureChildControls() to guarantee that the control hierarchy is rebuilt prior to allowing the page developer access.

Adding the Template to DisplayStats

When a page developer specifies a template in a Web control, the ASP.NET engine, when parsing the page, builds up a parse tree for the template just as it builds up a parse tree when parsing an entire page. This parser uses this parse tree to create an instance of an object that implements the ITemplate interface. The ITemplate interface defines only one method, InstantiateIn(), which takes a Control instance as input.

After the parser has built up a parse tree and created a corresponding ITemplate type, it assigns the ITemplate to the appropriate property of the control. Therefore, for a control to provide a template, we need to first create a property of type ITemplate. This is done by creating a private member variable of type ITemplate and then making a read-write property statement for this private member variable.

private ITemplate statsTemplate = null;

[
Browsable(false),
DefaultValue(null),
Description("The statistics template."),
TemplateContainer(typeof(StatsData)),
PersistenceMode(PersistenceMode.InnerProperty)
]
public virtual ITemplate StatsTemplate
{
   get
   {
      return statsTemplate;
   }
   set
   {
      statsTemplate = value;
   }
}

At this point the page developer can specify a template in the control's declarative syntax like so:

<skm:DisplayStats runat="server" ...>
   <StatsTemplate>
      ...
   </StatsTemplate>
</skm:DisplayStats>

Note that the name of the template in the declarative markup, StatsTemplate, is the name of the public ITemplate property in the DisplayStats class. While the above syntax is valid, it won't render any output when visited through a browser.

Recall that templates have an InstantiateIn() method that accepts as input a Control class instance. The control that is passed into this method has added to its Controls collection the control hierarchy specified by the template. What we have to do to have the template render is the following:

  1. Create a new class derived directly or indirectly from System.Web.UI.Control that will be used to instantiate the template. This class should have those properties in the DisplayStats class that we want to allow the user to reference in the template—namely Title, TotalPostCount, and TotalUserCount.
  2. In the CreateChildControls() method of the DisplayStats control, we want to create an instance of the class created in step (1) an instantiate the template in this class instance.
  3. Finally, we'll add this class instance to the DisplayStats Controls collection.

Let's look at these three steps in detail.

Creating the StatsData class

The first order of business is to create a class derived either directly or indirectly from the Control class. This class will need to have read-only properties for those properties we want the page developer to be able to access via the template. (I chose to use read-only properties because I didn't want to let the page developer attempt to programmatically alter these values.) The code for this class is quite straightforward and simple, and is shown below:

[ToolboxItem(false)] 
public class StatsData : WebControl, INamingContainer
{
   // private member variables
   private string statsTitle;
   private int totalPostCount;
   private int totalUserCount;

   internal StatsData(string title, int postCount, int userCount)
   {
      this.statsTitle = title;
      totalPostCount = postCount;
      totalUserCount = userCount;
   }

   public string Title
   {
      get
      {
         return this.statsTitle;
      }
   }

   public int TotalPostCount
   {
      get
      {
         return totalPostCount;
      }
   }

   public int TotalUserCount
   {
      get
   {
         return totalUserCount;
      }
   }
}

That's all there is to it! Note the attribute ToolboxItem is used prior to the class definition. The ToolboxItem attribute dictates whether or not the control appears in Visual Studio .NET Toolbox; a value of false here informs Visual Studio .NET that the StatsData class should not be added. Furthermore, notice that StatsData implements the INamingContainer interface, as it will have child controls added to it when it is passed into the template's InstantiateIn() method.

Note   The StatsData class is derived from the WebControl class. If we had wanted the template to render in a specific HTML element, like a table row, we could have instead derived StatsData from the TableRow class. As we'll see in a future article, for databound templated controls it often makes sense to derive the template's container from a more specific class than WebControl.

Examining the DisplayStat's CreateChildControls() Method

The next step is to instantiate the template in an instance of the StatsData class. We want to perform this step in DisplayStat's CreateChildControls() method. Before we do so, we'll need to create a private member variable to hold the StatsData instance used in CreateChildControls(). Too, we'll expose this private member variable through a property statement so that the page developer can programmatically access the StatsData object. (This is akin to the DataList providing the Items property, which is a collection of DataListItem instances.)

This property statement and private member variable are shown below:

private StatsData statsData = null;

[
Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
public StatsData StatsData
{
   get
   {
      this.EnsureChildControls();
      return statsData;
   }
}

The CreateChildControls() method is shown below. It starts by clearing out any controls that might currently be in the Controls hierarchy. Next, it creates a new instance of the StatsData class, setting the StatsData's Title, TotalPostCount, and TotalUserCount properties to the values specified by the page developer. Next, the StatsTemplate InstantiateIn() method is called, passing in the StatsData instance and finally the StatsData instance is added to the Controls collection.

protected override void CreateChildControls()
{
   Controls.Clear();      // clear out the control hierarchy

   // create a new StatsData instance based on the property values
   this.statsData = new StatsData(this.Title, this.TotalPostCount, 
     this.TotalUserCount);

   // instantiate the StatsData in the template
   StatsTemplate.InstantiateIn(statsData);

   Controls.Add(statsData);      // add the StatsData to the control 
     hierarchy
}

Note In the source code that accompanies this article, you'll find a slightly different version of the CreateChildControls() method. The version provided in the code checks to ensure that StatsTemplate is not null, which it might be if the page developer does not explicitly add a <StatsTemplate> tag in DisplayStat's declarative syntax. In such a case, a default template is used. For more details, refer to this article's source code download.

Instantiating a template inside the StatsData leads to the control hierarchy shown in Figure 2.

Aa478964.aspnet-buildtemplatedsrvrcntrls-02(en-us,MSDN.10).gif

Figure 2. The DisplayStats Control Hierarchy

For a non-databound templated control the StatsData may seem like an extra level of unnecessary indirection. The benefit, though, is that it provides a page developer-accessible control that contains the template, and this control is separate from the DisplayStats control. While the benefits of this separation might not be especially clear now, it will become crystal clear in the next article when we examine a databound templated control.

The Finishing Touches—Overriding the DataBind() Method

All that we have left to do with the DisplayStats class is to override the control's DataBind() method. The DataBind() method causes the databinding syntax in the template to be resolved. Inside the DataBind() method we need to call the CreateChildControls() method and then call the base class's DataBind() method. The complete DataBind() code is shown below:

public override void DataBind()
{
   // Create the child controls...
   CreateChildControls();
   this.ChildControlsCreated = true;
      // mark that the children have been created

   base.DataBind ();      // call the DataBind method
}

Testing the DisplayStats Control

To start working with the DisplayStats control, start by compiling the source code. From a Visual Studio .NET Web Application you need to add the DisplayStats compiled assembly to the References (if you are not using Visual Studio .NET, simply copy the assembly to the Web application's /bin directory). Next, in the ASP.NET Web Form where you want to use DisplayStats, add the following @Register directive to the top of the HTML portion:

<%@ Register TagPrefix="skm" Namespace="skmTemplatedControls" 
Assembly="skmTemplatedControls" %>

Then, in the HTML, add the following declarative markup:

<skm:DisplayStats id="anotherTemplateExample" runat="server" 
  Title="Messageboard Stats">
   <StatsTemplate>
      <h2><%# Container.Title %></h2>
      <ul>
         <li>
            <b>Post Count:</b> -
            <%# Container.TotalPostCount %>
         </li>
         <li>
            <b>User Count:</b> -
            <%# Container.TotalUserCount %>
         </li>
      </ul>
   </StatsTemplate>
</skm:DisplayStats>

Note the syntax used here in the databinding statements. Recall that the template is instantiated in a StatsData instance. The Container property references the control that contains the template—StatsData. Therefore, to access the TotalPostCount property of the StatsData control, we use the statement: <%# Container.TotalPostCount %>

In the code-behind class, add the following code to the Page_Load event handler:

private void Page_Load(object sender, System.EventArgs e)
{
   // only bind the data on the first page load...
   if (!Page.IsPostBack)
   {
      Random rndNumber = new Random();
      anotherTemplateExample.TotalPostCount = 
         rndNumber.Next(10000);
      anotherTemplateExample.TotalUserCount = 
         rndNumber.Next(1000);

      anotherTemplateExample.DataBind();
   }
}

If you visit this ASP.NET Web Form through a browser you should see output similar to that in Figure 3.

Aa478964.aspnet-buildtemplatedsrvrcntrls-03(en-us,MSDN.10).gif

Figure 3. The DisplayStats Templated Control in Action

Note   The DisplayStats control can be added to the Visual Studio .NET Toolbox. Once it has been included in the Toolbox, adding the DisplayStats control to a page is as simple as dragging it from the Toolbox and dropping it onto the Design tab.

Conclusion

In this article we examined how to create a non-databound custom ASP.NET server control that employed the use of a template. Realize that the control could be extended to provide multiple templates, much like how the DataList provides a multitude of templates. We added template support by tackling the following steps:

  1. Creating a private member variable of type ITemplate in the DisplayStats control class.
  2. Created a public read-write property statement for the template.
  3. Created a class (StatsData) derived indirectly from the Control class that provided properties that we wanted to allow the page developer to access in the template via databinding syntax.
  4. Overrode the CreateChildControls() method. This method cleared the Controls collection, created an instance of StatsData, then instantiated the StatsData instance in the template, and finally added the StatsData instance to the Controls collection.
  5. Overrode the DataBind() method, which called the CreateChildControls() method.

In the next installment of this article series we'll look at how to provide template support in a databound control, just like the DataList and Repeater provide. Specifically, we'll look at how to build a control to display the latest entries from a blog. Until the next installment, happy programming!

About the Author

Scott Mitchell, author of five books and founder of 4GuysFromRolla.com, has been working with Microsoft Web technologies for the past five years. Scott works as an independent consultant, trainer, and writer. He can be reached at mitchell@4guysfromrolla.com or via his blog, which can be found at http://ScottOnWriting.NET.

© Microsoft Corporation. All rights reserved.