Building a ContentRotator ASP.NET Server Control

 

Scott Mitchell
4GuysFromRolla.com

September 2005

Applies to:
   ASP.NET 1.1

Summary: This article examines the steps involved in creating a custom, compiled ASP.NET server control that randomly rotates through specified content, much like the built-in AdRotator control randomly rotates through a series of predefined banner advertisements. In examining the innards of the ContentRotator control this article touches upon several aspects of custom ASP.NET control development. (20 printed pages)

Download ContentRotator.msi.

Contents

Introduction
Thinking About Content Rotation
Specifying Content Items
   Specifying Content Through a Content File
   Specifying Content Items Declaratively
   Specifying Content Programmatically
Determining the Content Item to Display
   Reading Content Data from the Content File
   Reading Content Data Programmatically
   Reading Content from the Control's Declarative Syntax
   Picking a Random Content Item
Serving Dynamic Content
Customizing the Selected Content Item
Conclusion
Special Thanks to…
About the Author

Introduction

Back in the late 90s, anything seemed possible. The World Wide Web and its effect on business was growing astronomically—kids were dropping out of college to build Web sites and become millionaires overnight while companies were shelling out millions of dollars for prime-time television commercials to have a sock puppet extol the virtues of buying pet food online. Yes, this was the time of the New Economy, when attracting millions of Web surfers to a Web site was deemed more economically valuable than selling boring old products from a stuffy building.

At the height of these exuberant times, Microsoft introduced the AdRotator COM component for classic ASP, a tool that allowed budding Web entrepreneurs to easily add banner advertisements to their site. The first step in displaying a banner ad was to have an advertisements file listing the banners that could be displayed. This text file contained four bits of information about each banner:

  • A URL to the banner's image
  • The URL that the image links to
  • The alternate text for the image
  • The frequency by which the banner was to be shown relative to the other banners in the file

Armed with this text file, all an ASP developer had to do was call the AdRotator's GetAdvertisement(advertisementFile) method, passing in the path to the advertisements file, and the AdRotator would return the HTML markup for displaying a randomly selected banner advertisement from the specified file. At this point, I imagine, the money started rolling in.

When ASP.NET shipped in 2002, the New Economy was officially kaput. The NASDAQ composite, which had peaked around 5,000 in 2000, had slumped back down to below 2,000. Despite the dot-com meltdown, however, Microsoft must still have been bullish on online advertising, for they gussied up the AdRotator control from classic ASP and released it as one of the standard Web controls that shipped with ASP.NET. The ASP.NET version of the AdRotator offered several benefits over the classic ASP version:

  • The advertisements file was now XML-formatted, making it easier for page developers to create and edit the file's contents.
  • Each ad could contain a keyword indicating the category to which it belonged. This, in addition to the AdRotator's new KeywordFilter property, could be used to limit the set of banners displayed for a particular AdRotator control instance.
  • In addition to the standard set of advertisement properties, page developers could add their own additional settings in the advertisements file. These added settings could then be accessed programmatically by a page developer in the AdRotator's AdCreated event.

While the AdRotator makes it easy to randomly display a banner from a predefined list, it has a number of shortcomings, in my opinion, two of the main ones being:

  1. The AdRotator's content can only be specified through an XML-formatted advertisements file. This is not a major limitation, but it would be nice if one could specify content declaratively through the AdRotator's markup or programmatically through source code, in a similar fashion to how ListItems can be specified declaratively or programmatically for the DropDownList Web control.
  2. The AdRotator is rather limited in the markup it can generate. As its name implies, it is designed to display ads, which are either text or an image linked to some URL. With a bit more work, the AdRotator could have been created to display any type of content, not just banners or text ads.

It is this second shortcoming that particularly bugs me, as with just a bit more work the AdRotator could have been made into a very generic content rotator, as opposed to being designed to solely display advertisements. In this article we'll right this shortcoming and others by creating a full-featured ContentRotator server control.

Thinking About Content Rotation

Before diving into any coding project, it's important to take ample time to answer the following three questions.

  1. Is there an existing control that accomplishes what I need? If your days are anything like mine, with meetings, e-mails, and other such busy work taking up more hours of the day than I'd like to admit, writing code is oftentimes the most fun activity of the day. There's nothing more enjoyable than being genuinely excited about a problem and finding a solution to the problem through code.

    However, "fun" and "economically viable" are two different things. It may be enjoyable to create a particular control, but it probably doesn't make economic sense to spend one's time building, testing, and tweaking the control if there already exists one that offers the needed functionality.

    When I find myself gearing up to create a new server control, the first thing I do is head over to the ASP.NET Control Gallery to see if my efforts might be in vain. A quick inspection of the Control Gallery shows that there's an entire category dedicated to Content Rotators. Many of these content rotators, however, are the type that rotate through content on a single page, such as news or stock tickers.

    I found one control that rotates through arbitrary static HTML content like the AdRotator control—Duncan Mackenzie's ContentRotator User Control, presented in the article Rotating Is Fun. While Duncan's User Control offered the base functionality of the AdRotator, albeit with arbitrary HTML content, I decided not to use his solution as it does not provide quite the functionality I was looking for. (For example, Duncan's control does not allow for content to be tagged with a keyword.)

    In my searches I was unable to find a content rotator that met my requirements. Therefore, I decided to create my own. (Woo hoo! This meeting can wait, boss—I have a coding project to complete now!)

  2. What functionality does my control need to provide? If you decide that you need to create your own control, be certain not to rush into the fun part—coding; rather, have a clear picture on what, exactly, you need the control to do before writing your first line of code. One good way to determine the control's requirements is to devise common use cases that describe how end users (page developers, in this case) will be using your control. Among the first things I ask myself when building server controls are:

    • How will page developers use this control?
    • What will they need to have this control work as desired, and
    • How should the syntax look?

    After my initial brainstorming I came up with the following four use cases:

    Harry, an up-and-coming ASP.NET developer, wants to enhance his company's intranet so that the homepage randomly displays a short biography and picture of one of the company's employees. Since Harry's company has only a dozen employees, he's content with hard-coding the bio and picture markup in a file for now, but would like the ability to upgrade this later to retrieve the items from a database as the company grows. His aim is to have the ContentRotator randomly select one employee from that file and display his information on the homepage for each visit.

    Jisun is a developer at BuyPetFoodOnline.com, a new startup that has pegged its venture capital on the hopes that John Q. Public is interested in buying Alpo online. All the brands of pet food sold at BuyPetFoodOnline.com are maintained in a Microsoft SQL Server database. On the site's home page, Jisun wants to display either a list of the top 10 selling brands of pet food, a list of all types of dog food, or a list of all types of cat food. Furthermore, she wants the content for the top 10 selling brands to appear, on average, twice as often as the dog or cat food content combined.

    Todd runs a fitness Web site and has thousands of members registered on his site, with information such as their names, birth dates, weights, and other fitness-related data. At the bottom of each page, Jim would like to display a random fitness statistic tailored to the visitor's personal information. For example, he'd like to have messages like the following to appear: [[username]], in the [[numberOfDaysSinceBirth]] days you've been alive, your heart has beat over [[averageHeartRateTimesDaysAlive]] times," where each of the placeholders are filled with values specific to the logged-on user.

    Budding ASP.NET developer Darren is very new to XML and is worried he'll make a mistake when specifying content items in an XML-formatted content file. Darren is familiar with the DropDownList Web control and used to the DropDownList's declarative syntax for specifying ListItems. Darren would like to be able to specify content items for the ContentRotator in the same manner. Similarly, he would like to be able to programmatically manipulate the ContentRotator's content items with syntax similar to that required for working with the DropDownList's ListItems programmatically.

    The features of the ContentRotator flow from these use cases, which provide direction in the ContentRotator's implementation.

  3. Is there a possibility for code reuse? One of the main benefits of object-oriented programming is the ease with which existing functionality can be incorporated and extended. When creating a new server control it is very likely that there already exists an ASP.NET server control that provides similar functionality. Is it possible to merely extend this existing server control, rather than build one from the ground up? Building on top of an existing control will likely save countless hours of coding and testing.

    Before I began creating the code for the ContentRotator, I examined the possibility of having the ContentRotator extend the existing ASP.NET AdRotator control. The AdRotator class already contains the methods and properties needed for reading in items from an advertising file and randomly selecting an appropriate one. Might I be able to reuse this class and just override the specific methods that emit a banner or text ad, crafting them into methods that return more generic content?

    I considered this avenue, but decided against it for a couple of reasons. First, many of the AdRotator's methods are not marked virtual, meaning that they cannot be overridden. In particular, because the methods that parse the advertisements file are not virtual, my derived class would have to make use of the AdRotator's existing XML format. This would not necessarily limit the ContentRotator's functionality (as the AdRotator can have arbitrary XML elements added), but it would be a cosmetic downer, since it would use the advertisement file's <Advertisements> and <Ad> elements. Also, the AdRotator requires an <ImageUrl> element, which is an unacceptable requirement for a generic content rotator.

After proceeding through this thought process with prudence, I was ready to start writing the code—finally, the fun part! Throughout the remainder of this article I'll take you through some of the more interesting parts of the code for the ContentRotator control that will not only shed light on the inner workings of this particular control, but also provide an example for accomplishing similar functionality in the server controls that you create.

Specifying Content Items

The ContentRotator control provides three ways to specify content items:

  1. Through a separate content file in XML format.
  2. Through the ContentRotator's declarative syntax.
  3. Through server-side programmatic means.

The first option of using an external file provides better reuse of content items, since a single content file can be used by many content rotators on different pages in a single Web site. However, there may be times where you want to quickly put up a simple ContentRotator control without having to bother with creating a separate content file. In these cases, you can use the second option and provide the content items to iterate through in the control's declarative syntax. The final option allows you to programmatically specify the content items. This option is useful if the set of possible content items needs to be dynamically selected, or if they exist in a database or some other nonstatic store.

Specifying Content Through a Content File

When specifying content items in an XML-formatted content file, the content items must be presented according to a particular XML schema. Specifically, the content file must start with a <contents> element that contains a <content> element for each content item. Each <content> item has three optional attributes:

  • impressions—specifies the weight of the content item, which is used to determine the probability of the item being displayed.
  • keyword—specifies the content item's keyword. The ContentRotator control contains a KeywordFilter property that, if set, limits the content items to be considered for display to just those with a matching keyword parameter.
  • contentPath—content items can contain either static HTML markup or dynamic, code-driven content. If you want to make use of dynamic content you can specify the path to a User Control via this attribute. If the selected content item's contentPath attribute is set, the content is generated by the specified User Control.

The <content> element can also contain text that provides the static markup to display. This static markup is displayed if the contentPath attribute is not provided.

The following shows an example of a properly formatted content file with four content items. The first content item lacks any of the optional attributes, consisting only of the text content to display. The second content item has both the impressions and keyword attributes provided, while the third content item has just the keyword attribute set. Note that if you want to display HTML markup in the content item's text section you need to either XML-escape the markup as in the second example, by using &lt; and &gt; rather than < and >, or by wrapping the entire contents within a <![CDATA[...]]> section. The fourth and final content item refers to a User Control, RichContent.ascx, which is specified through the contentPath attribute. Additionally, the impressions attribute is set to 5.

<?xml version="1.0" encoding="utf-8" ?> 
<contents>
   <content>
      Things are just average... neither positive nor negative...
   </content>
   <content impressions="3" keyword="positiveComments">
      &lt;b&gt;You will soon see a workplace promotion.&lt;/b&gt;
   </content>
   <content keyword="positiveComments">
      <![CDATA[
      Happiness is <i>just around the corner!</i>
      ]]>
   </content>
   <content contentPath="~/RichContent.ascx" impressions="5" />
</contents>

The content file needs to be saved on the Web server's file system. To display content from a particular file, simply add a ContentRotator to an ASP.NET page and set its ContentFile property to the virtual path of the content file.

Note Keep in mind that XML is case-sensitive, so the casing of the XML elements is important. If you fail to use the proper casing—using <Content> instead of <content>, for example—the content items will not be retrieved from the content file, resulting in a ContentRotator control that does not emit any content items.

Specifying Content Items Declaratively

Many of the built-in ASP.NET Web controls allow most of their properties to be specified in the Web control's declarative syntax. For example, one can specify the particular DataGridColumns that should constitute a DataGrid through the <Columns> declarative syntax. The ContentRotator offers similar declarative syntax for specifying its content items. For each item the ContentRotator should consider for random display, add a <skm:ContentItem> element within the <skm:ContentRotator> tag. Each <skm:ContentItem> element can contain the following attributes:

  • Content
  • Impressions
  • Keyword
  • ContentPath

These attributes map to the text portion of the <content> element and the impressions, keyword, and contentPath attributes in the XML content file schema, respectively. (You can optionally specify the Content attribute as the inner tag's text content, as shown below in the first two <skm:ContentItem> instances.) The following shows the declarative syntax for a ContentRotator with the same four content items as used above:

<skm:ContentRotator id="ContentRotator1" runat="server">
   <skm:ContentItem>Things are just average... neither positive nor 
negative...</skm:ContentItem>
   <skm:ContentItem Impressions="3" Keyword="positiveComments"><b>You will soon see a workplace promotion 
</b></skm:ContentItem>
   <skm:ContentItem Content="Happiness is <i>just around the 
corner!</i>" Keyword="positiveComments"></skm:ContentItem>
   <skm:ContentItem ContentPath="~/RichContent.ascx" Impressions="5"></skm:ContentItem>
</skm:ContentRotator>

Note that with the declarative syntax you do not need to escape HTML characters in the Content attribute.

Specifying Content Programmatically

To aid with the concept of content and content items, the ContentRotator includes two classes:

  • ContentItem—abstractly represents a content item with properties like Content, ContentPath, Keyword, Impressions, and so on.
  • ContentItemCollection—a strongly typed collection of ContentItem instances.

The ContentRotator control contains an Items property that is of type ContentItemCollection. You can programmatically specify the content items that the ContentRotator should consider when randomly selecting an item by adding ContentItem instances to the Items property. As with other ASP.NET server controls, content added to the Items property is stored in the control's view state, so you need to add these items only on the first page visit and not on subsequent postbacks. The following code snippet shows how to programmatically add the same set of content items used in the earlier two examples:

  
    pr
  ivate void Page_Load(object sender, System.EventArgs e)
{
   if (!Page.IsPostBack)
   {
      // only need to load content items on first page visit – 
they are persisted across
      // postbacks in the ViewState...

      ContentRotator1.Items.Add(new ContentItem("Things are just 
average... neither positive nor negative..."));

      ContentRotator1.Items.Add(new ContentItem("<b> You will 
soon see a workplace promotion </b>", string.Empty, "positiveComments", 3));

      ContentItem thirdCI = new ContentItem();
      thirdCI.Content = "Happiness is <i>just around the 
corner!</i>";
      thirdCI.Keyword = "positiveComments";
      ContentRotator1.Items.Add(thirdCI);
      
      // Add dynamic content
      ContentRotator1.Items.Add(new ContentItem(string.Empty, 
"~/RichContent1.ascx", string.Empty, 5));
   }
}

As you can see, the ContentItem class has a number of constructor overloads that can reduce creating a new ContentItem instance and setting its properties down to just a single line of code.

If you'd rather not read about the code details and would instead prefer to start using the ContentRotator in your ASP.NET application, feel free to skip the rest of the article and download the control from the link at the top of this article. The download includes the full source code for the ContentRotator control (in C#) along with a sample ASP.NET Web application (also in C#) that shows solutions for the use cases discussed above. Additionally, the download includes a spiffy-looking compiled help file to assist page developers who decide to use the ContentRotator control.

Determining the Content Item to Display

Each time a page with a ContentRotator control is visited, the ContentRotator control must decide what content item to randomly display. Each content item has an associated impressions value that influences how likely it is to be selected relative to the other content items. This impressions parameter is a positive integer value and, if not specified, defaults to a value of 1. Additionally, each content item can have an optional keyword parameter. The ContentRotator control's KeywordFilter property, if specified, restricts the set of content items that are considered for display to those content items with a matching keyword value.

The algorithm used to randomly choose a content item works by laying out each applicable content item end-to-end, forming a line. The length of each content item is its impressions value, meaning that the total length of the line is the sum of the applicable content items' impressions. Next, a random number less than the total length is chosen, and the content item to display is the one that lies at the location of the random number. Figure 1 illustrates this algorithm graphically.

Click here for larger image

Figure 1. (Click to enlarge)

In order to apply this algorithm the ContentRotator control first needs to retrieve the list of content items for consideration. Recall that this list may reside on disk as an XML file, be specified in the ContentRotator's declarative syntax, or be provided programmatically. Let's examine how this list of content items can be accessed for each of these three techniques.

Reading Content Data from the Content File

The ContentRotator has an Items property of type ContentItemCollection that contains the set of applicable content items considered by the ContentRotator control. (Recall that the set of applicable content items depends on whether the control's KeywordFilter property is set and, if it is, the keyword parameters of the content items.) This Items property is populated in the Load event by a call to the GetFileData(virtualFilePath) method. This method returns a ContentItemCollection instance that contains all the content items in the content file specified by the virtualFilePath parameter.

Opening, reading, and parsing the entire content file on each and every page visit would be inefficient and unnecessary, especially considering that the file is likely to be changed infrequently. To improve performance, the items in the content file are cached using a file dependency. This means that the items in the content file will reside in the cache for improved performance, but the cache item will be invalidated automatically when the underlying content file is modified. The following code from the GetFileData(filePath) method illustrates this caching behavior:

  
    // 
  See if the item exists in the cache
string cacheKey = string.Concat("ContentRotateCacheKey:", 
physicalFilePath);

ContentItemCollection cachedContent = (ContentItemCollection) 
HttpContext.Current.Cache[cacheKey];
if (cachedContent == null)
{
   // it's *not* in the cache, must manually get the file data and cache it
   cachedContent = LoadFile(physicalFilePath);
   if (cachedContent == null)
      return null;
   else
      // Add the content to the cache
      HttpContext.Current.Cache.Insert(cacheKey, cachedContent, 
new CacheDependency(physicalFilePath));
}

// return the cached content
return cachedContent;

The variable physicalFilePath contains the physical path to the content file, and is used in forming the cache key. This ensures that each unique content file will have its own cache entry. Next, the Cache object is accessed, retrieving the value of the cache item named cacheKey. If this item is null—either because no such item has been inserted into the cache or the cache item has become invalidated—the contents from the content file are loaded and inserted into the cache along with a cache dependency based on the content file.

The LoadFile(physicalFilePath) method iterates through the content file using an XPathNodeIterator, stripping out the various attributes and text content and building up a ContentItem instance for each item in the content file. Each ContentItem instance is added to a ContentItemCollection, which is returned at the conclusion of the method. The following code shows the iteration through each item in the content file; fStream is an opened file stream to the content file.

// Use an XPathNavigator to iterate through the XML elements in the ContentFile
reader = new XmlTextReader(fStream);
XPathDocument xpDoc = new XPathDocument(reader);
XPathNavigator xpNav = xpDoc.CreateNavigator();

XPathNodeIterator xmlItems = xpNav.Select("/contents/content");

XPathExpression contentExpr = xpNav.Compile("string(text())");
XPathExpression contentPathExpr = xpNav.Compile("string(@contentPath)");
XPathExpression keywordExpr = xpNav.Compile("string(@keyword)");
XPathExpression impressionsExpr = xpNav.Compile("string(@impressions)");

if (xmlItems == null)
   throw new FormatException("ContentFile in invalid format.");
else
{
   while (xmlItems.MoveNext())
   {
      string content = (string) xmlItems.Current.Evaluate(contentExpr);
      string contentPath = (string) xmlItems.Current.Evaluate(contentPathExpr);
      string keyword = (string) xmlItems.Current.Evaluate(keywordExpr);
      string impressionsStr = (string) xmlItems.Current.Evaluate(impressionsExpr);

      int impressions = 1;      // default impressions value is 1
      if (impressionsStr != null && impressionsStr.Length > 0)
         impressions = Convert.ToInt32(impressionsStr, 
CultureInfo.InvariantCulture);

      contentItems.Add(new ContentItem(content.Trim(), 
contentPath, keyword, impressions));
   }
}

An XPathNodeIterator steps through each <content> element, applying an XPathExpression to pick out each attribute and the text content. As each <content> item is iterated, a ContentItem instance is created and its properties assigned the values from the XPathExpressions. Each ContentItem instance is added to a ContentItemCollection instance, which is later returned from the LoadFile(physicalFilePath) method.

Reading Content Data Programmatically

In order to programmatically add items to any type of Web control you need to perform the following three steps:

  1. Create a class that represents the items to be added.
  2. Create a class that represents a collection of the items.
  3. Add a property to the Web control of the type of class defined in step 2. This property holds the set of items for the control.

To see these steps in action, consider the built-in ASP.NET DropDownList Web control, which consists of a set of items that constitute the DropDownList's available selections. Each item is represented by an instance of the ListItem class (step 1). The ListItemCollection class provides a strongly typed collection of ListItem instances (step 2), and the DropDownList class has an Items property of type ListItemCollection (step 3). From an ASP.NET page's source code portion, a DropDownList can have ListItem instances programmatically added to it using syntax like:

  
    DropDownList1.Items.Add(new ListItem(text, value));

  

In order to accomplish similar functionality with the ContentRotator I created the ContentItem class to represent a particular content item. As discussed earlier, this class has properties specific to a content item, such as Content, Impressions, and so on. Next, I created a ContentItemCollection class that provides a strongly typed collection of ContentItem instances. Lastly, I created an Items property of type ContentitemCollection in the ContentRotator class.

With these classes and properties created, a page developer can programmatically add content to the ContentRotator in syntax not unlike that of the DropDownList:

ContentRotator1.Items.Add(new ContentItem(content));
ContentRotator1.Items.Add(new ContentItem(content, contentPath, 
keyword, impressions));
...

Reading Content from the Control's Declarative Syntax

In addition to being able to specify items programmatically, a number of Web controls also enable the page developer to specify the set of items through the control's declarative syntax. For example, with the DropDownList the ListItems can be spelled out declaratively like so:

<asp:DropDownList runat="server" ...>
  <asp:ListItem Value="value1">text1</asp:ListItem>
  <asp:ListItem Value="value2" Text="text2"></asp:ListItem>
  ...
  <asp:ListItem Value="valueN">textN</asp:ListItem>
</asp:DropDownList>

Adding this functionality to the ContentRotator is fairly simple and straightforward since we already have a ContentItem class defined as well as an Items property for the ContentRotator control. All we need to do is use two attributes to indicate that the items specified in the declarative syntax map to the ContentRotator's Items property.

First, at the class level, add the ParseChildren() attribute, indicating that the child markup should be parsed and that it maps to the Items property:

[ParseChildren(true, "Items")]
public class ContentRotator : Control
{
   ...
}

And last, add the PersistenceMode() attribute to the Items property declaration, indicating that the Items property is to be persisted as the inner, default property.

[PersistenceMode(PersistenceMode.InnerDefaultProperty]
public ContentItemCollection Items
{
   get { ... }
}

That's all there is to it! With these two attributes, a page developer can specify content items in the control's declarative syntax like so:

<skm:ContentRotator runat="server" ...>
  <skm:ContentItem Content="content" ContentPath="contentPath" 
Keyword="keyword" Impressions="impressions"></skm:ContentItem>
  ...
</skm:ContentRotator>

One thing to note is that with our current code all properties of the ContentItem instance must be specified as attributes of the <skm:ContentItem> element. Ideally, a page developer would be able to specify static content through either the Content attribute or as the text content that appears between the <skm:ContentItem> and </skm:ContentItem> tags. To accomplish that we need to add a small bit of code to the ContentItem class indicating how the class should parse inner text content.

Specifically, the ContentItem class needs to implement the IParserAccessor interface, which defines a single method, AddParsedSubObject(object). The AddParsedSubObject(object) method passes in the content of the <skm:ContentItem> element in the declarative syntax. If the inner content is plain text, a LiteralControl instance will be passed in. In this case, we want to assign the LiteralControl's Text property to the Content property of the ContentItem instance. This is accomplished with the following code:

public class ContentItem : IParserAccessor
{
   ...

   public void AddParsedSubObject(object obj)
   {
      if (obj is LiteralControl)
         Content = ((LiteralControl) obj).Text;
      else
         throw new HttpException(...);
   }
   #endregion
}

Picking a Random Content Item

After the ContentRotator's Items property has been retrieved in the Load event, the SelectContentFromItems() method is called. This method returns a randomly selected ContentItem instance from the Items collection. If the ContentRotator's KeywordFilter property is set, it removes the nonapplicable items from the collection. It then determines the sum of the impressions parameters for the remaining items and chooses a random number between 0 and the total impressions value minus one. Based on this random number, the appropriate content item is selected and returned.

The following code shows how, if the KeywordFilter property is set, the nonapplicable items are removed from consideration and how the sum of impressions is computed. Following this, a random impressions is selected and the appropriate ContentItem instance is returned.

// Determine the sum of the Impressions
int totalWeight = 0, i = 0;
string controlsKeywordFilter = this.KeywordFilter;
ContentItemCollection filteredArray = new ContentItemCollection();

for (i = 0; i < Items.Count; i++)
   // only add the content item to the list of filtered content 
// items if the KeywordFilter property hasn't been set or,
   // if it has, if the KeywordFilter matches the content item's
// keyword attribute.
   if (controlsKeywordFilter.Length == 0 || 
      CultureInfo.InvariantCulture.CompareInfo.Compare(Items[i].Keyword, controlsKeywordFilter, 
CompareOptions.IgnoreCase) == 0)
   {
      totalWeight += Items[i].Impressions;   // increment the totalWeight
      filteredArray.Add(Items[i]);
   }
      

// Randomly choose a number between 0 and totalWeight - 1
int randomWeight = random.Next(totalWeight);

totalWeight = 0;

// Now grab the appropriate ContentItem based on randomWeight
i = 0; 
while (i < filteredArray.Count && (totalWeight + 
filteredArray[i].Impressions) <= randomWeight)
   totalWeight += filteredArray[i++].Impressions;

return filteredArray[i];

Serving Dynamic Content

Determining which content item to display is only half the battle. We also need to be able to actually display the content item selected. This is accomplished in the Load event after the SelectContentFromItems() method has specified what content item to display. If the item contains static content—that is, if it does not have its ContentPath property set—then a new LiteralControl instance is created and added to the ContentRotator's control hierarchy with its Text property set to the value of the static content to display.

If, however, the content to display is dynamic—that is, its ContentPath property is set, specifying the path of the User Control that will provide the dynamic content—then the Page's LoadControl(path) method is called to load the specified User Control's control hierarchy. The root of the control hierarchy is returned by the LoadControl() method, and this control is added to the ContentRotator's control hierarchy. Figure 2 illustrates this process graphically.

Click here for larger image

Figure 2. (Click to enlarge)

By default, the ContentRotator will randomly select a content item to display on every page visit, including postbacks to the same page. If you want the same randomly retrieved content item displayed across postbacks, simply set the ContentRotator's NewContentOnPostbacks to false. If you are using dynamic content that requires postbacks—such as a User Control that prompts the user for some input and, on postback, displays detailed information about that input—then you will need to set NewContentOnPostbacks to false. If you leave NewContentOnPostbacks with its default value of true, the ContentRotator might select a different content item when the user posts back from the randomly selected User Control, thereby losing the postback data.

In fact, regardless of whether static or dynamic content is loaded, the ContentRotator's control hierarchy has its view state disabled if the NewContentOnPostbacks property is set to true. This is because on postback the ContentRotator might choose a different content item to display from the page's previous visit. If the ContentRotator did not disable view state in this case, an exception would be thrown during the load view state stage if a different content item had been randomly selected. If, however, the NewContentOnPostbacks property is set to false, the view state for the ContentRotator's control hierarchy is maintained.

Note For more information on loading User Controls programmatically with the LoadControl() method, be sure to read my earlier article, An Extensive Examination of User Controls. Information regarding view state and related issues can be found in Understanding ASP.NET View State.

Customizing the Selected Content Item

One of the feature requirements of the ContentRotator was to allow a page developer to specify dynamic placeholders in a content item. For example, a page developer should be able to create a content item with the content text "The current time is [[time]]," and have the [[time]] placeholder dynamically replaced by the current system time. I deduced that there were two approaches I could take to solving this problem:

  1. Provide a predefined set of placeholders that could be used for dynamic replacement.
  2. Allow the page developer to decide what placeholders to use and what values to replace them with.

The first option would be the easier of the two to implement: I could just have a check in the Load event of the ContentRotator control that methodically replaced all predefined placeholders in the selected content item with their corresponding dynamic values. While this approach would be easy to implement, it would not afford the page developer nearly the level of customizability I thought he deserved.

Therefore, I placed the onus of defining placeholders and injecting dynamic values squarely on the shoulders of the page developer. A page developer can make up whatever placeholders he'd like to, adding them into the text content of the content items in the content file or through the declarative syntax. All I needed to do was provide a mechanism for the page developer to tap into the selected content item. I did this by having the ContentRotator expose a ContentCreated event. This event is fired on each page visit whenever the content item to display is selected.

A page developer can create an event handler for this event. In the event handler, the page developer will receive the ContentItem instance that was selected for display. At this point, the text of the content item can be searched for placeholders to be replaced with their dynamic values. The following example illustrates this behavior. The ContentRotator has three content items defined via the declarative syntax, two with dynamic placeholders.

<skm:ContentRotator id="ContentRotator1" runat="server">
   <skm:ContentItem Content="The current time is 
[[time]]."></skm:ContentItem>
   <skm:ContentItem Content="Hello, World! Nothing dynamic 
here!"></skm:ContentItem>
   <skm:ContentItem Content="Welcome to page 
[[url]]."></skm:ContentItem>
</skm:ContentRotator>

In the code-behind class of the ASP.NET page an event handler is created and wired up to the ContentRotator's ContentCreated event. The second parameter to this event handler is of type ContentCreatedEventArgs, which contains the selected ContentItem instance in its ContentItem property. As the following code shows, the event handler replaces any instances of [[time]] with the current system time and any instances of [[url]] with the current page's URL.

private void ContentRotator1_ContentCreated(object sender, 
skmContentRotator.ContentCreatedEventArgs e)
{
   // Replace [[time]] with DateTime.Now.TimeOfDay
   e.ContentItem.Content = e.ContentItem.Content.Replace("[[time]]", 
DateTime.Now.TimeOfDay.ToString());

   // Replace [[url]] with Request.RawUrl
   e.ContentItem.Content = e.ContentItem.Content.Replace("[[url]]", 
Request.RawUrl);
}

Note The placeholders—[[time]] and [[url]] in this example—are at the page developer's discretion. That is, the page developer can use whatever names and markup he chooses as placeholders. Rather than [[time]], for example, we could have used *currentTime* as the placeholder, replacing all instances of [[time]] with *currentTime* in the content file and event handler.

Conclusion

With the dot-com boom a fading memory of the past, controls like Microsoft's Ad Rotator are scurrying away into obscurity. However, the concept of the Ad Rotator is a good one, and there are many scenarios in which a generic content rotation control would prove invaluable. The ContentRotator control presented in this article should fit the bill for most of these situations. The ContentRotator provides similar semantics and syntax to that of the Ad Rotator control, including an impressions value for content items, keyword filtering, and an event that allows page developers to tap into the selected content item. In addition, the ContentRotator allows for its content items to also be specified declaratively and allows for both static and dynamic content.

Happy Programming!

Special Thanks to. . .

Before submitting my article to my MSDN editor I have a handful of volunteers help proofread the article and provide feedback on the article's content, grammar, and direction. Primary contributors to the review process for this article include Julius Estrada, Hilton Giesenow, Milan Negovan, Carl Lambrecht, and Jeffrey Palermo. If you are interested in joining the ever-growing list of reviewers, drop me a line at mitchell@4guysfromrolla.com.

 

About the Author

Scott Mitchell, author of six books on Web development and founder of 4GuysFromRolla.com, has been working with Microsoft Web technologies for the past seven years. Scott works as an independent consultant, trainer, and writer. You can reach him at mitchell@4guysfromrolla.com or via his blog, http://ScottOnWriting.NET.

© Microsoft Corporation. All rights reserved.