A Crash Course on ASP.NET Control Development: Template Properties

 

Dino Esposito
Solid Quality Learning

March 2006

Summary: Dino Esposito continues his series on ASP.NET 2.0 controls development with this look at how to create template properties in a custom control. (14 printed pages)

Applies to:
   Microsoft ASP.NET 2.0

Download the associated code sample, ControlDev06.msi.

Contents

The ABCs of Control Templates
Internals of Template Properties
Defining a Template Property
Rendering Templates
The Template Container Class
The TemplateInstance Attribute
Parsing Nested Tags
Summary

Several controls include properties to set with text and values that display through the control's user interface (UI). The typical example is the Caption property of a GridView control; you set the text and the control renders it out on top of the grid. Such a property doesn't offer a great deal of flexibility, because all that it can do is show a static text. The passed value can be made more context-specific through dynamic formatting or perhaps data binding. Rest assured that the control layout that hosts the property value is fixed and immutable.

How can you design control properties that not just display context-specific information (i.e., data-bound information) but also support user-defined layouts? In ASP.NET, these properties are quite common and are implemented through templates. A template is a way to specify a custom user interface layout for certain graphical parts of the control.

The ABCs of Control Templates

Many ASP.NET controls expose template properties. Usually, a template property represents a block of user interface that has a default appearance that page authors can further personalize by specifying custom markup. As an example, let's briefly consider the new Login control.

The Login control is a composite control that provides all the common UI elements needed to authenticate a user on a Web site, such as a user name and password pair of text boxes and a submit button. These child controls are normally laid out and hard-coded in a standard way, as shown in Figure 1.

Aa479300.ccctemplates_01(en-us,MSDN.10).gif

Figure 1. The standard format of the Login control

The control, though, also provides a number of optional UI elements that support additional functions. Some of these elements are a link for a password reminder, a "Remember Me" check box for caching credentials between sessions, a "Register New User" link that redirects users to a registration page, instructions, and error messages that appear within the control.

All UI text messages are customizable through properties of the class. In addition, the appearance of the Login control is fully customizable through the LayoutTemplate property. The property gets and sets the template used to render out the Login control. Figure 2 shows a user-defined layout for the Login control.

Aa479300.ccctemplates_02(en-us,MSDN.10).gif

Figure 2. A Login control based on a custom layout

The source code for a template property is shown below:

<asp:login runat="server" ... >
    <layouttemplate>
        :
    </layouttemplate>
</asp:login>

As you can see, the control markup is extended to encompass a custom set of child controls and their properties. However, the surrounding tag, interpreted as a template, is parsed to build a control tree that is finally associated with the template property and used accordingly by the control.

In the end, a template property indicates a user-defined control tree that will be inserted at a given point in the overall output hierarchy for the control being implemented. Each control can support any number of template properties and decide where each template tree should be placed.

How do you add the template capabilities to your own controls? Before answering that question, a review of template internals is in order.

Internals of Template Properties

A template property is a property of type ITemplate. The ITemplate interface is declared in the System.Web.UI namespace as follows:

public interface ITemplate
{
   void InstantiateIn(Control container);
}

From the control developer's perspective, a template is a control tree to add at some point in the control's hierarchy. The developer defines a sort of logical placeholder for the template. When the rendering procedure gets to render the portion of the hierarchy where the placeholder is logically defined, it creates a container for the template tree, builds the template tree, and adds it to the main control tree.

From the page author's perspective, a template is a tool to modify the layout of certain portions of a given control. The page author knows from the control documentation which parts of the control the template is going to modify. The page developer then uses this information to plan a customized appearance for the control.

A template property can be set in two ways. First, you can explicitly indicate the template tree in the control markup, as you can see from the preceding <asp:login> code snippet. Second, you can programmatically set the ITemplate property with the instance of a class that implements the ITemplate interface.

login1.LayoutTemplate = new MyLoginTemplate();

The sample class MyLoginTemplate is built to implement the InstantiateIn method and build the template control tree on top of the container parameter. Basically, you programmatically build instances of child controls and add them to the container parameter.

Another interesting approach to setting template properties is based on Web user controls (*.ascx). The idea is that you define the template tree in the ASCX component and then load it into the template container through the Page.LoadTemplate method.

login1.LayoutTemplate = Page.LoadTemplate(ascxUrl);

The method builds a temporary class parsing the contents of the ASCX component and returns an instance of that class. The control then calls InstantiateIn on the class to incorporate the template tree.

Using a Web user control allows you to decouple the control code and the template code and makes possible to change the template on the fly without touching the source code. On the other hand, a separate file means that you have one more file to deploy and maintain.

Defining a Template Property

A template property is implemented as a read-write property of type ITemplate. The property storage is guaranteed by a local property; subsequently, no template-related information is persisted to the viewstate. This is not an issue if the template code is saved in the page source code; the template is, in fact, rebuilt automatically on every postback. However, if you load the template contents from an ASCX file, you should be aware that the template information is lost on postbacks, unless you reload it in the Page_Load event. Note, though, that you have to load the template on all postbacks and not just when IsPostBack is false.

void Page_Load(object sender, EventArgs e)
{
   login1.LayoutTemplate = Page.LoadTemplate(ascxUrl);
   if (!IsPostBack)
   {
      :
   }
}

Let's review the implementation of a real-world template property: the EmptyDataTemplate property of the GridView and DetailsView control. The property gets or sets the content of the control that is rendered when the control is bound to an empty data source.

private ITemplate _emptyDataTemplate;
public virtual ITemplate EmptyDataTemplate
{
      get { return _emptyDataTemplate; }
      set { _emptyDataTemplate = value; }
}

In addition, a typical template property is decorated with a few attributes as below:

Browsable(false)
PersistenceMode(PersistenceMode.InnerProperty)
TemplateContainer(typeof(GridViewRow))]

The Browsable attribute indicates whether the property is editable at design time through the Properties window. A template property is usually edited through ad hoc tasks that developers can invoke via the design-time control's context menu.

The PersistenceMode attribute indicates how the template information should be serialized in the page source code. The InnerProperty value specifies that the property persists as a nested tag within the opening and closing tags of the control.

<asp:GridView runat="server" ...>
   <EmptyDataTemplate>
     :
   </EmptyDataTemplate>
</asp:GridView>

Finally, the TemplateContainer attribute indicates the base type of the container of the template property. This information, though, is not required to implement the property or render the content of the template. Just as with many other control attributes, the TemplateContainer attribute is not consumed by the control itself. The information it carries out is used by the ASP.NET parser to know the type of the Container object that is used in data-binding expressions. Consider the following expression that is pretty common in grid examples. The code snippet describes the user-defined layout of a GridView column:

<asp:TemplateField>
   <ItemTemplate>
       <asp:TextBox runat="server" 
            Text="<%# DataBinder.Eval(Container.DataItem, "notes") %>" />
   </ItemTemplate>
</asp:TemplateField>

The Container expression denotes the object that is the innermost container of the template where the data-binding expression is being used. In this context, Container looks a lot like a cross-language super keyword. The ASP.NET parser resolves any reference to Container with an object of the type declared by the TemplateContainer attribute. The value of the TemplateContainer attribute is read on the template property that contains the Container expression.

It is important to remark that any control with a template property must implement the INamingContainer interface for the Container expression to be correctly exposed to the user-defined code.

Rendering Templates

Declaring the template property is only half the task, and not certainly the trickiest part. How would you render the content of the template? It is essential to note that the ASP.NET parser does most of the work with templates. At run time, a template property simply contains a reference to an object which, in turn, points to a control tree. This control tree is wrapped by a class that exposes the ITemplate interface and internally builds the tree from the markup read out of the page source.

From a control developer's perspective, incorporating a template into the overall control markup is as easy as invoking the ITemplate.InstantiateIn method on the object referenced by the template property.

To see a concrete example of a template property, let's extend the GridView control with a template for the caption. In ASP.NET 2.0, the GridView control features a Caption property that results in a text rendered just on top of the grid. To be precise, the displayed text is not a row in the table grid, but it is rendered through the <caption> element that HTML tables support for accessibility compliance.

A GridView control with the Caption property set to a non-empty string generates the following markup:

<table>
   <caption> ... </caption>
   <tr> …
   <tr> …
       :
</table>

There's no way to alter the way in which the caption is rendered; however, you can add a new CaptionTemplate property and implement it to add a new table row on top of the grid. The new row will contain a single cell that spans the whole column set. You can make Caption and CaptionTemplate coexist, or code the two properties to be mutually exclusive—it is entirely up to you. Here's the modified GridView control:

public class GridView : System.Web.UI.WebControls.GridView
{
    public GridView()
    {
    }

    private ITemplate _captionTemplate;
    private int _totalColumns;

    [Browsable(false)]
    [PersistenceMode(PersistenceMode.InnerProperty)]
    [TemplateContainer(typeof(CaptionContainer))]
    public ITemplate CaptionTemplate
    {
        get { return _captionTemplate; }
        set { _captionTemplate = value; }
    }

    protected override void  PrepareControlHierarchy()
    {
        base.PrepareControlHierarchy();

        Table t = (Table)Controls[0]; 
        if (_captionTemplate != null && String.IsNullOrEmpty(Caption))
        {
            TableRow row = new TableRow();
            t.Rows.AddAt(0, row);
            TableCell cell = new TableCell();
            cell.ColumnSpan = _totalColumns; 
            row.Cells.Add(cell);

            CaptionContainer cc = new CaptionContainer(this);
            _captionTemplate.InstantiateIn(cc);
            cell.Controls.Add(cc);
            cell.DataBind();
        }

        return;
    }

    protected override ICollection CreateColumns(
              PagedDataSource dataSource, bool useDataSource)
    {
        ICollection coll = base.CreateColumns(dataSource, useDataSource);
        _totalColumns = coll.Count;
        return coll;
    }
}

public class CaptionContainer : WebControl, INamingContainer
{
    private GridView _grid;
    public CaptionContainer(GridView g)
    {
        _grid = g;
    }

    public GridView GridView
    {
        get { return _grid; }
    }
}

The new GridView control defines the CaptionTemplate property and overrides a couple of GridView virtual methods, PrepareControlHierarchy and CreateColumns. The CaptionTemplate property is implemented as described above and uses a custom class as its container. (I'll return on template container classes in a moment.)

You need to override a couple of methods in order to inject the template tree in an additional grid row. The overall table that forms the grid is created in the CreateChildTable overridable method. The first option is the following:

protected override Table CreateChildTable()
{
   Table t = base.CreateChildTable();

   // Add the new row
   TableRow row = new TableRow();
   t.AddAt(0, row);
  
   // Add a cell to contain the template
   :

   // Return the modified table
   return t;
}

In this way, though, you add a new TableRow object where a GridViewRow object is expected. GridViewRow derives from TableRow and nothing bad happens until the GridView control is styled for rendering. This is what happens when the PrepareControlHierarchy is invoked. In this context, the base code of the GridView control attempts a cast that fails and throws an exception. So you override PrepareControlHierarchy instead, call the base method, and then add the new table row.

You need to know the exact number of columns in the grid to set the ColumnSpan property of the unique cell in the row. This information depends on autogenerated columns and the contents of the Fields collection. The Fields collection is empty if the columns for the grid are autogenerated. To stay on the safe side, you need to override CreateColumns and cache the number of total columns for further use.

In your implementation of PrepareControlHierarchy, you retrieve a reference to the grid's table through the Controls collection—the table is, in fact, the first element in the Controls collection. Next, you add a single-cell row to the table. Finally, you write the code to import the template tree in the control's tree.

CaptionContainer cc = new CaptionContainer(this);
_captionTemplate.InstantiateIn(cc);
cell.Controls.Add(cc);
cell.DataBind();

To incorporate a template, you create an instance of the container class and call InstantiateIn on the object exposed through the template property. When done, you just add the container to the control.

A fundamental further step is required to enable the template to successfully process data-bound expressions. You must place a call to DataBind on the cell element. A template means little if you can't use data-binding expressions in it. And data-binding expressions are evaluated only after a call to DataBind is made. Without the DataBind call, templates will still work correctly but won't display any <%# ... %> expression.

<x:GridView ID="GridView1" runat="server" DataSourceID="SqlDataSource1">
  <CaptionTemplate>
      The grid has <%# Container.GridView.Rows.Count.ToString() %> rows.
  </CaptionTemplate>
<x:GridView>

Figure 3 shows the CaptionTemplate property in action. The topmost row can now contain virtually any sort of control tree bound to any data you can expose through the container. Let's dig out what's behind the Container expression.

Aa479300.ccctemplates_03(en-us,MSDN.10).gif

Figure 3. A modified GridView control with a custom caption template

The Template Container Class

The template container class serves two main purposes. First, it is the parent control that wraps the template, thus providing a single entry point to the contents. In this way, attaching the template tree to the host control is much easier, no matter the internal structure of the template. The parser builds a class that generates the tree and the container inserts this tree into the overall control tree.

An ASP.NET template would be of little use without a mechanism to inject external data. The template container serves this purpose. An instance of the container class is exposed through the Container expression in data-binding expressions. Any public properties you declare on the template container can be accessed from within the Container expression. Here's the code of a typical template container class:

public class CaptionContainer : WebControl, INamingContainer
{
    private GridView _grid;
    public CaptionContainer(GridView g)
    {
        _grid = g;
    }

    public GridView GridView
    {
        get { return _grid; }
    }
}

The class receives an instance of the host control in the constructor and exposes it through a new property. The public interface of the container class is ultimately up to you. You can decide to expose the host control—the GridView in this case—as a whole or add a public property per each chunk of information you want to expose. For example, you can add a numeric property to return the number of rows:

public int RowCount
{
   get { return _grid.Rows.Count; }
}

Note that you are not limited to information related to the host control, even though any other information might be out of place. Any public property on the template container class can be successfully used in data-binding expressions within the template, as in the code snippet below:

<CaptionTemplate>
   The grid has <%# Container.GridView.Rows.Count.ToString() %> rows.
</CaptionTemplate>

The TemplateInstance Attribute

In control development, templates are essentially used in two circumstances. Some controls use a template to let page authors declare some repeatable contents. A good example is the Repeater control. To set the ItemTemplate property, for example, you tell the Repeater to repeat the template for each bound item. In this case, child controls in the template are scoped to the template instance and are given an ID that is guaranteed to be unique within the parent template but not within the overall control tree.

To get a reference to a particular control instance you need to use FindControl from the template container. As a result, you can't access a particular child control in a particular instance of the template from the page level. However, in the context of a repeatable template, this is not a big deal.

The second circumstance in which templates prove useful is when you use them to customize a portion of the control's user interface. This is exactly the scenario that I addressed with the previous example. The CaptionTemplate property refers to a single, non-repeatable template that page authors use to customize the top-most row of the grid. Also in this case, child controls in the template are scoped to the template and are not visible from the page level. However, for a non-repeatable template this may be seen as a limitation.

In ASP.NET 2.0, you can use the new attribute TemplateInstance to promote child controls of a template to the page level so that they can be directly accessed by name from the page without resorting to FindControl. Possible values for the attribute come from the TemplateInstance enumeration: Single and Multiple. The attribute specifies how many times an instance of a template can be created.

[TemplateInstance(TemplateInstance.Single)]
public ITemplate CaptionTemplate
{
   :
}

When should you use the TemplateInstance attribute?

The TemplateInstance attribute makes sense only for non-repeatable templates such as the CaptionTemplate of the custom grid created in this article. However, not all non-repeatable templates are necessarily good candidates for this attribute. For example, the EmptyDataTemplate property of the GridView control is a non-repeatable template but is not flagged with the TemplateInstance attribute. Why is this so?

Like the CaptionTemplate discussed in this article, the EmptyDataTemplate is designed to be a totally custom container of user-defined markup. Each page author can stuff in the template a different set of controls. Having these controls callable from the page level might still be useful in some cases, but doesn't deliver a clear, unequivocal advantage. Using the attribute in these cases is mostly a matter of preference.

The TemplateInstance attribute provides some concrete added value when the template refers to a UI block with some common elements. In other words, if the template is mostly a way to skin differently a standard piece of user interface, you might want to decorate the property with the attribute.

In ASP.NET, Web Parts zones (e.g., catalog and editor zones) feature a ZoneTemplate property that contains Web Parts. The TemplateInstance attribute in this case makes it easier for pages to access their internal components. Although defined within a template, Web Parts are much more than just child controls of a page. Their relevance exceeds the boundaries of the template and qualifies them as first-level children of the page; hence, the use of the attribute. A more thorough and insightful discussion of the TemplateInstance attribute can be found here.

Parsing Nested Tags

Templates are saved into the page source code and the contents are processed by the page parser. A template is persisted through a child tag nested in the control's root tag, as in the code snippet below:

<x:gridview runat="server" ... >
    <captiontemplate>
        :
    </captiontemplate>

    <!-- other tags go here -->
</x:gridview>

The server markup of an ASP.NET control is processed by a component known as the control builder. This component parses the source and builds the control tree. In doing so, the control builder interacts with any template properties.

Two attributes affect how the template is persisted to the ASPX source file and how the control builder works. These two attributes are ParseChildren and the aforementioned PersistenceMode. Let's tackle the latter first.

The PersistenceMode attribute specifies how a control property is persisted declaratively in a source file. Table 1 lists all possible modes.

Table 1. Feasible values for the PersistenceMode enumeration

Value Description
Attribute The property should be persisted as an attribute.
EncodedInnerDefaultProperty The property should be persisted as the only inner text of the control. The property value is HTML encoded. This value applies only to string properties.
InnerDefaultProperty The property should be persisted in the control as inner text. This property is the only control property that persists to markup as a nested tag.
InnerProperty The property persists in the control as a nested tag.

Most template properties are persisted using the InnerProperty approach; that is, templates are just one of the child tags of the control's server markup.

ParseChildren enables a control to specify how the page parser should interpret any nested elements within the control's declaration in an ASP.NET page. The attribute supports two properties—ChildrenAsProperties and PropertyName. The ChildrenAsProperties property represents a Boolean value; the PropertyName property is a string and represents the name of the control's default property.

When ChildrenAsProperties is set to true (the default setting), the parser expects that any nested elements correspond to a public property of the control, and generates an error otherwise. When ChildrenAsProperties is false, the page parser assumes that child tags are ASP.NET server controls. The page parser creates the child controls and calls the AddParsedSubObject method on the control. Controls can override the default implementation of AddParsedSubObject—a protected overridable method of Control. By default, the method adds the child controls to the Controls collection of the parent. Literal text found between tags are parsed as instances of the LiteralControl class.

Note that you can use a control builder component to change the default logic for parsing child tags.

Summary

Most ASP.NET controls have a default appearance and layout but let page developers manipulate both through properties and styles. Some controls also provide deeper customization based on templates.

A template is a set of HTML elements and controls that define the layout for a particular section of a control. A template can be set either declaratively by specifying the markup in the ASPX source or programmatically by loading the contents from a Web user control. In this article, I discussed how to create template properties in a custom control and the issues related to that.

 

About the Author

Dino Esposito is a mentor at Solid Quality Learning and the author of Programming Microsoft ASP.NET 2.0 Core Reference and Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics, both from Microsoft Press. Late breaking news is available at https://weblogs.asp.net/despos.

© Microsoft Corporation. All rights reserved.