Export (0) Print
Expand All

A Crash Course on ASP.NET Control Development: Building Composite Controls

 

Dino Esposito
Solid Quality Learning

February 2006

Applies to:
   ASP.NET 2.0
   Visual Basic 2005
   Visual C# 2005
   .NET Frameworks
   Visual Web Developer 2005

Summary: Dino Esposito continues his series on ASP.NET control development and in this fourth installment shows you how to use and create composite controls. (16 printed pages)

Both Visual Basic and C# source code are available with this article. Download them from here.

Contents

Introduction
Where's the Point of Composite Controls?
A Common Scenario for Composite Controls
The Rendering Engine of Composite Controls
CompositeControl to Fix Design-Time Issues
Building a Composite Data-Bound Control
Conclusion

Introduction

Composite controls are just plain ASP.NET controls, and not yet another type of ASP.NET server controls to deal with. Hence, why do books and documentation tend to reserve special sections for them? What's so special in ASP.NET composite controls?

As the name should suggest, a composite control is a control that aggregates multiple other components under a single roof and a single API. If you have a custom control that consists of a label and a textbox, then you can say you have a composite control. The word "composite" indicates that the control internally results from the runtime composition of other, constituent controls. The set of methods and properties exposed by a composite control is often, but not necessarily, given by the methods and properties of constituent controls, plus some new members. A composite control can fire custom events too; and it can also handle and bubble up events raised by child controls.

What makes a composite control so special in ASP.NET is not so much its possible identification as the representative of a new breed of server controls. It is rather the support it gets from the ASP.NET runtime when it comes to rendering.

Composite controls are a powerful tool to build rich and complex components that result from the interaction of live objects rather than from the markup output by some string builder objects. A composite control is rendered out as a tree of constituent controls, each with its own lifecycle and events, and all cooperating to form a brand new API, as much abstract as needed.

In this article, I'll discuss the internal architecture of composite controls to make it clear the benefits you get out of them in a number of situations. Next, I'll build a composite list control with a richer set of features compared to the controls I discussed in the previous article.

Where's the Point of Composite Controls?

Some time ago, I was, myself, trying to make sense of composite controls in ASP.NET. I learned the theory and the practice from the MSDN documentation, and worked out some good controls, too. However, it was only when, by mere chance, I ran across the following example that I really got the point (and beauty) of composite controls. Imagine the simplest (and most common) control ever that results from the combination of two other controls—Label and TextBox. Here's a possible way of writing such a control. Let's name it LabelTextBox.

public class LabelTextBox : WebControl, INamingContainer
{
   public string Text {
      get {
         object o = ViewState["Text"];
         if (o == null)
              return String.Empty;
         return (string) o;
      }
      set { ViewState["Text"] = value; }
   }
   public string Title {
      get {
         object o = ViewState["Title"];
         if (o == null)
              return String.Empty;
         return (string) o;
      }
      set { ViewState["Title"] = value; }
   }
   protected override void CreateChildControls()
   {
      Controls.Clear();
      CreateControlHierarchy();
      ClearChildViewState();
   }
   protected virtual void CreateControlHierarchy()
   {
       TextBox t = new TextBox();
       Label l = new Label();
       t.Text = Text;
       l.Text = Title;
       Controls.Add(l);
       Controls.Add(t);
   }
}

The control features two public properties—Text and Title—and the rendering engine. The properties are persisted to the viewstate and represent the contents of the TextBox and Label respectively. The control has no override for the Render method and generates its own markup through the CreateChildControls overridden method. I'll go through the mechanics of the rendering phase in a moment. The code for CreateChildControls first clears the collection of child controls, and then builds the tree of controls that form the output of the current control. CreateControlHierarchy is a control-specific method and doesn't have necessarily to be flagged as protected and virtual. Note, though, the most native composite controls (DataGrid, for example) just expose the logic used to build the control's tree through a similar virtual method.

The CreateControlHierarchy method instantiates as many constituent controls as necessary and composes the final output. When finished, the controls are added to the Controls collection of the current control. If the output of the control is expected to be a HTML table, you create a Table control and add rows and cells with their own contents as appropriate. All rows, cells, and contained controls are children of the outermost table. In this case, you simply add the Table control to the Controls collection. In the preceding code, Label and TextBox are direct children of the LabelTextBox control and are added directly to the collection. The control renders and works just fine.

In terms of pure performance, creating transient instances of controls is not as efficient as rendering out some plain text. Let's consider an alternative way of writing the same control without child controls. Let's name it TextBoxLabel this time.

public class LabelTextBox : WebControl, INamingContainer
{
   :
   protected override void Render(HtmlTextWriter writer)
   {
   string markup = String.Format(
         "<span>{0}</span><input type=text value='{1}'>",
       Title, Text);
      writer.Write(markup);
   }
}

The control features the same two properties—Text and Title—and overrides the Render method. As you can see, the implementation is considerably simpler and provides for slightly faster code. Instead of composing child controls, you compose text in a string builder and output the final markup for the browser. Also in this case, the control renders fine. But can we really say it works fine, as well? Figure 1 shows the two controls running in a sample page.

Aa479016.aspnetcontdev01(en-us,MSDN.10).gif

Figure 1. Similar controls using different rendering engines

Turn tracing on in the page and re-run. When the page displays in the browser, scroll it down and take a look at the control tree. It will be like this:

Aa479016.aspnetcontdev02(en-us,MSDN.10).gif

Figure 2. The control trees generated by the two controls

A composite control is made of live instances of constituent controls. The ASP.NET runtime is aware of these child controls and can talk to them directly when it comes to dealing with posted data. As a result, child controls can handle view state themselves and bubble events automatically.

The situation is different for a control that is based on markup composition. As in the figure, the control is an atomic unit of code with an empty Controls collection. If the markup injects interactive elements in the page (text boxes, buttons, drop-down lists), ASP.NET has no way to handle postback data and events without involving the control itself.

Try entering some text in both text boxes and click the Refresh button of Figure 1 in such a way that a postback occurs. The first control—the composite control—correctly maintains the assigned text across postback. The second control using Render loses the new text across postbacks. Why is it so? There are two combined reasons.

The first reason is that in the preceding markup I don't name the <input> tag. In this way, its contents won't be posted back. Note that you have to name the element using the name attribute. Let's modify the Render method as follows.

protected override void Render(HtmlTextWriter writer)
{
   string markup = String.Format(
      "<span>{0}</span><input type=text value='{1}' name='{2}'>",
      Title, Text, ClientID);
   writer.Write(markup);
}

Now the <input> element injected in the client page has the same ID as the server control. When the page posts back, the ASP.NET runtime can find a server control that matches the ID of the posted field. However, it doesn't know how to work with it. For ASP.NET to apply all the client changes to the server control, the control must implement the IPostBackDataHandler interface.

A composite control that includes a TextBox need not worry about postbacks, as the embedded control will work it out with ASP.NET automatically. A control that renders a TextBox needs to interact with ASP.NET to ensure that posted values are correctly handled and that events fire as expected. The following code shows how to extend the TextBoxLabel control to make it fully support postbacks.

bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
    string currentText = Text;
    string postedText = postCollection[postDataKey];
    if (!currentText.Equals(postedText, StringComparison.Ordinal))
    {
        Text = postedText;
        return true;
    }
    return false;
}
void IPostBackDataHandler.RaisePostDataChangedEvent()
{
    return;
}

A Common Scenario for Composite Controls

Composite controls are the right tool to architect complex components in which multiple child controls are aggregated and interact among themselves and with the outside world. Rendered controls are just right for read-only aggregation of controls in which the output doesn't include interactive elements such as drop-down or text boxes.

If you're interested in event handling and posted data, I heartily suggest you opt for composite controls. Building a complex control tree comes easier, and the final result is neater and more elegant, if you use child controls. Additionally, you have to deal with postback interfaces only if needed to provide additional functionalities.

Rendered controls require the implementation of additional interfaces, not to mention the work with needle and thread to sew together static portions of markup with property values.

Composite controls are also great to render list of homogeneous items, like in a DataGrid control. Having each constituent item available as a live object allows you to fire creation events and access their properties programmatically. In ASP.NET 2.0, most of the boilerplate code required for a full implementation of realistic and data-bound composite controls—the previous were just toy examples—is buried in the folds of a new base class: CompositeDataBoundControl.

The Rendering Engine of Composite Controls

Before we take the plunge into ASP.NET 2.0 coding techniques, let's review the internal mechanics of composite controls. As mentioned, the rendering of a composite control is centered around the CreateChildControls method that is inherited from the base Control class. You might think that overriding the Render method is essential for a server control to render out its contents. As we've seen earlier, this is not always required if CreateChildControls is overridden. But when is CreateChildControls invoked in the control's call stack?

The first time the page is displayed, CreateChildControls is invoked during the pre-rendering phase, as the figure demonstrates.

Aa479016.aspnetcontdev03(en-us,MSDN.10).gif

Figure 3. CreateChildControls is invoked during the pre-rendering phase

In particular, the request processing code (in the Page class) calls EnsureChildControls immediately before firing the PreRender event to the page and each child control. In other words, no controls are rendered if the tree has not been built entirely.

The following code snippet illustrates the pseudo-code for EnsureChildControls—another method defined on Control.

protected virtual void EnsureChildControls()
{
   if (!ChildControlsCreated)
   {
       try {
          CreateChildControls();
       }
       finally {
          ChildControlsCreated = true;
       }
   }
}

The method may be repeatedly invoked during the life cycle of pages and controls. To avoid controls duplication, the ChildControlsCreated property is set to true. If the property returns true, the method exits immediately.

When the page posts back, ChildControlsCreated is invoked earlier in the cycle. As in Figure 4, it gets called during the posted data processing phase.

Aa479016.aspnetcontdev04(en-us,MSDN.10).gif

Figure 4. Invoked during the posted data processing phase in case of postback

When the ASP.NET page begins processing the data posted from the client, it attempts to find a server control whose ID matches the name of the posted field. In doing so, the page code invokes the FindControl method on the Control class. This method, in turn, needs to make sure that the control's tree is entirely built before proceeding, so it invokes EnsureChildControls and has the control's hierarchy built, as needed.

What about the code to execute inside the CreateChildControls method? Although there are no official guidelines to follow, it is commonly agreed that CreateChildControls must accomplish at least the following tasks: clear the Controls collection, build the control's tree, and clear the viewstate of child controls. Setting the ChildControlsCreated property from within the CreateChildControls method is not strictly required. The ASP.NET page framework, in fact, always invokes CreateChildControls through EnsureChildControls, which sets the boolean flag automatically.

CompositeControl to Fix Design-Time Issues

ASP.NET 2.0 comes with a base class named CompositeControl. Therefore, new composite, not data-bound, controls should be derived from this class instead of WebControl. The use of CompositeControl doesn't change much in the way you develop the control. You still need to override CreateChildControls and code your way as shown previously. So what's the role of CompositeControl? Let's start from its prototype:

public class CompositeControl : WebControl, 
                                INamingContainer, 
                                ICompositeControlDesignerAccessor

The class saves you from decorating the control with INamingContainer—not a big savings, actually, as the interface is only a marker and has no methods. More importantly, the class implements a brand new interface named ICompositeControlDesignerAccessor.

public interface ICompositeControlDesignerAccessor
{
   void RecreateChildControls();
}

The interface is used by the standard designer of composite controls to recreate the control tree at design time. Here's the default implementation of the method in CompositeControl.

void ICompositeControlDesignerAccessor.RecreateChildControls()
{
   base.ChildControlsCreated = false;
   EnsureChildControls();
}

To cut a long story short, if you derive a composite control from CompositeControl you don't experience design-time troubles and don't have to resort to tricks and tweaks to make the control work just fine both at run time and design time.

To fully understand the importance of this interface, take the sample page that hosts a LabelTextBox composite control and turn it into design mode. The control works fine at run time, but is invisible at design time.

Aa479016.aspnetcontdev05(en-us,MSDN.10).gif

Figure 5. Composite controls don't enjoy special design-time treatment unless you derive them from CompositeControl

If you simply replace WebControl with CompositeControl, the control will still work fine at run time, but behaves well at design time, too.

Aa479016.aspnetcontdev06(en-us,MSDN.10).gif

Figure 6. Composite controls that work great at design time

Building a Composite Data-Bound Control

Most complex server controls are data-bound, perhaps templated, and formed by a variety of child controls. These controls maintain a list of constituent items—typically rows, or cells, of a table. The list is persisted across postbacks in the viewstate and is built from bound data or rebuilt from the viewstate. The control also saves in the viewstate the number of its constituent items so that the structure of the table can be correctly recreated in case of a postback caused by other controls in the page. Let me exemplify with a DataGrid control.

The DataGrid is made of a list of rows, each of which represents a record in the bound data source. Each grid row is represented with a DataGridRow object—a class derived from TableRow. When individual grid rows are created and added to the final grid table, proper events such as ItemCreated and ItemDataBound are fired to the page. When the DataGrid is created via data binding, the number of rows is determined by the number of items bound and the page size. What if the page with the DataGrid posts back?

In this case, if the DataGrid itself causes the postback— the user clicked to sort or page, for example—the new page will render the DataGrid through data binding again. This is obvious, as the DataGrid needs fresh data to display. Things are different if the host page posts back because another control on the page is clicked—say, a button. In this case, the DataGrid is not bound to data and must be rebuilt from the viewstate. (If the viewstate is disabled, that's another story and the grid can only be displayed through data binding.)

The data source is not persisted in the viewstate. As a composite control, the DataGrid contains child controls, each of which persists its own state to the viewstate and restores it from there. The DataGrid simply needs to track how many times it has to iterate until all rows and contained controls are restored from viewstate. This number coincides with the number of displayed bound items and must be stored in the viewstate as a part of control's state. In ASP.NET 1.x, you had to learn and implement this pattern yourself. In ASP.NET 2.0, it suffices that you derive your composite control from the new class CompositeDataBoundControl.

Let's try with a grid-like control that displays expandable data-bound news headlines. In doing so, we'll reuse the Headline control that we covered in previous articles.

public class HeadlineListEx : CompositeDataBoundControl
{
  :
}

The HeadlineListEx control counts an Items collection property in which all bound data items are collected. The collection is public and can also be populated programmatically as it happens with most list controls. Support for classic data binding is implemented through a couple of properties—DataTextField and DataTitleField. These properties indicate the fields on the data source that will be used to populate title and text of the news. The Items collection is persisted to the viewstate.

To transform the HeadlineListEx control into a true composite control, you first derive it from CompositeDataBoundControl and then override CreateChildControls. It is interesting to note that CreateChildControls is overloaded.

override int CreateChildControls()
override int CreateChildControls(IEnumerable data, bool dataBinding)

The first overload overrides the method as defined on the Control class. The second overload is an abstract method that each composite control must override. In practice, developing a composite controls reduces to two main tasks:

  • Overriding CreateChildControls.
  • Implementing a Rows collection property to track all the constituent items of the control.

The Rows property differs from Items because it is not persisted in the viewstate, has the same lifetime as the request, and references helper objects, not bound data items.

public virtual HeadlineRowCollection Rows
{
    get
    {
        if (_rows == null)
            _rows = new HeadlineRowCollection();
         return _rows;
     }
}

The Rows collection is populated as the control is built. Let's take a look at the override of CreateChildControls. The method takes two arguments—the bound items and a boolean flag that indicates whether the control is being created through data binding or viewstate.

override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
   if (dataBinding)
   {
      string textField = DataTextField;
      string titleField = DataTitleField;
      if (dataSource != null)
      {
         foreach (object o in dataSource)
         {
            HeadlineItem elem = new HeadlineItem();
            elem.Text = DataBinder.GetPropertyValue(o, textField, null);
            elem.Title = DataBinder.GetPropertyValue(o, titleField, null);
            Items.Add(elem);
         }
      }
   } 

   // Start building the hierarchy of controls
   Table t = new Table();
   Controls.Add(t);
   Rows.Clear();
   int itemCount = 0;

   foreach(HeadlineItem item in Items)
   {
       HeadlineRowType type = HeadlineRowType.Simple;
       HeadlineRow row = CreateHeadlineRow(t, type, 
                                           item, itemCount, dataBinding);
       _rows.Add(row);
       itemCount++;
    }

    return itemCount;
}

In the case of data binding, you first populate the Items collection. You loop through the bound collection, extract data, and fill newly created instances of the HeadlineItem class. Next, you loop through the Items collection—which might contain additional items added programmatically—and create rows in the control.

HeadlineRow CreateHeadlineRow(Table t, HeadlineRowType rowType, 
                      HeadlineItem dataItem, int index, bool dataBinding)
{
   // Create a new row for the outermost table
   HeadlineRow row = new HeadlineRow(rowType);

   // Create the cell for a child control
   TableCell cell = new TableCell();
   row.Cells.Add(cell);
   Headline item = new Headline();
   cell.Controls.Add(item);

   // Fire HERE a HeadlineRowCreated event 

   // Add the row to the HTML table being created
   t.Rows.Add(row);

   // Handle the data object binding
   if (dataBinding)
   {
       row.DataItem = dataItem;
       Headline ctl = (Headline) cell.Controls[0];
       ctl.Text = dataItem.Text;
       ctl.Title = dataItem.Title;
                
       // Fire HERE a HeadlineRowDataBound event 
    }
    return row;
}

The CreateHeadlineRow method creates and returns an instance of the HeadlineRow class—a class derived from TableRow. In this case, the row contains a cell filled with a Headline control. In other situations, you can change this part of the code to add as many cells as needed and filled as appropriate.

It is important that you divide the tasks to be accomplished in two distinct steps—creation and data binding. You first create the layout of the row, fire the row-created event, if any, and finally add it to the parent table. Next, if the control is being bound to data you set the child control properties sensitive to bound data. When done, you fire a row-data-bound event, if any.

Note that this is the pattern that better describes the internal architecture of ASP.NET native composite controls.

To fire events, you can use the following code.

HeadlineRowEventArgs e = new HeadlineRowEventArgs();
e.DataItem = dataItem;
e.RowIndex = index;
e.RowType = rowType;
e.Item = row;
OnHeadlineRowDataBound(e);

Note that you set the DataItem property only if you're firing the data-bound event. The event data structure is arbitrarily set to the following. Feel free to change it if you deem it worthwhile.

public class HeadlineRowEventArgs : EventArgs
{
   public HeadlineItem DataItem;
   public HeadlineRowType RowType;
   public int RowIndex;
   public HeadlineRow Item;
}

To physically fire an event, you typically use a protected method, defined as follows.

protected virtual void OnHeadlineRowDataBound(HeadlineRowEventArgs e)
{
   if (HeadlineRowDataBound != null)
      HeadlineRowDataBound(this, e);
}

To declare the event, in ASP.NET 2.0 you can use the new generic event handler delegate.

public event EventHandler<HeadlineRowEventArgs> HeadlineRowDataBound;

In a sample page, things go as usual. You define the handler on the control markup and write a method in the code file. Here's an example.

<cc1:HeadlineListEx runat="server" ID="HeadlineListEx1" 
    DataTextField="notes" DataTitleField="lastname" 
    DataSourceID="MySource" OnHeadlineRowDataBound="HeadlineRowCreated" />

The code for the HeadlineRowCreated event handler is shown here.

protected void HeadlineRowCreated(object sender, HeadlineRowEventArgs e)
{
   if (e.DataItem.Title.Contains("Doe"))
      e.Item.BackColor = Color.Red;
}

Aa479016.aspnetcontdev07(en-us,MSDN.10).gif

Figure 7. The HeadlineListEx control in action

By hooking the data-bound event, all items that contain Doe are rendered with a red background.

Conclusion

Composite controls are controls that are created by aggregating other controls under the roof of a common API. A composite control maintains live instances of its own child controls and doesn't limit to ask them to render out. You can easily see this by inspecting the control tree section in the trace output of a page. A few benefits derive from using composite controls, such as simplified event and postback handling. Building a complex data-bound control is a bit tricky in ASP.NET 1.x and requires a deep understanding of some implementation details. Most of this complexity is abstracted away in ASP.NET with the introduction of the CompositeDataBoundControl base class. In the end, in ASP.NET 2.0, you use the CompositeControl base class if you need a composite control that is not data-bound. For data-bound composite control you look at CompositeDataBoundControl instead. In both cases, you must provide a valid override of the CreateChildControls method, which is the heart of any composite control—where the hierarchy of child controls is created.

 

About the author

Dino Esposito is a Solid Quality Learning mentor and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch at cutting@microsoft.com or join the blog at http://weblogs.asp.net/despos.

Show:
© 2014 Microsoft