Tuesday, November 27, 2007

Avoiding CSS caching issues

I had a problem in several projects where we changed the CSS and deployed it to the load balanced server environment, and still the old CSS file was cached somewhere, and we couldnt roll out the changes to the users.
Another problem on the same line are browsers that cache the css files, and users do not see the changes we make to the css until we tell them to clear their browser's cache.

To resolve this, we took a trick from Microsoft - notice how Microsoft always links to the css with a version number in sharepoint? this is done to avoid caching when you put a new version.

My trick was to develop a custom web control (that I call the "CssInjector") that gets as a property the link to the css file, and renders that link with a random querystring. this forces the browsers (as well as IIS or other caching applications) not to cache the file.
The problem with this solutions is that it will have performance implications - slower loading of pages, so what I'd really like to do is what MS did - rely on a version number. However, I have yet to come up with the architecture that will let me manage version numbers in my solution package without having to manually setting the version every time. I thought of reading the version number from the CSS file itself, but that will introduce performance issues again - as the code will have to load and read the css file each time a page is opened.

So until I get a better solution, here is the code I am using for my web control:

using System;
using System.Web.UI.WebControls;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
namespace SharePointTips.SharePoint.WebControls{
public class CssInjector : WebControl
    {
        private const string CSS_LINK_FORMAT = @"<link rel=""stylesheet"" type=""text/css"" href=""{0}"">";
        private string _CSSFileLink = "";
        public string CSSFileLink
        {
            get { return _CSSFileLink; }
            set { _CSSFileLink = value; }
        }
        protected override void Render(HtmlTextWriter writer)
        {
            if (this.CSSFileLink.Length > 0)
            {
                writer.Write(string.Format(CSS_LINK_FORMAT, this.CSSFileLink + "?k=" + GetUniqueKey(8)));
            }
        }
        public static string GetUniqueKey(int length)
        {
            //create an array of acceptable characters
            char[] chars = new char[62];
            chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
            StringBuilder result = new StringBuilder(length);
            Random rnd = new Random();
            //create the random string in the specified length
            for (int i = 0; i < length; i++)
            {
                result.Append(chars[rnd.Next(chars.Length)]);
            }
            return result.ToString();
        }
    }
}
And here is how I use it in the master page:
<%@ Register Tagprefix="SharePointTipsWebControls" Namespace="SharePointTips.SharePoint.WebControls" Assembly="SharePointTips.SharePoint.WebControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxx" %>
<SharePointTipsWebControls:CssInjector runat="server" ID="SharePointTipsCSS1" CSSFileLink="/_layouts/SharePointTips/SharePointTipsCustomCss.css" />

1 comment:

Phil Lanier said...

If you Reflect against the SharePoint.dll, you'll see that Microsoft doesn't actually use version numbers in the URL when accessing CSS files. Rather, the key they use is an MD5 hash of the actual file in the 12-hive.

The great thing is that you can easily do the same thing by calling the SPUtility.MakeBrowserCacheSafeLayoutsUrl() method.