Rotating Is Fun

Duncan Mackenzie
Microsoft Developer Network

August 25, 2004

Applies to:
   Microsoft® Visual Basic® .NET
   Microsoft Visual C# .NET

Summary: Duncan Mackenzie describes the creation of a simple "content rotator" in ASP.NET. (8 printed pages)

Download the source code for this article.

Introduction

If you are into ASP.NET coding, then you probably know about the Ad Rotator control (which was available in pre-.NET ASP as well), so I will keep the background explanation brief. The Ad Rotator is a server control that lets you rotate a set of advertisements (images) on your site, so that each request for the page has the possibility of showing a different ad. Each ad has some text associated with it (for the alt-text of the image) and a hyperlink, and all of that information about all of the ads is stored into an XML file that is then referenced from the control. Each ad item also has an impressions value associated with it. This impressions value controls how much air time that particular ad should receive relative to the others.

This control, although it is quite simple, is amazingly useful; Web site designers just love being able to rotate through a list of ads without manually editing any HTML. Personally, though, whenever I looked at this control, I always thought it was a shame that you could only include images. What if I wanted to rotate through a variety of content including images, text, and even complicated html that included formatting, images, and more? A more general solution would be to rotate HTML instead of images, and images could still be used by just putting a <img> tag in as your content.

Well, despite having this thought fairly often, I never implemented a solution. It must not have been a unique idea, as there are several commercial "content rotators" available on the market. Recently though, I decided that I had the need for this control on the areas of MSDN that I manage (the Microsoft Visual Basic and Microsoft Visual C# Developer Centers). On those sites, I could use this type of control to rotate through featured community sites, books, blog entries, or anything else where I have a list of things I want to feature but only have the room to display one entry at a time.

So I set out to build myself a new control, and I decided to try my hand at creating an .ascx-style control instead of the .dll approach I used in my previous ASP.NET-focused article. Since I was going for simplicity, I also decided to download the Web Matrix IDE to build my control in, making it easy for me to create a control with all inline code.

The Design

The basic design can be described using only a few lines; this was not a rigorous development process.

The control will reference a list containing HTML content and impression information. The control will randomly pick an item from the list, with the probability of an individual item being selected based on its "impressions" value in the content list, and output it for display. The display information does not need to rotate upon every refresh, and there is no problem with selecting the same item twice or more times in a row, as long as the overall frequency of appearance corresponds to the "impressions" information.

The Content List

The first order of business was to determine where and how to store the content information, but the simplest course of action was to just follow the Ad Rotator's lead and use an XML file.

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Content>
    <Item>
        <html><![CDATA[<b>Value1</b>]]></html>
        <Impressions>20</Impressions>
    </Item>
    <Item>
        <html><![CDATA[<b>Value2</b>]]></html>
        <Impressions>20</Impressions>
    </Item>
    <Item>
        <html><![CDATA[<b>Value3</b>]]></html>
        <Impressions>50</Impressions>
    </Item>
</Content>

While it is not an exact copy of the Ad Rotator file, it is quite similar. I changed some element names, removed the keyword element, and wrapped my <html> element in a CDATA block because it will be used to hold HTML.

The Control

I already mentioned that I planned to create this control as an .ascx control, and to use inline code, but there is still the question of the control's public appearance. What properties will this control expose? I thought more properties might get added as I developed the control, but I managed to start and finish with only a single property: ContentFile. The ContentFile property is used to indicate the file name for your content, and can be supplied to the control using a relative or absolute path.

You might also be curious as to why I would choose to build an .ascx instead of a .dll, or why I decided to write my code inline, right in the .ascx file. There are a few different reasons:

  • The simplicity of editing the file directly, even through Microsoft FrontPage or Notepad, was appealing.
  • Several other controls written for use on MSDN are written as .ascx files, and I like to be consistent with the rest of the team.
  • And finally... I decided to build the control this way because I hadn't done it before and I wanted to try it out.

I created both C# and Visual Basic versions of the control, both of which are included in the download.

The Algorithm

To weight each content item based on its impressions value, I randomly selected an integer between zero and the total of all the impressions values. Then, I scanned through the items from the first to the last entry, keeping a running total of the impressions. Whenever my running total is greater than or equal to the random number, I stop and display the contents of the current entry.

Graphic showing how I use the impressions value for each item to determine how likely they are to be shown

Figure 1. Random item selection

This algorithm does not guarantee that each item will be shown in exactly the right proportions, but over a large number of rotations it should be close enough.

The Implementation

As I mentioned earlier, my control has only one public property, ContentFile, which is how the developer will configure the location of the content XML file. The implementation of this property is simple, but it calls out to another routine (LoadContentFile) to load the XML file from disk into an XML Document object.

    Private m_ContentFile As String
    Public Property ContentFile As String
        Get
            Return m_ContentFile
        End Get
        Set(ByVal Value As String)
            If m_ContentFile <> Value Then
                m_ContentFile = Value
                LoadContentFile
            End If
        End Set
    End Property

LoadContentFile in turn calls out to two other routines, GetXMLDoc() that abstracts the loading of the actual file to allow for caching, and GetSumOfImpressions() that takes the XmlDocument and obtains the total number of impressions across all of the content items.

    Private Sub LoadContentFile()
        m_Content = GetXMLDoc
        m_TotalImpressions = GetSumOfImpressions(m_Content)
    End Sub

GetXMLDoc uses the file name of the content file as a key into the Microsoft ASP.NET Cache object and checks first to see if an XmlDocument instance is already in the cache. If the XmlDocument isn't in the cache, then a new instance is created, the file is loaded in, and the new XmlDocument object is cached with a file dependency set to the file name. The use of a file dependency is particularly useful in this case, as the cached XmlDocument is valid unless the content XML file is modified, and that is exactly what a file dependency does; it invalidates the cached data whenever the monitored file is changed.

    Private Function GetXMLDoc() as System.Xml.XmlDocument
        Dim fileName As String _
= Me.Page.MapPath(m_ContentFile)
        Dim doc As System.Xml.XmlDocument
        Dim obj As Object

        obj = Me.Cache.Get(fileName)
        If obj Is Nothing Then
            Dim fd As New System.Web.Caching.CacheDependency(fileName)
            doc = New System.Xml.XmlDocument()
            doc.Load(fileName)
            Me.Cache.Insert(fileName, doc, fd)
        Else
            doc = DirectCast(obj,System.Xml.XmlDocument)
        end if

        Return doc
    End Function

Adding up the total number of impressions could be handled by a loop, but since it is stored in an XML document, we do have some nicer features available, such as XPath. Using the XPath sum() function, it is possible to obtain the total number of impressions with only a few lines of code. This not only simplifies the code, but it also avoids a wasteful walk through all of the items.

    Private Function GetSumOfImpressions( _
          xmlDoc as System.Xml.XmlDocument) As Integer
          Dim nav as System.Xml.XPath.XPathNavigator
          Dim total as Integer
          Dim expr as System.Xml.XPath.XPathExpression

          nav = xmlDoc.CreateNavigator()
          expr = nav.Compile("sum(//Item/Impressions)")
          total = CType(nav.Evaluate(expr), Integer)
          Return total
    End Function

All of the code above sets you up for producing actual output, but the only code in this control that actually writes something out is in the Render() method.

    Protected Overrides Sub Render(writer As HtmlTextWriter)
        Dim selectedItem as Integer
        Dim itemCount as Integer = m_TotalImpressions
        Dim rnd as new System.Random()
        Dim found As System.Xml.XmlNode
        If Not m_Content Is Nothing Then
            Dim elemList As System.Xml.XmlNodeList _
                = m_Content.GetElementsByTagName("Item")
            Dim runningTotal As Integer = 0
    
            selectedItem = rnd.Next(0,itemCount)

            For Each nd as System.Xml.XmlNode in elemList
               runningTotal += Cint(nd("Impressions").InnerText)
               If runningTotal >= selectedItem Then
                  found = nd
                  Exit For
               End If
            Next nd
        End If

        If Not found Is Nothing Then
            writer.WriteLine()
            writer.WriteLine("<!-- Begin Rotated Content VB-->")
            writer.WriteLine(found("html").InnerText)
            writer.WriteLine( _
             String.Format("<!-- Rendered at {0} -->",Now))
            writer.WriteLine("<!-- End Rotated Content -->")
        End If
    End Sub

I added the comment lines around the core output so that I could check the html source of the generated page (in Microsoft Internet Explorer) to find out details about the control's rendering components.

<!-- Begin Rotated Content C# -->
<b>Value3</b>
<!-- Rendered at 7/13/2004 5:06:56 AM -->
<!-- End Rotated Content -->

Knowing the date/time at which the output was rendered will be quite useful to see if caching is enabled and functioning.

Caching Strategy

Although I cache the XML file as an instance of XmlDocument, I still have to determine which item to display on every single request for this object, which is probably unnecessary. To reduce how often the content is rotated, I decided to enable output caching on the page.

<%@ outputcache duration="60" 
         shared="true" varybyparam="ContentFile" %>

These settings tell the ASP.NET engine to cache the output from this control and to only ask it to redraw itself every 60 seconds. With these settings, the content will change (and the code will run) a maximum of once per minute, and I don't need to add any additional caching code to my routines. The combination of varybyparam="ContentFile" and shared="true" means that, if the same ContentFile is specified, this control will be cached once for all pages that it is used on. That may not be what you want, depending on what you are rotating, but if this control is used on many different pages, it will certainly save on server resources.

For more information on output caching in ASP.NET, check out these links:

  • @ OutputCache
  • Caching Portions of an ASP.NET Page
  • Caching Multiple Versions of User Control Output

Conclusion

Developing your own controls is a great way to encapsulate your code in an easily reusable fashion. For Microsoft Windows Forms or ASP.NET the concept is the same. If you haven't done this type of development before, you really should try it out and see if you can break some of your existing code into one or more controls. Here are some suggested resources related to control development in ASP.NET:

Books

Sites

Newsgroups/Forums

Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!

 

Coding4Fun

Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.