Saturday, January 23, 2010

Automatically keeping CSS file current on any web page

The problem: You update your site's CSS file but your users aren't seeing your latest crazy colors and styles that you've selected for your web site.

The reason: Your users' browsers have cached the CSS files and it could take days before those caches expire and your new CSS file is requested from your server.

The solution: Change the name of your CSS file that's linked in the header of your page each time you change the contents of the CSS file.

The new problem: You don't want to change the name manually each time because (1) it may need changing in more than one place, (2) you might forget to change it, (3) you're lazy, (4) you might miss somewhere it needs to be changed, (5) each change increases the risk you might do something wrong.

The new solution: Have it done automatically for you.

This is how I implemented it using ASP.NET and C#. I did this on a hybrid ASP.NET MVC and WebForms web site that has two base master pages; one for MVC and one for WebForms. All other master pages inherit from these two master pages so there were just two locations that need to be changed.

In the <head> tag I originally had something like this:

<link href="/site.css" rel="stylesheet" type="text/css" media="all" />

and I wanted something like this:

<link href="/site.css?v=1" rel="stylesheet" type="text/css" media="all" />

with the 1 changing each time the site.css file changed.

In any major project that I'm working on I usually have a class called something like StaticData. This class holds arbitrary bits of data that are loaded or calculated once and then never or rarely change. It's like a hybrid of a constants file and a cache.

In this class I added the following property and private variable:

string _CssVersion = null;
public string CssVersion
{
    get
    {
        if (String.IsNullOrEmpty(_CssVersion))
        {
            try
            {
                string cssFile = System.Web.HttpContext.Current.Server.MapPath("~/site.css");
                FileInfo fi = new FileInfo(cssFile);
                DateTime lastWriteTime = fi.LastWriteTime;
                _CssVersion = lastWriteTime.ToString("yyyyMMddHHmmss");
            }
            catch
            {
                return "1";
            }
        }
        return _CssVersion;
    }
}

So what I'm doing is generating a version number based on the time stamp of the CSS file. If we update the CSS file then that time stamp will automatically change and we'll only have to load it once because after that it's in the "constants cache."

To load the CSS name dynamically in ASP.NET we add the following code snippet to the Page_Load() function of the code behind file of the master page. This applies to both WebForms and MVC applications.

HtmlLink css = new HtmlLink();
css.Href = String.Format("/site.css?v={0}", StaticData.Instance.CssVersion);
css.Attributes["rel"] = "stylesheet";
css.Attributes["type"] = "text/css";
css.Attributes["media"] = "all";
Page.Header.Controls.Add(css);

The Instance property of the StaticData class is a public static property of type StaticData making this class a singleton.

5 comments:

  1. I've done something similar except I tie it to the current version of the page's assembly via reflection. Thus whenever there's a new build, the version number automatically changes. (It's great for local box development also.)

    ReplyDelete
  2. I was thinking of using a version number but the CSS file doesn't change with each version number so I wanted to keep the file's signature the same and only change it if the CSS file changed. That's why I resorted to the date/time stamp for the version just for the CSS file.

    ReplyDelete
  3. Hmm, I see you check if there is a value in cssversion:
    if (String.IsNullOrEmpty(_CssVersion))
    But I don't see a dependency on the file. How will changing the contents of the file trigger the version to be reloaded? Once you have set the version you are always returning it regardless of a file change. Only a restart will force you to read the value again?
    I may be missing something.
    I have updated the CssVersion property to be as follows:
    public static string CssVersion
    {
    get
    {
    if (HttpContext.Current.Cache["CssVersion"] == null)
    {
    FileInfo style = new FileInfo(HttpContext.Current.Server.MapPath("~/styles/site.css"));
    HttpContext.Current.Cache.Insert("CssVersion",
    style.LastWriteTime.ToString("yyyyMMddHHmmss"),
    new CacheDependency(style.FullName),
    DateTime.Now.AddMinutes(60),
    TimeSpan.Zero,
    CacheItemPriority.Default,
    null);
    }
    return HttpContext.Current.Cache["CssVersion"].ToString();
    }
    }
    This will trigger the version to be reloaded should be file be changed.

    ReplyDelete
  4. Sam - you are correct about the caching of the CSS file inasmuch as only a restart will force a read on the value. This is designed for the CSS to be changed to be changed at each deployment which goes along with a reset so that is why this paradigm works in this situation. However, if you're deploying CSS files without an application reset then you would have to implement it the way that you described.

    ReplyDelete
  5. This is terrific, over two years later and I was able to adapt this for a razor website, for CSS and JS links. Thanks!

    ReplyDelete