CSS Sprite for ASP.NET

CSS sprites are becoming popular as a way to increase application performance by eliminating HTTP requests by the client to the web server. It also serves as a path for better cache management. I will need to go through a bit of background before we start (sorry... we'll get to the generation in a second!).

 

What is a CSS Sprite?

A CSS sprite is a fancy name for an image that is composed other images. In your CSS you have this one base image, but you pick the image inside of the image. It does what you expect, to just show that one single image. The following is an example of a CSS sprite:

 

sprite

 

In your CSS file, you would pick this base image as your background-image and then to pick a particular image, you could need to specify the height, width, and also the position of where the image starts... background-position.

Long story short, it cuts down on the number of images that need to be passed from your server to the client that is visiting the site. This adds up over time, especially is you have A LOT of images.

A lot of people go to a sprite generator online. Those are nice, but what happens when you need to change an image? You have to go back and re-upload all your images and recalculate your background-positions. That's not a good thing if you are following DRY (don't repeat yourself). Plus it's hard work NO ONE should have to do.

 

How I handle output caching

What I do is this: all my CSS files, JavaScript files, etc have a common ID which is basically what I call the "Distro ID" which is generated at the beginning of my ASP.NET applications so every time you do a publish, the number will change and your files will also change to their updated content. The heavy lifting is all done in the Global.asax file.

   1: //C#
   2: public class Global : System.Web.HttpApplication {
   3:     public static string DistributionNumber { get; set; }
   4:  
   5:     protected void Application_Start(object sender, EventArgs e){
   6:         SetDistro();
   7:         ..
   8:     }
   9:     private void SetDistro() {
  10:         DistributionNumber = DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Day.ToString() + DateTime.Now.Hour.ToString() + DateTime.Now.Minute.ToString() + DateTime.Now.Second.ToString();
  11:     }
  12: }
   1: 'VB
   2: Inherits System.Web.HttpApplication
   3:  
   4: Private m_distronum as String = String.Empty
   5: Public Shared Property DistributionNumber() As String
   6:     Get
   7:         Return m_distronum
   8:     End Get
   9:     Set(ByVal value as String)
  10:         m_distronum = value
  11:     End Set
  12: End Property
  13:  
  14: Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
  15:     SetDistro()
  16:     ..
  17: End Sub
  18: Private Sub SetDistro()
  19:     DistributionNumber = DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Day.ToString() + DateTime.Now.Hour.ToString() + DateTime.Now.Minute.ToString() + DateTime.Now.Second.ToString()
  20: End Sub

 

Now since I am mostly using ASP.NET MVC, I have a controller for the Distribution. So my URLs look like this: [BASE_URL]/20080303010101/CSS. I ago ahead and create separate Routes for this, but the default would also work. The ID that is passed to the CSS, JavaScript, Sprite is the distro number. To do the output caching, you need a separate ID so that the browser can differentiate one distribution's CSS, JS, etc from another. This way, we can client side cache with no worries.

If you were using Web Forms, I would suggest using separate Generic handlers (one for CSS, JS, and the Sprite). They would accomplish the same thing. Just remember that you need to take in an ID as a Query String for the Client-side cache to be safe for your users.

If the ID is not passed to the controller OR the generic handler (whichever you are using), don't client-side cache. But this is something you will want to offer as it, too, saves you in the performance department.

 

Back to the Sprite generator... get me some images!

You have 2 options when it comes to getting images. You have the option of enumerating through a folder(s) or putting the URLs in by hand. I suggest enumeration if you have the same file type. If you put in the file names by hand, that can get tricky... which I'm sure you can tell why. In my Sprite generator, I process the CSS and replace the images with the sprite reference... so we don't need the URL of the original images anyway's. All that we need to reference the images is the name (lowercase).

 

As I showed you in my last post about CSS Minification (which we will refer back to), I have a file enumerator method:

   1: //C#
   2: public static IList<System.IO.FileInfo> GetFiles(string serverPath, string extention)
   3: {
   4:   if (!serverPath.StartsWith("~/"))
   5:   {   
   6:     if (serverPath.StartsWith("/"))   
   7:       serverPath = "~" + serverPath;
   8:     else
   9:       serverPath = "~/" + serverPath;
  10:   }
  11:   string path = HttpContext.Current.Server.MapPath(serverPath);
  12:   
  13:   if (!path.EndsWith("/"))
  14:     path = path + "/";
  15:   
  16:   if (!Directory.Exists(path))
  17:     throw new System.IO.DirectoryNotFoundException();
  18:   
  19:   IList<FileInfo> files = new List<FileInfo>();
  20:   
  21:   string[] fileNames = Directory.GetFiles(path, "*." + extention, System.IO.SearchOption.AllDirectories);
  22:   
  23:   foreach (string name in fileNames)
  24:     files.Add(new FileInfo(name));
  25:   
  26:   return files;
  27: }
   1: 'VB
   2: Public Shared Function GetFiles(ByVal serverPath As String, ByVal extention As String) As IList(Of System.IO.FileInfo)
   3:   If Not serverPath.StartsWith("~/") Then
   4:     If serverPath.StartsWith("/") Then
   5:       serverPath = "~" + serverPath
   6:     Else
   7:       serverPath = "~/" + serverPath
   8:     End If
   9:   End If
  10:   Dim path As String = HttpContext.Current.Server.MapPath(serverPath)
  11:  
  12:   If Not path.EndsWith("/") Then
  13:     path = path + "/"
  14:   End If
  15:  
  16:   If Not Directory.Exists(path) Then
  17:     Throw New System.IO.DirectoryNotFoundException()
  18:   End If
  19:  
  20:   Dim files As IList(Of FileInfo) = New List(Of FileInfo)()
  21:  
  22:   Dim fileNames As String() = Directory.GetFiles(path, "*." + extention, System.IO.SearchOption.AllDirectories)
  23:  
  24:   For Each name As String In fileNames
  25:     files.Add(New FileInfo(name))
  26:   Next
  27:  
  28:   Return files
  29: End Function

 

If you were going the hand-coded image URLs, you would just need to have a method that return a List of FileInfo, just like the method from above.

 

Get the SPRITE already!

We need to specify a few data models so that they can contain the individual images and the sprites. They aren't anything special... just basically containers for the bitmap object (SpriteImage) and a Collection of the images for the Sprite generation (SpriteImageCollection). The collection is actually the generator of the sprite. The GetSprite() method is the method that will do the image concatenating for you.

   1: //C#
   2: public class SiteImage
   3: {
   4:   public SiteImage(FileInfo fileinfo)
   5:   {
   6:     Stream stream = new FileStream(fileinfo.FullName, FileMode.Open, FileAccess.Read);
   7:     Img = (Bitmap)Bitmap.FromStream(stream);
   8:     File = fileinfo;
   9:   }
  10:  
  11:   public int Width {
  12:     get { return Img.Width; }
  13:   }
  14:   public int Height {
  15:     get { return Img.Height; }
  16:   }
  17:   public int YValue { get; set; }
  18:   public Bitmap Img  { get; set; }
  19:   public FileInfo File { get; set; }
  20: }
  21:  
  22: public class SiteImageCollection : List<SiteImage>
  23: {
  24:     public int MaxWidth 
  25:     {
  26:         get 
  27:         {
  28:             int largest = 0;
  29:  
  30:             foreach (SiteImage s in this)
  31:                 if (s.Width > largest)
  32:                     largest = s.Width;
  33:  
  34:             return largest;
  35:         } 
  36:     }
  37:  
  38:     public int TotalHeight 
  39:     {
  40:         get 
  41:         {
  42:             int ttl = 0;
  43:             foreach (SiteImage s in this)
  44:                 ttl += s.Height;
  45:             return ttl;
  46:         }
  47:     }
  48:  
  49:     public Bitmap GetSprite() 
  50:     {
  51:         Bitmap spriteImg = new Bitmap(MaxWidth, TotalHeight);
  52:  
  53:         Graphics sprite = Graphics.FromImage(spriteImg);
  54:  
  55:         int curY = 0;
  56:         foreach (SiteImage si in this)
  57:         {
  58:             sprite.DrawImage(si.Img, 0, curY);
  59:             si.YValue = curY;
  60:             curY += si.Height;
  61:         }
  62:  
  63:         return spriteImg;
  64:     }
  65: }
   1: 'VB
   2: Public Class SiteImage
   3:     Public Sub New(ByVal fileinfo As FileInfo)
   4:         Dim stream As Stream = New FileStream(fileinfo.FullName, FileMode.Open, FileAccess.Read)
   5:         Img = DirectCast(Bitmap.FromStream(stream), Bitmap)
   6:         File = fileinfo
   7:     End Sub
   8:  
   9:     Public ReadOnly Property Width() As Integer
  10:         Get
  11:           Return Img.Width
  12:         End Get
  13:     End Property
  14:  
  15:     Public ReadOnly Property Height() As Integer
  16:     Get
  17:       Return Img.Height
  18:     End Get
  19:     End Property
  20:   
  21:     Private m_yval As Integer
  22:     Public Property YValue() As Integer
  23:     Get
  24:         Return m_yval
  25:     End Get
  26:     Set(ByVal value As Integer)
  27:         m_yval = value
  28:     End Set
  29:     End Property
  30:  
  31:     Private m_Img As Bitmap
  32:     Public Property Img() As Bitmap
  33:         Get
  34:             Return m_Img
  35:         End Get
  36:         Set(ByVal value As Bitmap)
  37:             m_Img = value
  38:         End Set
  39:     End Property
  40:   
  41:     Private m_file As FileInfo
  42:     Public Property File() As FileInfo
  43:         Get
  44:             Return m_file
  45:         End Get
  46:         Set(ByVal value As FileInfo)
  47:             m_file = value
  48:         End Set
  49:     End Property
  50: End Class
  51:  
  52: Public Class SiteImageCollection
  53:   Inherits List(Of SiteImage)
  54:   Public ReadOnly Property MaxWidth() As Integer
  55:     Get
  56:       Dim largest As Integer = 0
  57:  
  58:       For Each s As SiteImage In Me
  59:         If s.Width > largest Then
  60:           largest = s.Width
  61:         End If
  62:       Next
  63:  
  64:       Return largest
  65:     End Get
  66:   End Property
  67:  
  68:   Public ReadOnly Property TotalHeight() As Integer
  69:     Get
  70:       Dim ttl As Integer = 0
  71:       For Each s As SiteImage In Me
  72:         ttl += s.Height
  73:       Next