Export (0) Print
Expand All

Building DataBound Templated Custom ASP.NET Server Controls

 

Scott Mitchell
4GuysFromRolla.com

March 2004

Applies to:
   Microsoft® ASP.NET

Summary: Examines using templates in databound Web controls, and builds a custom server control that displays content items syndicated using the RSS specification. Also looks at a bevy of topics of concern when developing templated databound controls, including creating the control's content based on external data, raising events during the control's construction, and bubbling events that occur in the templates. (27 printed pages)

Download the source code for this article.

Contents

Introduction
RssFeed: a Custom Server Control for Displaying RSS Syndication Feeds
Creating a Non-Templated DataBound Control
Adding Templates to the DataBound Control
Conclusion
About the Author

Introduction

While Microsoft® ASP.NET server controls are capable of maintaining their state across postbacks and of raising server-side events in response to client-side events, the main objective and most important task for server controls is rendering. Rendering is the process of generating the corresponding HTML markup, and is something all server controls do. In fact, the HTML returned to the browser from an ASP.NET Web page is simply the sum of the page's controls' markup.

The HTML emitted by a server control is typically based upon the values of its properties. In fact, the HTML emitted by the majority of Web controls is based upon just its property values. For example, the TextBox Web control will always emit an <input> HTML element. The attributes of this element can differ based on the property values, but there's no way a page developer could radically change the emitted HTML of the TextBox.

There are, however, controls that do allow for a much finer degree of control over the emitted HTML. The most common ones are the data Web controls—the DataGrid, DataList, and Repeater—which use templates to allow the page developer to customize their rendered HTML. Templates can contain a mix of HTML syntax, Web controls, and databinding syntax.

When creating your own custom server controls, you might want to provide page developers using the controls a greater amount of flexibility in determining the control's rendered output. In an earlier article, Building Templated Custom ASP.NET Server Controls, I examined the basics of templated controls and looked at an example of creating a non-databound templated control. In this article, we will examine creating databound templated controls, building a custom server control to display content syndicated through RSS.

Note   The data Web controls are examples of databound templated controls, as their content is constructed from a data source, and they utilize templates to customize and render their markup. Templated controls and databound controls are orthogonal; that is, you can have a templated, non-databound control, or databound, non-templated control. For example, the DropDownList, RadioButtonList, and CheckBoxList are all examples of databound, non-templated controls. In Building Templated Custom ASP.NET Server Controls, I created a non-databound templated control that displayed website statistics.
Note   RSS is a specification for syndicating content by an XML format. For more information on what RSS is and where it's being used, refer to an earlier article on mine, Creating an RSS News Aggregator with ASP.NET.

RssFeed: a Custom Server Control for Displaying RSS Syndication Feeds

To get a deep understanding of the process (and common pitfalls) of creating a databound templated control we'll be stepping through the creation of a custom server control I call RssFeed. RssFeed is a databound templated control that displays an RSS syndication feed in an HTML table. It is very easy to use; in its simplest form you just need to drop the control on an ASP.NET Web page and then, in the code-behind class, set its DataSource property to the URL of the RSS feed and then call the DataBind() method.

private void Page_Load(object sender, System.EventArgs e)
{
   if (!Page.IsPostBack)
   {      
      RssFeed1.DataSource = "http://dotavery.com/blog/Rss.aspx";
      RssFeed1.DataBind();
   }
}

Figure 1 shows the output of RssFeed when used in its simplest form. The look and feel of RssFeed can be improved through its numerous stylistic properties. Like the data Web controls, RssFeed has styles such as ItemStyle, AlternatingItemStyle, HeaderStyle, and so on. Figure 2 shows a more aesthetically pleasing view of RssFeed.

Aa479322.databoundtemplatedcontrols_fig01(en-us,MSDN.10).gif

Figure 1. Simple output from RssFeed

Aa479322.databoundtemplatedcontrols_fig02(en-us,MSDN.10).gif

Figure 2. RssFeed with style

RssFeed does not require that templates be used. Figures 1 and 2 show RssFeed's output when a template is not specified. RssFeed has two optional templates: ItemTemplate and HeaderTemplate. These templates can be used to optionally customize the appearance of the header and RSS items. Each RSS item has a number of properties: Title, Link, Description, PubDate, and others. The values of these properties can be emitted in a template using the following databinding syntax: <%# Container.DataItem.PropertyName %>. For example, the following RssFeed control uses a template to customize the display of the Title and PubDate properties:

<skm:RssFeed ...>
  <ItemTemplate>
    <strong><%# Container.DataItem.Title %></strong>
    <br>
    <i><%# Container.DataItem.PubDate.ToShortDateString() %></i>
  </ItemTemplate>
</skm:Rss>

The output generated by this templated version can be seen in Figure 3.

Aa479322.databoundtemplatedcontrols_fig03(en-us,MSDN.10).gif

Figure 3. RssFeed after applying a template

The RssFeed control also provides three events that can be used by page developers to programmatically tap into the control. These events' names and semantics are identical to three common events in the data Web controls:

  • ItemCreated. Fires as each RSS item is created.
  • ItemDataBound. Fires once for each RSS item after the item has been databound.
  • ItemCommand. Fires if a Command event was raised in the RssFeed's ItemTemplate template. This could happen if a page developer added a Button, LinkButton, or ImageButton Web control to the template that has its CommandName and/or CommandArgument set. When this button is clicked, the Web page is posted back and the RssFeed's ItemCommand event will fire.

In this article, then, you will see the steps you, as a control developer, need to take to create a templated databound control. There are a lot of meaty topics to cover! Let's start with a look at creating a non-templated databound control, and then segue into adding template support to the databound control.

Note   The complete source code for the RssFeed control can be downloaded using the link at the beginning of this article. There is also a GotDotNet Workspace with the latest version of the code, available at http://workspaces.gotdotnet.com/RssFeed. There's also extensive online documentation for page developers using RssFeed, and an article, A Custom ASP.NET Server Control for Displaying RSS Feeds, that discusses using RssFeed in an ASP.NET Web page.

Creating a Non-Templated DataBound Control

There are numerous built-in, non-templated, databound ASP.NET Web controls, such as the RadioButtonList, DropDownList, and CheckBoxList. Non-templated databound controls are useful when you want to have the control's output be based upon some data source but you do not need to provide the user with any degree of customization of the rendered HTML. In fact, RssFeed was initially created as a non-templated databound control. Its rendered HTML was fixed as a multi-column table (as was seen in Figures 1 and 2).

Databound controls are controls that provide a DataSource property that can be assigned to some set of data, along with a DataBind() method that, when invoked, binds the DataSource to the control. This binding process is typically accomplished by enumerating the data, adding some sort of "item" for each data record. For example, the DropDownList Web control's DataSource property can be set to the results of a database query. Calling the control's DataBind() method then iterates through the results of the DataSource, creating a ListItem for each record. When the control is rendered, each ListItem is rendered as an HTML <option> element in a <select> element.

So, one common aspect of databound controls is that they are a composition of "item" controls. The DataGrid, for example, is composed of a collection of DataGridItems. The DataList, of DataListItems. The RadioButtonList, CheckBoxList, and DropDownList are made up from a set of ListItems. The RssFeed control, as we'll see, will be composed of RssFeedItems.

In Building Templated Custom ASP.NET Server Controls, I discussed the differences between rendered controls and composite controls. Rendered controls are ones whose HTML markup is generated by manually generating the appropriate HTML markup. Composite controls are controls that contain a set of child controls, and these child controls are delegated the responsibility of generating the HTML markup. Since databound controls are composed of a number of "items," where each item is added to the databound control's control hierarchy, databound controls are composite controls.

It is important to have a firm understanding of composite controls, and the methods involved in working with composite controls. If you haven't already, please do take a moment to read Building Templated Custom ASP.NET Server Controls, especially the "Rendered Controls and Composite Controls" section.

Creating a databound control involves the following three steps:

  1. Create a DataSource property.
  2. Override the DataBind() method, building up the control hierarchy.
  3. Override the CreateChildControls() to build the control hierarchy.

Let's look at each of these tasks individually.

Creating the DataSource Property

When creating the DataSource property, it is important to decide what, precisely, constitutes the data that will be bound to the control. For the data Web controls, any data source that implements IEnumerable or IListSource can be bound to the control. Objects that implement IEnumerable include arrays, the objects in the System.Collections namespace, and DataReaders, among others. The DataSet implements IListSource. The data Web controls, then, accept a DataSource of type object, but in the property's set accessor, the assigned object is checked to make sure it is null, of type IEnumerable, or of type IListSource.

The RSS specification spells out an XML format for encoding syndicated data. Therefore, the data being processed by RssFeed will be an XML file. Typically, RSS is used to syndicate content online, from news websites or blogs. Therefore, the page developer should be able to specify a remote URL as the source for the data. Since the data might not be remote, but actually a local file, the DataSource property should also accept XmlReader objects, TextReader objects, or XmlDocument objects. To handle this, our DataSource property will be of type object, but when assigned to, will check to ensure that the assigned value is of the proper type.

object dataSource;

public virtual object DataSource
{
   get
   {
      return dataSource;
   }
   set
   {
      // make sure we're working with a string, XmlReader, or TextReader
      if (value == null || 
        value is string || 
        value is XmlReader || 
        value is TextReader || 
        value is XmlDocument)
         dataSource = value;
      else
         throw new ArgumentException("DataSource must be assigned a 
            string, XmlReader, or TextReader.");
   }
}

Notice that if the page developer attempts to assign an object to the DataSource that is not one of the supported types, an ArgumentException is thrown.

Overriding the DataBind() Method

After a page developer assigns some data to the DataSource, she'll next call the control's DataBind() method to bind the data to the control. The DataBind() needs to first call the OnDataBinding() method to raise the DataBinding event. This is an important step, as the DataBinding event will cause any databinding expressions the page developer has added into the control's templates to be evaluated.

Next, the control hierarchy needs to be cleared out and then rebuilt. The reason it needs to be cleared out is that the CreateChildControls() method may have already executed by this point, which would have already built the control hierarchy. After rebuilding the control hierarchy, the last step is to set the control's ChildControlsCreated property to True, so that future calls to EnsureChildControls() do not cause the hierarchy to be rebuilt yet again.

public override void DataBind()
{
   base.OnDataBinding(EventArgs.Empty);

   // Create the control hierarchy.  First, clear out the child controls
   Controls.Clear();
   ClearChildViewState();
   TrackViewState();

   // Create the control hierarchy
   CreateControlHierarchy(true);

   // Mark the hierarchy as having been created
   ChildControlsCreated = true;
}

The DataBind() method creates the control hierarchy by calling the CreateControlHierarchy() method. This method, which we'll examine in the next section, builds up the control hierarchy.

Creating the Control Hierarchy

The Control class contains a CreateChildControls() method whose responsibility is to create the control hierarchy. This method can be invoked in various places during the control's lifecycle through a call to the EnsureChildControls() method. EnsureChildControls() simply checks to see if the ChildControlsCreated property is False. If it is, the CreateChildControls() method is invoked. The CreateChildControls() method is guaranteed to be called, at the latest, during the control's PreRendering stage.

It is in the CreateChildControls() method, then, that you need to construct the control hierarchy. Rather than have all of this logic within this method, let's create a custom method—CreateControlHiearchy()—that performs this task. Therefore, our first attempt at the CreateChildControls() method would look like:

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

   // Build up the control hierachy
   CreateControlHierarchy();
}

Each time the ASP.NET Web page is visited, the RssFeed control needs to construct its control hierarchy. As we saw earlier, the ASP.NET Web page's Page_Load event handler calls the RssFeed control's DataBind() method on the first load. But imagine, for a moment, if this wasn't the case. RssFeed's CreateChildControls() method would still execute, and the CreateControlHiearchy() method would still build up the control hierarchy, even though you didn't necessarily want it to.

So, does that mean you don't need CreateChildControls() to call CreateControlHierarchy() at all? Can you just let the DataBind() method handle calling CreateControlHiearchy()? Imagine if this was the approach you took. When a Web page was first visited, and DataBind() was called, the control hierarchy would be created correctly. But what would happen on postback? Recall that the Page_Load event handler was written such that the DataBind() method was called only on the first visit to the page.

Note   Typically, the DataBind() method of databound controls is only called on the first page load, or when some event happens that requires the data be rebound to the control. An example of calling DataBind() again in response to some event would be when using a sortable DataGrid. If the user opts to have the data sorted in a different manner, the DataGrid's SortCommand event fires, and in the event handler, the page developer re-sorts the data and then rebinds it to the DataGrid.

CreateChildControls(), then, should only call CreateControlHierarchy() when the page is being posted back. In this scenario, CreateControlHierarchy() will need to reconstruct the items from the control's ViewState. If, however, CreateControlHierarchy() is called from the DataBind() method, it needs to construct its items from the DataSource. To determine whether or not CreateControlHierarchy() should construct the hierarchy from the DataSource, CreateControlHierarchy() will accept a Boolean value—True to create the hierarchy from the DataSource, False to create it from the ViewState.

Our final CreateChildControls() method is shown below. Note that it only called CreateControlHierarchy() on postback. (The RssItemCount ViewState variable is set after the control hierarchy has been created, and is persisted across postbacks. Therefore, on the first page visit prior to the control hierarchy having been created, the RssItemCount ViewState variable will be null, and therefore CreateControlHierarchy() won't be called.)

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

   // see if we need to build up the hierarchy
   if (ViewState["RssItemCount"] != null)
      CreateControlHierarchy(false);
}
Note   Recall from the earlier "Overriding the DataBind() Method" section, that the DataBind() method calls CreateControlHierarchy(), passing in a value of true, since when this method is invoked from DataBind(), the control's content should be built up from the DataSource.

Building the CreateControlHierarchy() method

The last, and most important, step in creating the control hierarchy is to write the CreateControlHierarchy() method, which does all the actual legwork of building up the control hierarchy.

Before examining the somewhat lengthy code, let's first discuss what this method does in English. The method starts by determining if the DataSource should be used to construct the control hierarchy, or if the hierarchy should be build up from the ViewState. If the DataSource is to be used, then a call to GetDataSource() is performed. GetDataSource() returns an ArrayList of RssItem objects.

An RssItem is an abstract representation of an RSS item. For example, a news site might use RSS to syndicate its latest news stories. Each story is considered an RSS item. According to the RSS specification, RSS items have properties like Title, Link, Description, Author, Category, PubDate (the date the item was published), and so on. Recall that the DataSource is XML data. Essentially, GetDataSource() iterates through the XML data and returns an ArrayList of RssItem instances. The specifics of GetDataSource() are not important; just realize that it parses the XML into an ArrayList. After binding the data from the DataSource, use a ViewState variable, RssItemCount, to hold the number of items that were bound to the control.

If the control's structure is to be reconstructed from the ViewState, then a dummy data source is created, namely an array of type object with RssItemCount number of element.

Recall from our earlier discussions in this article that databound controls are made up of "items," which are typically Web controls derived from Web controls that provide the desired rendering behavior. For example, DataGrid renders as an HTML <table> with each DataGrid item rendered as a row in the table. Not surprisingly, the DataGridItem class is derived from TableRow. The RssFeed control renders in a similar fashion to the DataGrid control—as an HTML <table>. The RssFeed control is composed of RssFeedItem controls, which, like the DataGridItem control, is a Web control derived from the TableRow class.

The RssFeed control contains a private ArrayList named rssItemsArrayList whose purpose is to contain references to the items in the RssFeed control. This private ArrayList is maintained because the RssFeed control has an Items property, which gives programmatic access to these RssFeedItem instances. In the CreateControlHierarchy() method, the rssItemsArrayList ArrayList is populated with the RssFeedItem instances added to the RssFeed control.

The following code shows the germane pieces of the CreateControlHierarchy() method. A few bits have been omitted for brevity.

protected virtual void CreateControlHierarchy(bool useDataSource)
{
   IEnumerable rssData = null;

   // Clear out and/or create the rssItemsArrayList
   if (rssItemsArrayList == null)
      rssItemsArrayList = new ArrayList();
   else
      rssItemsArrayList.Clear();
   

   // Get the rssData
   bool isValidXml = true;
   if (useDataSource)
   {
      // get the proper dataSource 
      //(based on if the DataSource is a URL, 
      // file path, XmlReader, etc.)
      rssData = GetDataSource();
   }
   else
   {
      // Create a dummy DataSource
      rssData = new object[(int) ViewState["RssItemCount"]];
      rssItemsArrayList.Capacity = (int) ViewState["RssItemCount"];
   }

   if (rssData != null)
   {
      // create a Table
      Table outerTable = new Table();
      Controls.Add(outerTable);

      // Add a header
      TableRow headerRow = new TableRow();
      TableCell headerCell = new TableCell();
      headerCell.Text = this.HeaderText;
         
      // Add the cell and row to the row/table
      headerRow.Cells.Add(headerCell);               
      outerTable.Rows.Add(headerRow);

      int itemCount = 0;
      foreach(RssItem item in rssData)
      {
         // Determine if this item is an Item or AlternatingItem
         RssFeedItemType itemType = RssFeedItemType.Item;
         if (itemCount % 2 == 1)
            itemType = RssFeedItemType.AlternatingItem;

         // Create the RssFeedItem
         RssFeedItem feedItem = CreateRssFeedItem(outerTable.Rows, 
           itemType, item, useDataSource);
         this.rssItemsArrayList.Add(feedItem);

         itemCount++;
      }

      // Instantiate the RssItems collection
      this.rssItemsCollection = new RssFeedItemCollection(rssItemsArrayList);

      // set the RssItemCount ViewState variable if needed
      if (useDataSource)
         ViewState["RssItemCount"] = itemCount;
   }
}

Note that the RssFeed control hierarchy contains a Table control, which then contains a single header row followed by a row for each item in the rssData ArrayList. Each row is created in a foreach loop. In each iteration of the loop, a new RssFeedItem is created by calling the CreateRssFeedItem() method. Let's take a moment to examine this method.

The purpose of CreateRssFeedItem() is to create a new RssFeedItem instance and add it to the Table's Rows collection. Then, if the control's structure is being created from the DataSource, the RssFeedItem's DataItem property needs to be assigned the current RssItem instance and the columns for the row need to be created and populated with the data from the current RssItem.

protected virtual RssFeedItem CreateRssFeedItem(TableRowCollection rows, 
  RssFeedItemType itemType, RssItem item, bool useDataSource)
{
   RssFeedItem feedItem = new RssFeedItem(itemType);
   RssFeedItemEventArgs e = new RssFeedItemEventArgs(feedItem);

   TableCell titleCell = new TableCell();
   TableCell pubDateCell = new TableCell();

   HyperLink lnkItem = new HyperLink();
   lnkItem.Target = this.Target;
   titleCell.Controls.Add(lnkItem);

   feedItem.Cells.Add(titleCell);
   if (ShowPubDate)
      feedItem.Cells.Add(pubDateCell);

   OnItemCreated(e);   // raise the ItemCreated event

   rows.Add(feedItem);

   if (useDataSource)
   {
      feedItem.DataItem = item;
   
      if (item.Link == String.Empty)
         titleCell.Text = item.Title;
      else
      {
         lnkItem.NavigateUrl = item.Link;
         lnkItem.Text = item.Title;
         titleCell.Controls.Add(lnkItem);
      }

      if (ShowPubDate)
         pubDateCell.Text = item.PubDate.ToString(this.DateFormatString);

      OnItemDataBound(e);      // raise the ItemDataBound event
   }

   return feedItem;
}

With the conclusion of these three steps—creating a DataSource property, overriding the DataBind() method, and building up the control hierarchy through CreateChildControls()—you have yourself a working databound Web control.

Styles in DataBound Controls

The DataGrid and DataList Web controls allow for page developers to easily tailor the appearance of the output through the use of styles. These include top-level styles that apply to the entire Web control, as well as more targeted styles, like ItemStyle, AlternatingItemStyle, HeaderStyle, FooterStyle, and so on. For RssFeed, there are three such targeted style properties: HeaderStyle, ItemStyle, and AlternatingItemStyle. Since RssFeed renders as an HTML table, with its header and items as rows in the table, these three styles are of type TableItemStyle.

private TableItemStyle headerStyle = null;
private TableItemStyle itemStyle = null;
private TableItemStyle alternatingItemStyle = null;

public virtual TableItemStyle HeaderStyle
{
   get
   {
      if (headerStyle == null)
         headerStyle = new TableItemStyle();

      return headerStyle;
   }
}

public virtual TableItemStyle ItemStyle
{
   get
   {
      if (itemStyle == null)
         itemStyle = new TableItemStyle();

      return itemStyle;
   }
}

public virtual TableItemStyle AlternatingItemStyle
{
   get
   {
      if (alternatingItemStyle == null)
         alternatingItemStyle = new TableItemStyle();

      return alternatingItemStyle;
   }
}
Note   Realize that styles are complex properties, and therefore require care to ensure that they are properly stored in the RssFeed control's ViewState. (Such code has been omitted in the above example for brevity, and because it only comprises part of the code needed to successfully maintain the style state over postbacks.)

Intuitively, you might think that these styles should be applied to the RssFeed control's header and items as they are being created in the CreateControlHierarchy() and CreateRssFeedItem() methods. However, doing so will persist these style settings in the children controls' ViewStates. Since the style is also being persisted in RssFeed's ViewState, having it stored in the child controls is wasteful, leading to unnecessarily bloated ViewStates (which impacts the size and response time of your ASP.NET Web page).

To prevent the styles from being stored in the child controls' ViewStates, you need to apply the styles after the ViewState has been saved. That means, you need to apply the styles in the Render phase. Therefore, you need to override the Render() method and, from there, apply the styles. To make the Render() method streamlined, let's create another method, PrepareControlHierarchyForRendering(), to apply the styles. The Render() method, then, calls PrepareControlHierarchyForRendering() prior to rendering its contents.

protected override void Render(HtmlTextWriter writer)
{
   // Parepare the control hiearchy for rendering
   PrepareControlHierarchyForRendering();

   // We call RenderContents instead of Render() 
   // so that the encasing tag (<span>)
   // is not included; rather, just a <table> is emitted...
   RenderContents(writer);
}

In the PrepareControlHierarchyForRendering() method, you need to programmatically reference the Table control in the control hierarchy, grab the header and apply its style, and then iterate through the remaining rows, applying the proper style (be it the ItemStyle or AlternatingItemStyle).

protected virtual void PrepareControlHierarchyForRendering()
{
   // Make sure we have a control to work with
   if (Controls.Count != 1)
      return;

   // Apply the table style
   Table outerTable = (Table) Controls[0];
   outerTable.CopyBaseAttributes(this);
   outerTable.ApplyStyle(ControlStyle);

   // apply the header formatting
   outerTable.Rows[0].ApplyStyle(this.HeaderStyle);

   // Apply styling for all items in table, if styles are specified...
   if (this.itemStyle == null && this.alternatingItemStyle == null)
      return;

   // First, get alternatingItemStyle setup...         
   TableItemStyle mergedAltItemStyle = null;
   if (this.alternatingItemStyle != null)
   {
      mergedAltItemStyle = new TableItemStyle();
      mergedAltItemStyle.CopyFrom(this.itemStyle);
      mergedAltItemStyle.CopyFrom(this.alternatingItemStyle);
   }
   else
      mergedAltItemStyle = itemStyle;

   bool isAltItem = false;
   for (int i = 1; i < outerTable.Rows.Count; i++)
   {
      if (isAltItem)                           
         outerTable.Rows[i].MergeStyle(mergedAltItemStyle);
      else
         outerTable.Rows[i].MergeStyle(ItemStyle);

      isAltItem = !isAltItem;
   }
}

The remainder of this article examines how to add template support, including how to use event bubbling to respond to events that occur inside a template.

Adding Templates to the DataBound Control

In Building Templated Custom ASP.NET Server Controls, I examined the steps necessary to add template support to a non-databound control. Recall that this involved the following three steps:

  1. Creating a private member variable of type ITemplate.
  2. Creating a public property of type ITemplate. It is through this property that the page developer will specify the HTML markup, Web controls, and databinding syntax for the template.
  3. Applying the template using the ITemplate's InstatiateIn() method.

These steps are the same for adding a template to a databound control.

Many databound controls contain more than one template. The DataList, for example, contains a HeaderTemplate, FooterTemplate, ItemTemplate, AlternatingItemTemplate, and so on. For each template a control needs to provide, a separate private member variable must be created, and a separate public ITemplate property must be provided. For RssFeed, let's use just two templates: HeaderTemplate, which can customize the markup of the header; and ItemTemplate, which specifies custom markup for each item (RssFeedItem) in the RssFeed control.

Start by defining the private member variables and public ITemplate properties for the HeaderTemplate and ItemTemplate:

private ITemplate _itemTemplate;
private ITemplate _headerTemplate;

public ITemplate ItemTemplate
{
   get
   {
      return _itemTemplate;
   }
   set
   {
      _itemTemplate = value;
   }
}

public ITemplate HeaderTemplate
{
   get
   {
      return _headerTemplate;
   }
   set
   {
      _headerTemplate = value;
   }
}

Next, you need to return to the CreateControlHierarchy() and CreateRssFeedItem() methods and, if needed, use the templates' InstiateIn() methods to render the header and item content. I say, if needed, because with RssFeed the template is optional. Without specifying the template, we'd like RssFeed to render its content in the standard multi-column table, as we saw in Figures 1 and 2.

To determine if a template has been specified, simply check to see if the applicable private member variable is null or not. If it is null, then a template has not been provided. The following snippet of code is from the CreateControlHierarchy() method, and creates the RssFeed header from the HeaderTemplate if the HeaderTemplate is supplied.

protected virtual void CreateControlHierarchy(bool useDataSource)
{
   ...
   
   if (rssData != null)
   {
      // create a Table
      Table outerTable = new Table();
      Controls.Add(outerTable);

      // Add a header, if needed
      TableRow headerRow = new TableRow();
      TableCell headerCell = new TableCell();

      // see if we should use the template or the default
      if (_headerTemplate != null)
      {
         _headerTemplate.InstantiateIn(headerCell);
      }
      else
      {
         // add a default header
         ...
      }
         
      // Add the cell and row to the row/table
      headerRow.Cells.Add(headerCell);               
      outerTable.Rows.Add(headerRow);

      ...
   }
}

Note that before creating the header, check to see if _headerTemplate is null. If it isn't, then that means the user has specified a HeaderTemplate, so instantiate the template in the headerCell. If a HeaderTemplate hasn't been provided, then the default header is added. (In the "Creating a Non-Templated DataBound Control" section, we examined the code for creating this default header.)

In a similar vein, the CreateRssFeedItem() method checks to see if _itemTemplate is null or not, and based on that comparison, it either instantiates the template in the created RssFeedItem instance, or it programmatically builds up the default RssFeedItem interface.

protected virtual RssFeedItem CreateRssFeedItem(TableRowCollection rows, 
  RssFeedItemType itemType, RssItem item, bool useDataSource)
{
   RssFeedItem feedItem = new RssFeedItem(itemType);
   RssFeedItemEventArgs e = new RssFeedItemEventArgs(feedItem);

   // see if there is an ItemTemplate
   if (_itemTemplate != null)
   {
      TableCell dummyCell = new TableCell();

      // instantiate in the ItemTemplate
      _itemTemplate.InstantiateIn(dummyCell);

      feedItem.Cells.Add(dummyCell);

      OnItemCreated(e);   // raise the ItemCreated event

      rows.Add(feedItem);

      if (useDataSource)
      {
         feedItem.DataItem = item;
         feedItem.DataBind();

         OnItemDataBound(e);      // raise the ItemDataBound event
      }
   }
   else
   {
      // manually create the item
      ...
   }

   return feedItem;
}

Notice that with the template, if you are building up the control from the DataSource, the current RssItem is assigned to the created RssFeedItem's DataItem property, and then the RssFeedItem's DataBind() method is called. This will cause any databinding syntax in the template to be resolved. And that's all there is to adding template support!

At this point you have enhanced your control so that it allows the page developer to customize the rendered HTML using templates. But what if the page developer wants to add, say, a Button Web control inside the template, and then respond programmatically to its click event? As you are likely aware, the DataGrid, DataList, and Repeater all have an ItemCommand event that is fired if a Command event is raised from within the bowels of its templates. Let's look at how to extend RssFeed to support the ItemCommand event as well.

Detecting and Bubbling Up Events

There are numerous actions that can cause a Web control to raise an event. If a Web surfer, for example, clicks a Button Web control causing a postback, upon postback that Button's Command event will fire. What should happen if one of the child controls in a composite control raises an event? The ASP.NET server control model uses event bubbling to percolate the event up through the control hierarchy until some control dictates the bubbling should stop.

Event bubbling is made possible through two methods: OnBubbleEvent() and RaiseBubbleEvent(). RaiseBubbleEvent(), as its name implies, bubbles an event to the control's parent. The OnBubbleEvent() method for a composite control will execute if one if its child controls bubbles up an event. To clarify things, let's look at a simple example. Let's say you have a composite control p, which has a child control c. Now, imagine that c fires an event that is bubbled up to its parent through a call to RaiseBubbleEvent(). When the event is bubbled up, p's OnBubbleEvent() method will execute.

The OnBubbleEvent() method returns a Boolean value indicating if the bubbling is to be cancelled. A value of True, then, halts the bubbling; a value of False automatically bubbles the event on up. The default implementation of OnBubbleEvent() simply returns False. You can override this method, though, to inspect a bubbling event and determine if you want to halt it and, perhaps, raise a different event.

This technique is used in the DataGrid, DataList, and Repeater to handle the Command event of Buttons, LinkButtons, and ImageButtons within the controls. Since the button's Command event calls RaiseBubbleEvent(), this percolates the event up to the button's parent. The classes that make up the items of the DataGrid, DataList, and Repeater—the DataGridItem, DataListItem, and RepeaterItem—override the OnBubbleEvent() to catch bubbling events. If a Command event has been bubbled up, these item classes stop the bubbling and build a suitable EventArgs instance. (DataGridItemCommandEventArgs for the DataGrid, DataListItemCommandEventArgs for the DataList, and RepeaterItemCommandEventArgs for the Repeater.) This is then bubbled up to the DataGrid, DataList, or Repeater. The DataGrid, DataList, and Repeater also override OnBubbleEvent(); upon getting an event percolated up, the bubbling is stopped and the data Web control's ItemCommand event is raised.

Armed with this understanding of how the data Web controls bubble events, let's look at the steps needed to add an ItemCommand event to the RssFeed control. First you need to override the RssFeedItem class's OnBubbleEvent() method. Here you need to listen for bubbling Command events. Upon getting such an event, package up the details into an RssFeedItemCommandEventArgs instance and then percolate the event to the RssFeed control through a call to RaiseBubbleEvent(). (RssFeedItemCommandEventArgs is a class created in the project derived from CommandEventArgs. It has the same properties as DataGridItemCommandEventArgs: Item, a reference to the RssFeedItem whose containing button tripped the event; CommandSource, a reference to the button that raised the event; and CommandName and CommandArgument, which have the values of the button's CommandName and CommandArgument properties.)

protected override bool OnBubbleEvent(object source, EventArgs args)
{
   // only bother bubbling appropriate events
   if (args is CommandEventArgs)
   {
      RssFeedItemCommandEventArgs e = new RssFeedItemCommandEventArgs(this, source, (CommandEventArgs) args);
      base.RaiseBubbleEvent(this, e);

      return true;
   }
   else
      return false;
}

As you can see, OnBubbleEvent() checks to see if the incoming EventArgs is of type CommandEventArgs. If it is, it creates a new RssFeedItemCommandEventArgs instance and bubbles it up to the RssFeedItem's parent. (Recall that the RssFeedItem's parent is actually a Table control. The Table control then bubbles up the event to its parent, the RssFeed control.) If the event is bubbled up, the method returns True, terminating the Command event's bubbling process. If it is some event other than a Command event, OnBubbleEvent() returns False, allowing the bubbling to proceed unhampered.

All that remains is to have the RssFeed control's OnBubbleEvent() method overridden. When an RssFeedItemCommandEventArgs instance is bubbled up, it's time to raise the ItemCommand event and terminate the bubbling; otherwise, let the bubbling continue unimpeded.

protected override bool OnBubbleEvent(object source, EventArgs args)
{
   // only bother bubbling appropriate events
   if (args is RssFeedItemCommandEventArgs)
   {
      OnItemCommand((RssFeedItemCommandEventArgs) args);
      return true;
   }
   else
      return false;
}

Now that the RssFeed control raises an ItemCommand event, you can add buttons to the ItemTemplate and programmatically respond to their clicking. A simple example illustrating this follows. Imagine you have your ItemTemplate configured to display a Visit button for each item. When the user clicks this button, the goal is to have them whisked to the URL of the RSS item (which is stored in the Link property). The following template would create a Visit button for each RssFeed item, passing along the Link property in the CommandName.

<skm:RssFeed ...>
  <ItemTemplate>
    <strong><%# Container.DataItem.Title %></strong>
    <br>
    <asp:Button runat="server" Text="Visit"
        CommandName='<%# Container.DataItem.Link %>'>
    </asp:Button>
  </ItemTemplate>
</skm:Rss>

Next you'd want to wire up the RssFeed's ItemCommand event to an event handler. The code for this event handler would be painfully straightforward—just a simple Response.Redirect() to the value of the CommandName of the clicked button.

private void blog_ItemCommand(object sender, skmRss.RssFeedItemCommandEventArgs e)
{
   Response.Redirect(e.CommandName);
}

Conclusion

In this article we saw how, first, to create a databound control, and then how to add template support to this databound control. Specifically, we dissected RssFeed, a custom ASP.NET server control designed to display information from an RSS syndication feed. When creating a databound templated control, I find it helpful to first create the control as a databound control, and then later to add template support. As discussed in the "Creating a Non-Templated DataBound Control" section, creating a databound control involves three steps:

  1. Create a DataSource property.
  2. Override the DataBind() method.
  3. Override the CreateChildControls() method and provide a method for creating the control hierarchy.

Adding template support to a databound control is not particularly difficult. Start by adding a private member variable and corresponding public property of type ITemplate for each template the control must support. Then, in the CreateChildControls() method, instantiate the template in the databound item (RssFeedItem, for this control).

Databound controls are a useful means for helping ASP.NET page developers display structured data in a Web-presentable format. Adding template support to the databound control grants page developers a high degree of freedom in crafting the control's resulting HTML markup.

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 through his blog, which can be found at http://ScottOnWriting.NET.

Show:
© 2014 Microsoft