Adding Variables To Style Sheets

Posted on July 08, 2004   |   Download sample code

18 comments

Even though the spec defines CSS as a style sheet language, this language is missing one of the essential features: variables. Every time I've seen people in newsgroups ask how to implement variables in CSS a typical advice was to build the style sheet on the server. Luckily, it is easy to do in ASP.NET with the help of HTTP Handlers.

An HTTP Handler Refresher

An HttpHandler is a class that implements the IHttpHandler interface. This class can be associated with pretty much any file extension. Once you request a resource with a certain extension a corresponding HttpHandler processes your request and sends back a response. For example, when you request an ASP.NET page with the .aspx extension the System.Web.UI.Page handler processes the request and responds with an HTML page.

We're going to write a custom handler and associate it with .css files. This handler will allow us to add variables to our style sheets. For example, we'll define variables and their values in a configuration file (how about web.config?):

<DynamicCss>
  <variables>
  <variable name="$main_color" substitute="#fff" />
  <variable name="$bkg_color" substitute="gray" />
  </variables>
</DynamicCss>

and then use the variables in a style sheet:

body
{
  background: $bkg_color;
  color: $main_color;
}

When a .css file is requested our HttpHandler will replace the variables on the fly. To make this happen we'll need to implement two pieces:

  1. A handler of configuration settings to help us initialize variables, and
  2. An HttpHandler to substitute these variables in real time

Implementing a Configuration Section Handler

You may extend the standard set of settings in web.config with your own settings. This is great news because you may add any configuration settings you want as long as you also write a handler to digest these settings. A configuration section handler is a class which implements the IConfigurationSectionHandler interface:

using System;
using System.Configuration;
using System.Xml;
using System.Xml.Serialization;

namespace DynamicCSS
{
  public class SettingsHandler : IConfigurationSectionHandler
  {
   private XmlSerializer _ser = new XmlSerializer (typeof (Settings));

    public object Create (object parent, object configContext, 
                          System.Xml.XmlNode section)
    {
       Settings cfg = (Settings) 
                _ser.Deserialize (new XmlNodeReader (section));
        return cfg;
    }
  }
}

To put this handler to good use you need to register it in web.config:

 <configSections>
  <section name="DynamicCss" type="DynamicCSS.SettingsHandler, 
   DynamicCSS" />
 </configSections>

The Settings class in a listing above is going to handle variable names and their values:

using System;
using System.Collections;
using System.Xml;
using System.Xml.Serialization;

namespace DynamicCSS
{

[XmlRoot("DynamicCss")]
public class Settings
{
 private Variable[] _variables;

 [XmlArray ("variables")]
 [XmlArrayItem ("variable", typeof (Variable))]
 public Variable[] Variables
 { 
   get { return _variables; }
   set { _variables = value; }
 }
}

/// <summary>
/// This class represents a variable and its substituted value
/// </summary>
public class Variable
{
  string _name;
  string _substitute;

  public Variable () {}

  [XmlAttribute ("name")]
  public string Name
  {
    get { return _name; }
    set { _name = value; }
  }

  [XmlAttribute ("substitute")]
  public string Substitute
  {
    get { return _substitute; }
    set { _substitute = value; }
  }
 }
}

Variable is a painfully simple class that represents a variable and its value (declared in web.config, as we agreed), while the Settings class is another painfully simple class which simply implements a collection of these variables in an array. You might've noticed the use of an XML serializer and XML attributes in both Settings and Variable. XML serialization is outside the scope of this article, but suffice it to say you only need to carefully match serialization attributes with XML elements and attributes in your configuration section. The XML serializer will take care of the rest! This is both elegant and powerful!

Implementing an HTTP Handler

Now that we've defined variables, their values, put them in web.config and wrote a handler to read them, we'll implement the final and most important piece: an HttpHandler itself. Please, download the source code of the handler and poke around. I will only outline the steps our handler is supposed to take:

  1. A request for a .css file is received. The handler looks for the file on the hard drive. If it's missing or ASP.NET is not authorized to read it for whatever reason, we send an appropriate HTTP code along with an error message.
  2. Otherwise, we check if the file has been modified since we served it last. If it has been intact and the browser is willing to use it from its cache, we serve the HTTP code 304 ("Not Modified") and leave.
  3. If we've come this far, we read the style sheet and replace every variable occurrence (if any) with its value.

Since we've discussed a configuration section handler in great detail here's how you iterate over variables in the HttpHandler:

Settings cfg = (Settings) System.Configuration.«
               ConfigurationSettings.GetConfig ("DynamicCss");

foreach (Variable var in cfg.Variables)
{
    // ------------------------------------
    // Search for the variable

    // NOTE: The search is case-sensitive
    // ------------------------------------
    Regex re = new Regex (string.Concat (
                            "\\$\\b", var.Name.Substring (1), "\\b"));
    css = re.Replace (css, var.Substitute);
}

I use a simple regular expression to replace variable occurrences.

NOTE: Variable names are case sensitive. If you want to "relax" this condition you need to add RegexOptions.IgnoreCase to the Regex constuctor.

At this point we've got all bits and pieces in place. One last thing is to configure IIS to cooperate with us and we're truly done.

Configuring IIS

When you install the .NET Framework a number of file extensions are associated with the ASP.NET ISAPI. For example, IIS hands requests for .aspx, .asmx and .ashx files to aspnet_isapi.dll. On the other hand, .css files are not mapped to .NET and therefore IIS handles them on its own, and our HttpHandler won't be even invoked. We need to remap .css files to the ASP.NET ISAPI so the handler will get a piece of the action.

To do so launch the Internet Information Manager applet from Administration Tools, find your web app, righ click and go to Properties. On the Virtual Directory tab click Configuration. On the Mappings tab click Add and configure extension mapping as shown below:

Configuring CSS extension mapping

The ISAPI path is quite long, so simply copy and paste it from another extension. Done and done.

Application Notes

A good fit for this technique is branding web applications. For example, you have to configure color schemes of your web app for different instances. We do it with our main product. Making a separate style sheet for each instance is painful enough as the customer base grows. Introducing variables makes matters much easier since you don't have to tweak CSS once you make it variable-oriented.

Using this technique would be wrong if you plan on changing variables often. Remember we store them in web.config? Each time you edit web.config you cause your web app to restart. Often restarts will degrade performance among other things, as I explained in my article Beware Of Deploying Debug Code In Production. Therefore you might want to configure variables and leave them alone for a while.

If you do need to change variables often I'd recommend you move them out of web.config into a separate XML file, for example. In fact, you won't need a configuration section handler any more. All you need to do is to somehow feed variables to the HTTP handler.

Acknowledgements

Scott Watermasysk kindly provides source code of his blogging engine, .Text. If you feel adventurous poke around it—you'll find many interesting code snippets in there. I borrowed the idea of sniffing for the If-Modified-Since header from Scott.

18 comments

SomeNewKid
on July 10, 2004

Excellent article, Milan, particularly as it touches on some less-well-known ASP.NET abilities such as XML serialization and custom config sections.

I once came up with a quick-and-dirty method of adding variables to stylesheets. However, your method is indeed much sweeter.

Keep up the great work!


Ryan Farley
on July 12, 2004

Great stuff (as always) Milan. Rory Blyth did something similar a while back. His approach was not as flexible as yours but still another example for your readers to look at.

http://neopoleon.com/blog/posts/4069.aspx

-Ryan


Bobby
on August 06, 2004

besides the fact that his downloadable project doesnt work, it would almost sound like a cool trick


Milan Negovan
on August 06, 2004

Bobby, did you remember to register the HttpModule in web.config?


Bobby
on August 09, 2004

im using the same web.config file.. this is the error i get:

File or assembly name DynamicCSS, or one of its dependencies, was not found.

Source Error:


Line 8:
Line 9:
Line 10:
Line 11:

Line 12:


Bobby
on August 09, 2004

well, it didnt show the code above.. but it is the lines in the config fo rthe httpHandlers section


no fun
on August 19, 2004

Another (easier?) solution could be to just create a file, dynamic.css.aspx, and change the Content-type.

private void Page_Init(object sender, System.EventArgs e)
{
this.ContentType = "text/css";
}


You could even include something like
<%@ OutputCache Duration="86400" VaryByParam="None" Location="ServerAndClient" %>


...and in the file you might have

const string BODY_COLOR = "black";
...
body {
color: [%=BODY_COLOR%]; // couldn't post tag brackets...
}


James
on October 13, 2004

no fun > I use it every day and even if the first solution is technically interesting, I find the latter easier (few code, no server configuration).

Even I havn't got any "aspx.cs" file, everything is in the "aspx" file.

It helps me to create dynamic stylesheets "on the fly", and does not confuse me with multiple files, because most things are common from one browser to another, except details like font size (mac).

< link rel="stylesheet" href="stylesheet.aspx" >

and that's all for the caller !


Jeremy
on February 03, 2006

Nice article. Seems to be extremely flexible, but I'm wondering if it is completely so. With the variables stuck in the Web.Config, can you use seperate themes for different domains using the same content?

What if you have an application that is using multiple color schemes based on domain names? How can you adjust for having a green-cyan theme for one domain, an orange-gray theme for another domain and a tan-maroon theme for another domain?

Can you just specify different style sheets to take in the variables that you create in the Web.config? Or would you have to change the way you are specifying the CSS vars?


Milan Negovan
on February 07, 2006

Jeremy, I haven't dealt with master pages and skins in 2.0 yet. What I've outlined here works for one site because I wrote it when 2.0 wasn't around. I think it's very feasible to implement this on a skin-by-skin basis, though.


sharad singh
on February 21, 2006

Hi all
This Article is good, but this is not sufficient for me. I'm new to ASP.NET. i understood the code but i didn't understand how to make a request to the CSS from your .aspx file. so i put a link to that CSS in HTML of my page. yeah that started to work, but when you change your color next time, it doesn't change, i figured it out, that its because it requires to refresh the cache. which is not mentioned in this code. it really dont serve my purpose. another thing which i needed is that,that user can change CSS at runtime through httphandler. i want that user of the site can change the css via httphandler at runtime. can someone help me out in this?


David
on August 26, 2006

How come after you set the SuppressContent variable to true, you don't immediately end the request?

if (modifiedSince != null)
{
DateTime dtLastModified = Convert.ToDateTime (fi.LastWriteTimeUtc.ToString ());
DateTime dtModifiedSince = Convert.ToDateTime (modifiedSince);

if (dtLastModified == dtModifiedSince)
{
// The file has not changed
ctx.Response.StatusCode = 304;
ctx.Response.SuppressContent = true;
}
}

sr = fi.OpenText ();
css = sr.ReadToEnd ();
sr.Close ();


Milan Negovan
on August 28, 2006

There are a couple of ways to end a request. I believe the safest way is to call HttpApplication.CompleteRequest (). I simply didn't see need to interfere with the page life cycle.


Priya
on August 29, 2006

I set the context.user to my custom user class in the Global_AcquireRequestState event. This seems to interfere with the ProcessRequest in the HTTPHandler and the CSS is not generated correctly. Everything works great if I remove the line of code that sets the context.user. Any ideas on how I can overcome this behavior? Any help would be greatly appreciated!


Larry
on January 08, 2008

I always read these types of articles with relish, until I get to the inevitable end - "now we just have to configure IIS" - or something like that. I don't know how many new controls and methods that come with asp.net 2.0 that are completely useless because my site is hosted on a server I have no control over. I've already encountered two or three of them. No hosting company is going to make special allowances for each client.

Sigh...


Milan Negovan
on January 09, 2008

Larry, I feel your pain. When it comes to configuring IIS, you have to jump through hoops to get the hosting company to cooperate.

Ask them. What's described in the article is quite a trivial task and it's their job to help you.


Bull
on September 26, 2009

Interesting article. All replacements works fine. I've tried both substituting from web.config and an own separate file. Smooth and solid. This actually works:)
I do though have one question: my CssHandler-class seems to be executed before "anything else". So, if I produce variable names + values somewhere "out on the page", makes it accesible to the whole application / page and then tries to substitute DynamicCSS-vars in the stylesheet, my "runtime vars" I've produced is not available.

Som what I would like to know, is WHEN the CssHandler class is executed and IF it is possible to fire it later ...


Milan Negovan
on September 28, 2009

The timing of the handler is set, really. Take a look at the lifetime of a request (the diagram at two thirds down the page).