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.
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
- Developing ASP.NET Server Controls and Components (by Nikhil Kothari and Vandana Datye)
- Essential ASP.NET with Examples in Visual Basic .NET (by Fritz Onion)
Sites
- The ASP.NET Developer Center on MSDN has a Web Controls Section
- The Control Gallery (part of the ASP.NET team's Web site)
Newsgroups/Forums
- The Server Control Development Forum on www.asp.net
- The microsoft.public.dotnet.framework.aspnet.buildingcontrols newsgroup
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.