Implementing Constants in Cascading Style Sheets in ASP.Net 2.0 using HTTPHandlers
By Nagi Punyamurthula (nagibabu@microsoft.com)
Many times I have found myself messing with my CSS files where I usually have anywhere between 50-120 different elements and each with many attributes that define things like colors, fonts, etc.. Predominantly, elements’ attributes within my CSS files have repetitive values that follow a certain theme or style; that warrant that these values are applied across to many elements . If you're like me, you'd hate spending your time on the mundane task of tweaking these settings every time you decide to change the preferences. Many times, I find myself spending hours together figuring out my "new" preferences and then re-applying those to the many elements’ attributes in my CSS files. What a waste of time and resource!
As I was finalizing my photo blogging engine fstop (fstop.crosscity.com), I was all the motivated to finding a solution to this problem - to making the CSS Editor's life a bit easy and So, the "HTTP Handler to CSS” was born.
In this article, I will attempt to describe what is involved in creating your own CSS Handler so you have an understanding of what it is and how you can build one yourself. Of course, you have the full access to my source code by following the link provided later on in this article.
Why and What?
ASP.Net 2.0 provides some very powerful development tools and artifacts to help developers with building state-of-the-art web applications. The CSS theming and skinning developer capabilities among the many, are out of the world features that make ASP.Net 2.0 a web developers’ best friend. As with many product cycles, a few features do not make it to the platform feature considerations. CSS Constants happens to be just one among those.
Imagine having the ability in your CSS file to define "constants"; You'd apply “values” to the constants. The attributes within the class names, IDs, will thereafter, 'reference' these constants (not the values, only the name of the constants). Later on, when you decide to change your preferences, simply change the value of the constants, and you're all set to go. That would save me some serious time!
So how my constant declarations in my CSS will be recognized by my browser?
Now, that’s a very good question. This article is all about answering that and more. In a nutshell, at runtime, the CSSHandler that we’re going to build, will intercept calls to the CSS file, pick it up and process the constants and references to it and finally render a CSS content that looks like a regular CSS content. In other words, our CSS Handler will replace all references to constants with the corresponding defined values.
Let’s get started
Here's a 50K foot plan:
1. Build an HTTPHandler to intercept .CSS extension
2. Our handler will look for the custom block of content within the CSS – let’s call it “Constants Definitions Region” (CDR). We will determine later on, what the syntax will be to defining these constants within this region.
3. Our handler will look for the references to the constants and replaces those with the actual value that constant was assigned (in the CDR above).
4. The processed CSS file content is rendered back to the calling client or browser
5. To save the CPU from re-processing the same CSS file and to save the disk I/O, we will store the processed CSS file content into the Server Cache for later re-use. This will save the latency on subsequent calls to the CSS file and to the overall web pages rendering experience. Once we complete our handler, we will enable it in our sample project by editing the web.config file.
Before we kick the pedals...
There's a link to my fully functioning example project at the very end of this article; please feel free to play with it, modify per your needs, or whatever you need to do with it. It is yours, it is free, It used to be mine alone thus far, now I’ll let you use it.
RESOURCES and TIPS
Before we start our project, I wanted to point out a couple of good resources that you might find useful. In our project, we will use a couple of Regular Expressions in our code. A few good resources exist today. These worked for me. They may or may not for you. But, here's my list of these resources:
· A great online site dedicated to helping folks like us with understanding Regular Expressions : http://regexlib.com/. Has a great tool to test your Reg Exps.
· A well written quick reference: Regular Expressions with .NET, by Dan Appleman (ISBN: B0000632ZU)
Now let’s dive into the details:
First things first. I briefly mentioned that our CSS file will contain a “special” block of content called, “Constant Definition Region” (CDR). CDR will contain a list of Constants and the assigned values. We are at a point where we will need to determine what the syntax is for this CDR.
1. Create a default.css file using your favorite editor (I use Microsoft-Visual Studio 2005).
a. Add the <CDR> tag to the top of the CSS file as follows:
|
<CDR>
/* Define your constants here */
BodyBGColor :#696969;
BodyFGColor :#FFFFFF;
DefaultFont :verdana;
DefaultFontSize :7pt;
</CDR>
|
This section defines our constants and the assigned values . A couple of things to note here. The opening and closing <CDR> tags are important to enclose the CDR block. The constant name followed by a colon “:” and the assigning value. The constant name is not case sensitive, at least the one that we’re going to be building.
|
/* Body */
Body{
background-color : @BodyBGColor;
color : @BodyFGColor;
margin-top : 10px;
margin-bottom : 10px;
padding : 0px;
text-align : center;
font-family : @DefaultFont;
font-size : @DefaultFontSize;
}
|
b.
Now that we have our CSS Constants defined as above, let’s add style to our Body by adding the follow. Note how we’re referencing the constants in place of the actual values to the attributes. Note the “@” symbol to prefix the constant name.
This is going to be our syntax of 1) defining the CDR and 2) referencing those constants in the remaining of the CSS element/attributes.
Our default.css file is now ready to be processed. Now let’s jump on to step 2 in our 50K foot plan.
2. In this step, we will be building an ASP.Net 2.0 HttpHandler class that knows how to intercept calls to our default.css and how to process it. Now that our default.css is available with its <CDR> block, we’re now ready to create our CSS Handler that will do just that.
a. Open Visual Studio 2005 and add a new class called, "CSSHandler.cs”
Since we need to intercept calls to and process our default.css file, we need to implement what is called an “HttpHandler”.
What is an “Handler” and What is an “HttpHandler” ?
ASP.Net 2.0 exposes a mechanism for developers to build “handlers” to be able to implement their implementation of responses to resources with filename extensions. For example, suppose when you call a “default.aspx”; the call is handled by the ASP.Net 2.0’s ASPNet Worker or the ASPNet_ISAPI.dll. The call to the default.aspx is intercepted by this dll, processed and output is produced and rendered by it back to the calling client or the browser that initiated the call.
Note: if you want to view where this mapping is registered, open up the properties dialog in your IIS server for a web site, click on the “Home Directory” tab, click on the “Configuration” button and then onto the “Mappings” tab on the resulting configuration dialog.
ASP.Net 2.0, by default, does not have a mapping to process files with “.CSS”. Which means, by default, when a browser or a client calls for “default.CSS”, ASP.Net simply hands out the content “AS IS” back to the caller. No special processing is done before handing out the content to the caller.
In our “custom” default.css’ case however, we want to intercept that call so that we can process and treat the “CDR” segment and render the content so that all constants references are replaced with the constant-values. To do exactly that, we need to build an “HttpHandler” based class that is mapped to handle “.CSS” files.
Within the CSSHandler.cs, add the following to ensure you’re implementing the IHttpHandler Interface. Doing this will be one of the steps to enable your class to intercept calls to the “.CSS” GET requests.
As you add the following, pay extra attention to the Namespace; we will come back to that later on in this article.
|
/* Pay extra attention to this namespace. We will come back to this later. */
namespace Utils{
public class CSSHandler : IHttpHandler {
// ::IsReusable is not applicable to our application. However, we
// will do due diligence to our coding best practices.
public bool IsReusable{
get { return true; }
}
// ::ProcessRequest() is the work-horse of our handler.
// He gets called everytime someone or page or server component
// requests
// the default.css file via HTTP GET request
public void ProcessRequest(System.Web.HttpContext context) {
}
} |
The code block above is pretty much a standard "template" for all HTTPHandlers. The next steps will go into more specific code blocks that mean something to our CSS Handler.
Every HTTPHandler needs a ::ProcessRequest()
The ::ProcessRequest() method will get called by asp.net worker process every time, a HTTP GET request for “.CSS” file comes in. It is the responsibility of this method to respond to that request, do whatever needs to be done and then respond back to the request with the content. In our case the result of the processed default.css file.
Note: We keep using the terms, "processed” default.css. It is the "final" content of the default.css file that has the replaced constant references with the actual value.
In the ::ProcessRequest() we will need to make the required additions to process the “default.css” file. Let’s add the following code to the ::ProcessRequest() method.
|
try{
// Get the fullpath to the default.css file from the REQUEST that is coming in.
string __SourceCSSFile = context.Request.PhysicalPath;
// Step : 2.1
// To be able to process this css file, it must first exist in the file system.
// We must ensure that it does before proceeding. If does not exist, return - we can't do much.
if (!File.Exists(__SourceCSSFile))
// we don’t have a a physical copy of the requested “.css” file.
// we will simply return;
return;
// just a holder to hold our content in this proc
string __ProcessedContent = "";
// Per our 50K foot plan above, we are going to store the processed CSS file content in
// the Cache. Cache (like Session) stores stuff in the "name:value" pairs.
// To store our CSS content, we will use, "CSS_CONTENT" for name.
const string __CACHE_TOKEN_NAME = "CSS_CONTENT";
// Now check if we have stuff in the cache that we already processed.
//
// And, if we do, we will extract that content from the cache and spit that out as our result.
// Step : 2.2
if (context.Cache[__CACHE_TOKEN_NAME] != null){
__ProcessedContent = context.Cache[__CACHE_TOKEN_NAME].ToString();
}
else{
// Read the css file using the full path into the variable we defined above
StreamReader __StreamReader = new StreamReader(__SourceCSSFile);
__ProcessedContent = __StreamReader.ReadToEnd();
__StreamReader.Close();
// Step: 2.2.1:
// Remove comments
__ProcessedContent = _RemoveComments(__ProcessedContent);
// Steps: 2.2.2:
__ProcessedContent =
_SearchReplaceConstantsWithValues(
__ProcessedContent, __CACHE_TOKEN_NAME, __SourceCSSFile
);
// Steps: 2.2.3:
_StickContentIntoCache(__ProcessedContent, __CACHE_TOKEN_NAME, __SourceCSSFile);
}
// Step: 2.3 (We will revisit this - see below):
context.Response.Write(__ProcessedContent);
} // </EO try { } block
// our try { } was successful
// If on the other hand, it failed, we will simply spit out the error in our response.
catch (Exception ex){
context.Response.Write(ex.Message);
}
} // </EO ProcessRequest>
|
There are a few things going on in this above code block. Let’s take these one step at a time and try to describe what we’re doing here.
Note: To better able to follow the steps, within the code block, I have commented with the step label. We will use these to help describe the inner workings.
Step 2.1:
Within the try block, we will first check if we have the access to the physical file that is being requested. Suppose we get a request for “default.css”, we will use the full path to the “default.css” file and see if it exists. If it does not exist, there is no point in proceeding forward – we will simply return.
Step 2.2:
If you look back in our 50K foot plan above, in step 5 there, we decided that we will store the already processed default.css content in the cache so as to save of Server I/O and CPU cycles.
Note: Before we start processing the default.css file, we want to check if our cache already has the content in it. If we do, we will simply return the content from the cache to the caller. If not, we will process the default.css file, stick the processed content in the cache and then return the cached content to the caller - pretty straight forward stuff.
|
// Now check if we have stuff in the cache that we already processed.
//
// And, if we do, we will extract that content from the cache and spit that out
// as our result.
// Step : 2.2
if (context.Cache[__CACHE_TOKEN_NAME] != null){
__ProcessedContent = context.Cache[__CACHE_TOKEN_NAME].ToString();
} |
We will use our constant, “__CACHE_TOKEN_NAME “ as a key to store and extract the content from our cache. In this step, we will check if our cache has any preprocessed content that we can extract and send back to the caller. If it does, we will pick that up in the “__ProcessedContent” variable. If we don’t have a preprocessed content in our cache, we will need to process the default.css file.
|
else{
// 2.2.1
// Read the css file using the full path into the variable we defined above
StreamReader __StreamReader = new StreamReader(__SourceCSSFile);
__ProcessedContent = __StreamReader.ReadToEnd();
__StreamReader.Close();
// 2.2.2
// Remove comments
__ProcessedContent = _RemoveComments(__ProcessedContent);
// 2.2.3
__ProcessedContent =
_SearchReplaceConstantsWithValues(
__ProcessedContent, __CACHE_TOKEN_NAME, __SourceCSSFile
);
// Steps: 2.2.4
_StickContentIntoCache(__ProcessedContent, __CACHE_TOKEN_NAME, __SourceCSSFile);
} |
We will follow these steps – use the labels in the code to follow the steps here.
2.2.1 Read the physical file into memory for processing
2.2.2 Remove the comments within the <CDR> segment in the default.css file via the call to “_RemoveComments()”. Comments are enclosed within “/* */” markers.
2.2.3 Now, search for references to the constant names within the remainder of the CSS file and replace those with the values of the defined constants via the call to “_SearchReplaceConstantsWithValues()”.
2.2.4 Once the default.css file is processed, we will stick the processed content into the Cache for subsequent uses via the call to – “_StickContentIntoCache()”.
We will go into the details of each of these methods in detail in the following sections.
By the end of step 2.2.4, our processed content must be available in the cache for future use. The next step is simply to respond back to the caller with the content from the cache file. Step 2.3 does exactly that via the call to ::Response.Write().
|
// Step: 2.3 (We will revisit this - see below):
context.Response.Write(__ProcessedContent);
} // </EO try { } block |
Now what if the try… failed, here's the code that falls within the "catch…" block above.
|
// If our try{} block failed, we will simply spit out the error in our response.
catch (Exception ex){
context.Response.Write(ex.Message);
} |
That covers our main code. In the following sections, I will describe each of the methods that are called by the above code block. And, then we will be done and ready to deploy and see it work.
Let’s look at the ::_RemoveComments(). We will use the following RegEx match string and replace “/* … */” with an empty string - so the comment markers and anything in between is removed in the final output. That was simple enough.
|
// What we're doing here is simplt search replace for comments and
// replace with "" using Reg Exp.
private string _RemoveComments(string __SourceCSSContent){
// Many thanks to Regular-Expression.info site for this expression
// to get all content btwn the "/*" and "*/"
return Regex.Replace(
__SourceCSSContent, @"/\*.+?\*/", "", RegexOptions.Singleline);
}
|
|
// This where the real work is getting done. One of our modern "@define" stylesheets
// comes in this end, and goes out the other in a format that a browser will understand.
private string _SearchReplaceConstantsWithValues(
string __SourceCSSContent,
string __CACHE_TOKEN_NAME,
string __FQPhysicalCSSFilePath
) {
// Many thanks to Regular-Expression.info site for this expression
// to get all content btwn the <CDR> tags
const string __REToCaptureEverythingBetweenCDRTag = "<CDR[^>]*>(.*?)</CDR>";
// Step 3.1
// Using the above expression, get the matched string
Regex __RE = new Regex(@__REToCaptureEverythingBetweenCDRTag, RegexOptions.Singleline);
Match __REMatch = __RE.Match(__SourceCSSContent);
// If we did *NOT* find the <CDR> tag, we no need to proceed forward,
// simply exit
if (!__REMatch.Success)
return __SourceCSSContent;
// Step 3.2
// Remove the CDR header from the css content.
// This will not be needed in the resp write
__SourceCSSContent = __SourceCSSContent.Replace(
(string)__REMatch.ToString(), (string) "").Trim();
// Step 3.3
// If you got this far, we must have found the CDR Tags and
// hopefully we also got some defs string in there.
// if we found more than one constant:value pairs, the return value
// from the ::Match method (above),
//
// results in a single string with all of the constant:value pairs
// - each separated by ";"
//
// To be able to individually process each of the constant:value pairs,
// we will split the mushed up Constant:Value pairs string into individual
// items using the split against ";"
string[] __CDRDefs = __REMatch.Groups[1].Value.Split(new char[] { ';' });
// at the end of this, we will get an array of individual array items
// representing "<constant>:<value>" string
//
// just a holder to help w/ search/replace
StringBuilder __SB = new StringBuilder(__SourceCSSContent);
// Step 3.4
// We will now start searching for each of the constants in the __CDRDefs in the
// default.css content and replace with the value defined for the
// constant in question
foreach (string __CDRDef in __CDRDefs){
// proceed only if we have a valid non-empty <constant:value> pair
if (__CDRDef.Trim() != string.Empty) {
// __CDRDef will contain text in the format of "<ConstantName>:<Value>" pair
//
// We will split that into a Name:Value pair array as follows using split
// against ":" so we can address the name and value separately.
string[] __ConstantValuePair = __CDRDef.Trim().Split(new char[] { ':' });
// Now search in the css content for "@<ConstantReference>" and replace it
// with the 'Value' component found in the "Name:Value" pair
__SB.Replace("@" + __ConstantValuePair[0].Trim(), __ConstantValuePair[1].Trim());
} // repeat for loop for the next <constant>:<value> pair element in the __CDRDefs array.
}
// Step 3.5
// at this stage, all of the constant references found in the <CDR>
// block must have been serviced. simply return the "post-processed" string content.
return __SB.ToString();
} // </EO: _SearchReplaceConstantsWithValues> |
Now that that’s out of the way, let’s spend some time understanding the next method _SearchReplaceConstantWithValues(). Off all the other methods, this method is the most meatiest. I have again, labeled individual code blocks to help us follow through.
Before we begin, let’s ensure we understand the purpose of this method. Its sole purpose in life is to look for references to constants in the body of the CSS file and replace those references with the actual values that are defined in the CDR segment.
So, let’s get rolling.
Step 3.1
Our first step here is to get all of the CDR content segment from the default.css file. That is, all constants/values - name/value pairs. We are able to do this by leveraging the RegEx again w/ the pattern - "<CDR[^>]*>(.*?)</CDR>".
Step 3.2
If we did not find any content, simply return. We don’t have anything to process – it is as simple as that. If on the other hand, we find something, we can now remove the CDR opening and closing tags from the found segment. A simple ::Replace() will do that trick.
Step 3.3
If you look in the CDR segment above, each of the constant/value pairs are separated by a “;”. We will now split those constant/value pairs into individual array elements. A simple call to the ::Split() should do that trick. It is worth noting that we are calling ::Split() against the __REMatch object that we populated earlier on in step 3.2 above. This object was populated when we called the ::Match() method.
|
// To be able to individually process each of the constant:value pairs,
// we will split the mushed up Constant:Value pairs string into individual
// items using the split against ";"
string[] __CDRDefs = __REMatch.Groups[1].Value.Split(new char[] { ';' });
|
Step 3.4
This is the fun part of this method. It start out by separating the “constant” from the “value” using “:” as the separator does a full text search/replace for each of the constant name occurrences. The following code does this.
|
foreach (string __CDRDef in __CDRDefs){
// proceed only if we have a valid non-empty <constant:value> pair
if (__CDRDef.Trim() != string.Empty) {
// __CDRDef will contain text in the format of "<ConstantName>:<Value>" pair
// We will split that into a Name:Value pair array as follows using split
// against ":" so we can address the name and value separately.
string[] __ConstantValuePair = __CDRDef.Trim().Split(new char[] { ':' });
// Now search in the css content for "@<ConstantReference>" and replace it
// with the 'Value' component found in the "Name:Value" pair
__SB.Replace("@" + __ConstantValuePair[0].Trim(), \
__ConstantValuePair[1].Trim());
}
// repeat for loop for the next <constant>:<value> pair element
// in the __CDRDefs array.
} |
The resulting string is a fully processed CSS content string that we can return back to the caller – Step 3.5.
We are almost done with running down our methods. Here’s the last one - _StickContentIntoCache(). It does exactly that – it takes the fully processed CSS content string and persists into the cache for future use. Since Cache is volatile, it will be gone when the server is rebooted or IIS is restarted – I’m sure you’re aware of that already. So, the default.css file will be processed every time the cache is empty.
The key to understand here is, we have a processed css content that we want to remember for future incoming requests. We want to keep this content in the cache up to date with the latest default.css file. What does that mean? It means that if you happen to modify the default.css file with any changes, we want our handler to be able to reprocess the file and create a new image of the processed css content in the cache. Makes sense?
So how are we going to check if the default.css file changed and have us process the file again. It is very simple – fortunately, we have ASP.Net 2.0’s CacheDependency() object. CacheDependency object allows us to bind the cache content with a certain physical file. So that when the file happens to change, the cache is automatically tagged as invalid or null’ed. Pretty cool – huh?!
Note: It is important to note that we are passing the CacheDependency object to the Cache::Insert() method. That’s the glue that binds the cache content with the physical file (in our case default.css).
|
// this method sticks the fully post-processed css content into
// the cache file so, we don't need to redo what we did above
// for every single request to default.css that comes in.
// It saves a good quality server cycles.
private void _StickContentIntoCache(
string __PostProcessedCSSContent,
string __CACHE_TOKEN_NAME,
string __FQPhysicalCSSFilePath){
// we will use the .Net 2.0's ::CacheDependency object.
// We will establish a dependency btwn the cached post css content
// and the default.css physical file.
//
// The benefit to that?
// Yes, if the you decide to change the file and save it, the dependency tells us
// that the "post-processed" content in the ::ProcessRequest method does NOT exist.
// Thereby allowing us to re-proc and re-gen the new content.
//
// Very cool ASP.Net feature!
//
CacheDependency __CD = new CacheDependency(__FQPhysicalCSSFilePath);
// Save into Cache using the above CD and cache token name
HttpContext.Current.Cache.Insert(__CACHE_TOKEN_NAME, __PostProcessedCSSContent, __CD);
} // </ EO ::_StickIntoCache> |
We’re now done with our coding. Compile the code and generate the resulting DLL.
Recall, when we were writing our handler, we paid extra attention to the namespace of the class. Keep an eye on that namespace name. We will be using that now.
Now, let’s Enable our HttpHandler
Step 1:
Open your web.config file and add the below:
|
<pages styleSheetTheme="A"/> |
Step 2:
add a new entry into the <httphandlers> node as follows. If you don’t have an existing <httphandlers> section, create one:
|
<httpHandlers>
<add verb="GET" path="default.css" type="Utils.CSSHandler"/>
</httpHandlers> |
In the above httpHandlers entry, pay attention to the
· “verb” attribute.
· We are asking the ASP.Net worker to intercept and process all HTTP GET calls to the “default.css” file. We are not asking it to intercept the PUT verb, but, ONLY the GET verb. In your application, you can build a behavior for PUT if that need exists. In ours, it simply does not exist.
· “path” attribute.
o We are asking our ASP.Net worker that our handler will ONLY process "DEFAULT.CSS" file (so, if you had a TEST.CSS, we won’t bother picking it up). In these cases, the text is rendered back as-is.
· “type” attribute
o We are asking our ASP.Net worker that our handler where to look for the handler. This is the FULLY QUALIFIED namespace.class name (in our case, it is “Utils.CSSHandler”) . THIS IS IMPORTANT. IF THIS IS NOT SET PROPERLY, ASP.NET WILL FAIL TO LOAD.
Once you have done the above, we are good to go. Run any page in your project that uses the default.css file.
I have attached the fully working project here. You can unzip the attached project and run it.
Summary:
Hope this article was helpful to you. I have attempted to explain how to enable "constants" in CSS behavior. Constants or variables are NOT inherent to CSS. But, our handler will extend that cool feature to your web applications.
ciao, Nagi