September 9, 2010  
  You are here:  Blog
LINQ to Joe Archive
Module Border Module Border
My Book

C#


VB

Module Border Module Border

Creating A Reusable AdRotator ASP.NET User Control Using LINQ
 
Location: BlogsLINQ to Joe    
Posted by: Joe Rattz 2/11/2010 11:07:33 PM

Creating A Reusable AdRotator ASP.NET User Control Using LINQ

I recently had the need for an AdRotator on one of my websites.  ASP.NET already comes with an AdRotator server control but since I wanted to make it even easier to reuse, I decided to create an ASP.NET user control that leverages the ASP.NET AdRotator server control, but handles all the lower-level details.

Requirements

One of my requirements is that I wanted to be able to have multiple instances of this user control, perhaps even on the same page.  I also wanted to be able to either have an instance have its own pool of ad content, or share the ad content with another instance.  Since the built-in AdRotator control has a Keyword feature, that would be how I would control which pool of ad content an instance of the user control displayed content from.

Additionally, I wanted the ad content coming from the database, as opposed to a configuration (XML) file, which the built-in AdRotator server control allows as of ASP.NET 2.0.  This way I could add new content right into the database.

Another requirement was that I didn't want the AdRotator hitting the database every time the page was refreshed.  For this, I planned to use the Cache object that exists in ASP.NET.

Another requirement was that I wanted the ability to be able to temporarily disable an ad without having to remove it from the database, and I wanted to be able to provide effective and expiration dates for an ad so that they could come online and go offline automatically as scheduled.

My last requirement was that I wanted to use LINQ to SQL for the database access.

Creating an AdRotator user control using the built-in AdRotator server control and LINQ made this all fairly trivial to accomplish.  Here is how I did it.

Creating the LINQ to SQL DBML File

First, lets start with my data model.  Here is what my SQL Server database table looked like:

AdRotatorContentLayout.jpg

As you can see, I have all the fields needed for an ad including an image URL, navigation URL, keyword, enabled, effective date, and expiration date.

Next, in Visual Studio 2008, I created a LINQ to SQL DBML file for my AdRotatorContent table.  To do this, right-click on your Project in the Solution Explorer window and select the Add | New Item menu.  Select the LINQ to SQL Classes template and give the new item the name of your database.  In this example, I will use Storefront for the name of my new item.  This will create a file named Storefront.dbml in my project.  This also means that I will have a derived DataContext class named StorefrontDataContext that I will use to access the database with LINQ to SQL.

StorefrontDBMLDesigner.jpg

Once the desginer canvas window is open, find your database in the tree of Data Connections in the Server Explorer window.  You may need to add a connection to your database by right-clicking on the Data Connections node and selecting the Add Connection menu item.  Once you have located (or created) your database in the Data Connections node, expand the Tables node.  Drag the AdRotatorContent table node into the designer canvas as seen in the image above.  Save your DBML file.  This will allow you to access the AdRotatorContent table with your derived DataContext class, StorefrontDataContext in my case, using LINQ to SQL.

Creating the AdRotatorControl User Control

Next, I need to create the user control.  I will name it AdRotatorControl.  Here is the markup in the AdRotatorControl.ascx file:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="AdRotatorControl.ascx.cs" Inherits="GPCStoreFront.UserControls.AdRotatorControl" %>

That's all there is to the markup.  This is really trivial so far.  I simply set the NavigateUrlField, AlternateTextField, and ImageUrlField properties to the names of the columns in the database where those values will come from.

Now let's take a look at the code-behind in the AdRotatorControl.ascx.cs file:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace [YOUR NAMESPACE GOES HERE]
{
    public partial class AdRotatorControl : System.Web.UI.UserControl
    {
        public string Keyword { get; set; }
        public string Target { get; set; }
        public string CacheName { get { return String.Format("AdContent-{0}", Keyword); } }

Please notice that you will need to update the namespace with your namespace.  Notice that I have added a using directive for the System.Data.SqlClient namespace since I will be using LINQ to SQL.

The only other significance of the code so far is that I have added three public properties: Keyword, Target, and CacheName.

I will use the Keyword property as a filter on the records from the AdRotatorContent table Keyword column.  This is what will allow me to have multiple instances of the AdRotatorControl user control that either share the ad content or have independent content.  I have made this a public property so that I can set the property in the markup for the AdRotatorControl user control.  This will allow me to add the user control to a page without adding any code to the page to configure the user control.

I will use the Target property merely as a passthrough to set the AdRotator server control's Target property.  This will dictate where the navigation URL will be loaded when the ad image is clicked.  Use _blank to cause the URL to be opened in a new browser window.  Use _self to open the URL in the current browser window.  Of course you can use any valid target, but those are the standard ones that make the most sense.  In my case, I will always use _blank because it is one of my self imposed web developement rules that no link should ever navigate away from your own site.  This is also a public property so that it can be set in the markup thereby preventing the requirement of adding code.

I will use the CacheName property to retrieve the key name I will use to store and retrieve the ad content data from the Cache.  Please notice that this property is a read-only property as there is no set statement.  I wanted a property for the key name to make sure it was used consistently through the code.  Also please notice that the key name is dependent on the Keyword property thereby establishing it's uniqueness in the Cache.  You may want to make this property private, but I made it public just in case I want to access it from outside the class for debugging purposes.

Next, let's take a look at the Page_Load method:

        protected void Page_Load(object sender, EventArgs e)
        {
            this.AdRotator1.Target = Target;

            AdRotatorContent[] data = GetAdData();
            this.AdRotator1.DataSource = data;
            this.AdRotator1.DataBind();
            this.Visible = data.Length > 0;
        }

First, please notice that I am setting the AdRotator server control's Target property based on the user control's Target property.  This is what I meant earlier when I said I was using it as a passthrough.  Its sole purpose is to allow the AdRotator server control's Target property to be set from the user control's markup preventing the need to actually write any code to add an instance of the user control to a page.

Next I call the GetAdData mefhod.  This is where all the real work will be done as well as caching.  For now, we'll just assume it returns an array of ad content matching the Keyword specified in the user control's markup regardless of whether that data comes from the database or cache.

Then I simply bind the AdRotator server control to the array of ad content data and set the visibility of the user control depending on whether there is any ad content.  This allows the user control to simply disappear if no ads exist for the specified Keyword that are enabled and valid for the current date and time.

Now let's take a look at the GetAdData method:

        private AdRotatorContent[] GetAdData()
        {
            //  Attempt to retrieve the ad content data from the cache.
            AdRotatorContent[] data = Cache[CacheName] as AdRotatorContent[];
            if (data == null)
            {
                using (StorefrontDataContext dc = new StorefrontDataContext())
                {
                    DateTime now = DateTime.Now;
                    data = dc.AdRotatorContents
                        .Where(arc => arc.Keyword.Equals(Keyword) && arc.Enabled == true && 
                                     arc.EffectiveDate <= now && arc.ExpirationDate >= now)
                        .ToArray();
                    Cache.Insert(CacheName, data, null, DateTime.Now.AddMinutes(5), TimeSpan.Zero,
                        System.Web.Caching.CacheItemPriority.NotRemovable, null);
                }
            }
            return data;
        }
    }
}

I am hoping you are blown away by how trival this method turned out to be.  First, I attempt to retrieve the ad content array from the Cache using the CacheName property I described earlier.  Next, I check to see if it is null.  If it is not null, I merely need to return the AdRotatorContent array.  If it is null, meaning it is not in the Cache, I instantiate a StorefrontDataContext.  This is my LINQ to SQL derived DataContext class.  Notice that I am taking advantage of the using statement to automatically handle disposing the object when I am finished with it.

Next, I simply store the current time in a variable since I will reference it in more than one spot, just to ensure consistency of it.

Then I make my LINQ to SQL query.  Notice that I am using the Where operator to retrieve only those ads for the specified Keyword that are enabled and whose effective and expiration dates make the ad valid now.  Also please notice that I am calling the ToArray method to convert the returned IQueryable to an array.  This is to prevent deferred query execution and the query executing later, after I have disposed of the StorefrontDataContext.  Despite discussing deferred query execution at great length in my book, Pro LINQ: Language Integrated Query in C# 2008, even I forgot to be mindful of it, and this initially bit me.  The most important part though is recognizing it when it happens and knowing how to resolve the problem.  For this case, I decided to use the ToArray method to create an array from the IQueryable.

Next, I am inserting the array of ad content into the Cache using the CacheName property, again to ensure consistency between the insert here, and the retrieval at the beginning of the method.  I am also specifying for the data to live in the Cache for 5 minutes.  This means that the data will be cached for 5 minutes and then deleted from the Cache.  When the next query arrives after the deletion, the code will cause the database to be queried and the data will be stored in the cache again where it will live for another 5 minutes.  I used 5 minutes for this example because it is simple and makes testing the caching, disabling of ads, and effective and expiration dating of ads easier.  You may want a longer time in production.  In my real code, I will set this longer and store the time period in a configuration setting for the web application.

There are other, more sophistocated, ways to expire this data from the Cache, but for this purpose, a time limit is sufficient.  You should be aware though that you can create Cache dependencies that will detect if a file or database table (among other things) is changed and delete the object from the Cache when that happens.  So in fact, we could have created a database table dependency on the AdRotatorContent table itself so that when it is changed, the ad content is deleted from the Cache and then read again from the database.  For a really important real-time system, that would be ideal.  However, the way you do this is dependent on the database and its version so this is beyond the scope of this blog post.  And, for most people's ad content, having such real-time access to the ads is not necessary.  I expect Google would feel differently though.  ;-)  For more information about creating Cache dependencies or the Cache object in general, Matthew MacDonald's Pro ASP.NET 3.5 in C# 2008 (ISBN 1-59059-893-8) is a good read and I refer to it often.

Also notice that I am specifying a CacheItemPriority of NotRemovable.  This is because I found that the caching on my production server didn't behave as I would expect.  Despite having plenty of memory available, my Cache objects were getting flushed prior to their expiration time, and almost immediately.  You can read more about this here:

http://www.west-wind.com/Weblog/ShowPost.aspx?id=11379

The last argument to the Insert method is a callback handler for when items get flushed from the cache.  I have specified this to be null since I don't want a callback handler.  This can however be a useful feature to determine why your Cache objects are getting flushed.

Adding the AdRotatorControl User Control To a Page

All that's left is to add the user control to a page.  Here is the markup to do that:

Please notice that I am specifying the Keyword and Target properties.  The Keyword will be used to filter just those ads in the database matching the Keyword specified in this property.  This would allow me to put more than one instance of the user control on a single page but to have a different pool of ad content.  For example, I might put one instance of the user control on the page and specify the Keyword as "Tech" to pull just those ads from the database having the Tech keyword.  I might add another instance of the user control on the same page and specify the Keyword as "Political" to pull just the political ads.  I could even add an instance of the user control to another page and specify the "Tech" or "Political" Keyword so that it would share the same pool of ad content.

Here are a couple screenshots of the AdRotatorControl user control in action:

AdRotatorInAction1.jpg

AdRotatorInAction2.jpg


Using ADO.NET Instead of LINQ to SQL

While it was one of my requirements to use LINQ to SQL, nothing says that you must.  Should you prefer to use ADO.NET rather than LINQ to SQL, simply replace the LINQ to SQL code with ADO.NET code retrieving a DataSet from the database and store that DataSet in the Cache object instead of the array.  This is a very trivial change to make.

All in all, I am very pleased with the way this AdRotatorControl turned out.  I think you will find it fairly useful and simple to add to your web application.

 

Copyright ©2010 Joe Rattz
Permalink |  Trackback

Your name:
Title:
Comment:
Add Comment   Cancel 
 
 
Home|Forums|Blog|LINQ Extras|Contact Me|Book Reviews
  Copyright (c) 2010 LINQDev Terms Of Use Privacy Statement