Using the AdRotator Class + Dynamic Image Generation in ASP.NET 2.0

As part of my study group homework, I promised to bone up on the AdRotator class in ASP.NET2.0 and report back to the group. Turns out this lead to another cool thing that I needed to report back on - dynamic image generation.

The source code I’ll refer to throughout this post is available for download. Of course, it’s intended as a sample only. Use it at your own risk, no warranty expressed or implied, contents may be hot. Don’t say you weren’t warned. The source code demonstrates the 3 modes of populating an AdRotator control and a technique for dynamically displaying images stored as binary data in fields of a database.

AdRotator Class

The AdRotator class basically allows you to display a random image (more on where images come from and how "random" works in a bit) with an associated bit of ALT text and (optionally) a hyperlink from the image to somewhere else. You’ve seen this kind of thing on web pages before, in fact it goes back to ASP.NET 1.0 and has been implemented in any number of other web frameworks as well. Since we’ve been studying for the ASP.NET 2.0 exam, that’s what I’ll cover here.

3 Modes

AdRotator populates its image and navigation properties in 3 different ways, which I’ll call “Database”, “XML” and “Programmatic” for reasons that should become obvious, if they’re not already.

Database

In database mode, an AdRotator is linked to a DataSource that needs, at the very least, a column for the URL of the image, a column for the URL to navigate to when the imaged is clicked and a column for the string to be displayed in the ALT tag of the image. In the MSDN documentation (online and offline), the full allowable schema is listed and includes some compulsory fields:

  • An int primary key, which can be called anything you like
  • an nvarchar field called “ImageURL” that has a relative or absolute URL pointing to the image to be displayed
  • an nvarchar field called “NavigateURL” that has a relative or absolute URL that the image is hyperlinked to. If you leave the contents of this field blank, there’s no hyperlink from the image.
  • an nvarchar field called “AlternateText” containing a string that will be inserted into the <IMG> tag’s ALT attribute (and will, therefore, depending on the viewing browser’s capabilities or preferences, be displayed as a tooltip, or instead of the image or be read aloud to the user etc). By the way, is it just me, or should this be called "AlternativeText"?

Note that the name of these fields can be overridden in the AdRotator by setting the ImageURLField, NavigateURLField and AlternateTextField properties to the appropriate field name.

In addition, the following fields are optional.

  • an int field called “Impressions”. This is the relative weighting of each field and determines the likelihood of a particular image being displayed. You can use this field to do absolute weighting (where the impression values don’t change), or to smooth the curve somewhat by decrementing the value in the source data whenever an image is displayed (or when the hyperlink is clicked, or whatever). I can imagine putting something in this field like the amount of money each advertiser has given me to display their add. The more you give me, the better the chance your ad comes up.
  • an nvarchar field called “Keyword”, which allows you to keep all your ads for the entire site (or perhaps many sites) in the one table and the instance of the AdRotator Class on a particular page can filter to just get the relevant ads for that page by setting the KeywordFilter property.
  • int fields called “Width” and “Height”, which allow the AdRotator to include height and width hints as attributes of the generated <IMG> tag.

Of course, with the cool data handling functionality of data sources in ASP.NET 2.0, you don’t need a table anything like this, you can pull the data into the appropriate structure with a SQL statement in the DataSource itself. So, if you were dynamically displaying the thumbnail image from the ProductPhoto table in the AdventureWorks sample database, allowing a click to redirect to the LargePhoto image in the same table (again, dynamically rendered) and taking a description from the Product table for the Alternative text (OK, from now on I’ll call it Alternate text and grind my teeth), you could use something like

 <asp:SqlDataSource ID="ProductPhotos"
  runat="server"
  ConnectionString="<%$ ConnectionStrings:AdventureWorksConnectionString %>"
  SelectCommand="SELECT Production.ProductPhoto.ProductPhotoID AS ID, 
    './ProductThumbNail.ashx?ID=' + LTRIM(STR(Production.ProductPhoto.ProductPhotoID)) AS ImageUrl, 
    './ProductPhoto.ashx?ID=' + LTRIM(STR(Production.ProductPhoto.ProductPhotoID)) AS NavigateUrl, 
    Production.Product.Name AS AlternateText 
    FROM Production.ProductPhoto 
    INNER JOIN Production.ProductProductPhoto 
      ON Production.ProductPhoto.ProductPhotoID = Production.ProductProductPhoto.ProductPhotoID 
    INNER JOIN Production.Product 
      ON Production.ProductProductPhoto.ProductID = Production.Product.ProductID">
</asp:SqlDataSource> 

Which produces a resultset with the 4 required columns. Of course, writing SQL statements like that is much easier when you do it like this:

QueryBuilder

The AdRotator control itself then looks like this:

 <asp:AdRotator ID="AdRotator1" runat="server" DataSourceID="ProductPhotos" />

All of the properties (except the DataSourceID and the name, of course) are left at their default values because I've used the default names for the fields in the DataSource definition.

XML

The next way to populate the fields of an AdRotator is to use a static XML file. It has a very similar structure to the schema of the table required for the Database method. All of the details of the schema are available in MSDN (online and offline). There’s an example file included in the accompanying downloads. Here's an extract (of a file called Advertisments.xml):

 
<Ad>
  <ImageUrl>~/ProductThumbNail.ashx?ID=70</ImageUrl>
  <NavigateUrl>~/ProductPhoto.ashx?ID=70</NavigateUrl>
  <AlternateText>Product Number 70</AlternateText>
  <Impressions>100</Impressions>
</Ad>

 And the AdRotator looks like this

 
<asp:AdRotator ID="AdRotator2" runat="server" AdvertisementFile="~/Advertisments.xml" />

Again, the default values work here because I’ve followed the naming conventions.

Programmatic

The third (and potentially most interesting from a developers’ point of view, although the database method is pretty cool too) is to populate the properties programmatically whenever the AdRotator AdCreated event fires (occurs once per round trip to the server after the creation of the control, but before the page is rendered). The event handler is passed an AdCreatedEventArgs object. You set the 3 properties, ImageURL, NavigateURL and AlternateText (familiar looking names aren’t they) and they are used to render the control.

The AdRotator control looks like this:

 
<asp:AdRotator ID="AdRotator3" runat="server" OnAdCreated="AdRotator3_AdCreated" />

And in the code-behind file (mine's in C#, but VB.NET would work just as well), I just wired up an event handler (via the properties grid in my case, because I'm lazy)

 
protected void AdRotator3_AdCreated(object sender, AdCreatedEventArgs e)
{
  // Just for this example - see the download for a different way of using ProductID
  int ProductID = 1;
  e.ImageUrl = "~/ProductThumbNail.ashx?ID=" + ProductID.ToString();
  e.NavigateUrl = "~/ProductPhoto.ashx?ID=" + ProductID.ToString();
  e.AlternateText = "Programatically generated text for product ID " + ProductID.ToString();
}

This is potentially very powerful. You could do real-time image targeting based on the navigation history of this user, based on personalisation they’ve done or any number of other criteria.

Dynamic Image Generation

Up until now, I’ve glossed over the fact that the AdRotator class expects URLs pointing to images, not byte streams or arrays of binary data, or anything else that might represent an image. This potentially presents a problem when (as we have in our example), the images are stored as binary data in a database file. You don’t want to have to write them out to disk, where they’ll take up as much space again and need to be kept in sync with the database version. Up until Beta 1 of VS2005, it looked like this was going to be very easy, just use the Dynamic Image Control. Alas, it was dropped between Beta 1 and Beta 2 (see https://msdn.microsoft.com/asp.net/beta2/beta2changes.aspx). Fortunately, that very same page lists some code that can be adapted to do exactly what we need.

Essentially, we need to create an HTTP Handler class that intercepts calls to a particular URL and returns the image in a format that the browser expects. There are docs on the IHTTPHandler interface (which the specialised dynamic image generation classes in the example implement) in the MSDN docs (online and offline). HTTP handlers give you a means of interacting with the low-level request and response services of the IIS Web server and provide functionality much like ISAPI extensions but with a simpler programming model.

Here’s the code for one of the Handler classes (ProductThumbNail.ashx). The other one is identical, except a different TableAdaptor is used to retrieve the bigger photo.

 
<%@ WebHandler Language="C#" Class="ProductThumbNail" %>



using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Web;
using System.Web.Caching;
using System.Data;
using System.Data.Sql;
using System.Web.UI.WebControls;
using System.Web.Configuration;
using System.Configuration;
using System.Collections;



public class ProductThumbNail : IHttpHandler
{
  public void ProcessRequest (HttpContext context) {

    // Get the image ID from querystring, and use it to generate a cache key.
    String imageID = context.Request.QueryString["ID"];
    String cacheKey = context.Request.CurrentExecutionFilePath + ":" + imageID;

    Byte[] imageBytes;
    
    // Check if the cache contains the image.
    Object cachedImageBytes = context.Cache.Get(cacheKey);
    
    if (cachedImageBytes != null)
    {
      imageBytes = (Byte[])cachedImageBytes;
    }
    else
    {
      GetImageTableAdapters.ThumbNailTableAdapter oTA = new GetImageTableAdapters.ThumbNailTableAdapter();
      GetImage.ThumbNailDataTable oDT = oTA.GetData(int.Parse(imageID));
      
      if (oDT.Rows.Count == 0)
      {
        // MAGIC NUMBER!
        // Photo ID 1 displays "No Image Available" image
        oDT = oTA.GetData(1);
      }
      
      imageBytes = (byte[])oDT.Rows[0]["ThumbNailPhoto"];
      
      // Store it in the cache (to be expired after 2 hours).
      context.Cache.Add(cacheKey, imageBytes, null, 
        DateTime.MaxValue, new TimeSpan(2, 0, 0), 
        CacheItemPriority.Normal, null);
      
    }
     
    // Send back image.
    context.Response.ContentType = "image/jpeg";
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.BufferOutput = false;
    context.Response.OutputStream.Write(imageBytes, 0, imageBytes.Length);
  }
  
  public bool IsReusable {
    get {
      return false;
    }
  }
}

The important method to implement in the IHTTPHandler interface is ProcessRequest(). This gives you low-level access to the context object which, in turn, gives to access to the Request and Response objects so you can get the ID passed as a query string in the URL (using Request) and shove stuff back out with the Response object. The stuff to shove back out is just a byte array grabbed from the binary column in the table. Notice, I’ve just grabbed the contents of the ThumbNailPhoto column in the first row and cast it to a byte array.

To send the image back, just set the content type (note that this could come from the table as well, or could be dynamically determined from the header in the binary if required), tell the Response object just to stream the bits back, don't wait for the stream to close, and then just write the byte array to the output stream.

A couple of other things to note here (and I’m not claiming they’re best practice – comments always welcome):

Using a DataSet to retrieve data from the server

Notice that instead of programatically creating connections and commands, I’ve opted to create a dataset (GetImage.xsd in the App_Code folder) with a couple of table adaptors. The naming convention adopted by ASP.NET gets me every time I use these. The TableAdaptors are in the <DataSetName>TableAdaptors namespace and aren’t available to intellisense until you’ve built the project after the xsd is created. The data tables are classes of the <DataSetName> class. So in this case The GetImageTableAdaptors namespace contains the ThumbNailTableAdaptor and the ProductPhotoTableAdaptor classes. The GetImage class has two properties – instances of the ThumbNail and ProductPhoto classes.

Configuring these datasets is heaps easier than remembering how to use them. Just add a DataSet to the project and the first TableAdaptor is added automatically. I added a very simple SQL statement with a parameter (the ProductPhotoID) to return a single row. To add the other, just right-click on the xsd design surface and choose Add TableAdaptor.

Note that I’ve opted for the very simplest Table Adaptors – no INSERT, UPDATE or DELETE clauses. These are only ever designed to be read-only.

Caching Images

This bit was already in the code on the MSDN site about the DynamicImageControl being pulled, but I left it in there because it seemed like such a good idea. The cache engine takes a key to see whether it has already looked up the image and stored it for future use. In this case I’ve left the cache duration at 2 hours. Obviously, you’d massage this based on how often images were likely to change.