mostlylucid

scott galloway's personal blog...
posts - 916, comments - 758, trackbacks - 11

My Links

News

Archives

Post Categories

Misc. Coding

Little hack: CSS Combiner / Minifier ASP.NET Control

CAUTION: When I say hack I mean it...this is in NO WAY production ready...it's just a sample at this stage. If it causes your dog to explode, don't blame me!

I thought I'd throw a work in progress online for people to have a play with. This is one of my little pet projects (I'm not allowed to write code at work...not a dev as I'[m always reminded!).

So, what does it do?
 
Well, one of the major performance issues for current websites is the number of requests they make back to the server, not the actual server-side processing time. When I first wrote about this after reading Steve Sounders excellent  'High Performance Websites' it was frankly a surprise to me!  Well, anyway recently I've been playing with a number of ASP.NET server control which aim at making optimizing the number of server calls an ASP.NET app has to make much fewer. The first of these is the (as yet) unpublished CSS Sprite Control. Below is the beginning of the second; this was written in a couple of hours last night as I couldn't sleep for thinking about how it might work. The code is VERY rough but it demonstrates the concept pretty well.
In essence I wanted the minimum effort possible to combine and compress multiple <link rel="stylesheet"> CSS file entries together, minimize them using YUI Compressor (for .NET...on Codeplex) and output a time dependent (in this case, Date Modified for the file) hash for the file name (in a fixed location at the moment, "~/OutputCSS/"), then output a tag in the page for this new file.
 
So, how does it work?
 
Well, imagine you had a page with some stylesheet definitions like this:
 
   1: <link type="text/css" rel="stylesheet" href="../../../build/logger/assets/logger.css"/>
   2: <link type="text/css" rel="stylesheet" href="../../../build/yuitest/assets/testlogger.css"/>        
   3: <style type="text/css">
   4: #container, #container2 {
   5:     width: 400px;
   6: }
   7:  
   8: .yui-carousel-element {
   9:     margin: 0;
  10:     padding: 0;
  11: }
  12:  
  13: .yui-carousel-element li {
  14:     border: none;
  15:     margin: 0;
  16:     padding: 0;
  17:     width: 100px;
  18: }
  19: </style>

 

As you can see we have two CSS file definitions and a style tag. Wouldn't it be nice if we could combine and minify these...well...here's how my little hacky control lets you do it:

 

   1: <asp:CSSManager runat="server">
   2:         <link type="text/css" rel="stylesheet" href="~/build/logger/assets/logger.css"/>
   3:         <link type="text/css" rel="stylesheet" href="../../../build/yuitest/assets/testlogger.css"/>        
   4:         <style type="text/css">
   5:         #container, #container2 {
   6:             width: 400px;
   7:         }
   8:  
   9:         .yui-carousel-element {
  10:             margin: 0;
  11:             padding: 0;
  12:         }
  13:  
  14:         .yui-carousel-element li {
  15:             border: none;
  16:             margin: 0;
  17:             padding: 0;
  18:             width: 100px;
  19:         }
  20:         </style>
  21: </asp:CSSManager>

 

You can see in the snippet above that we just wrapped the CSS definitions in <asp:CSSManager>...</asp:CSSManager> tags...that's all there is to it! One other thing, the observant will have noticed that I can now use a ~/ in the href...it's a nice side effect of this method...lets you avoid the crazy path traversal stuff (../../../)

Obviously this is still a bit limited, it doesn't work in Partial Trust, doesn't handle multiple scopes; say, Site level CSS, Page Level CSS and Masterpages where you'd want multiple sheets with different contents. Plan is to keep working on this for a little while...so I will almost certainly update this soon.

 

Source follows...

 

   1: using System;
   2: using System.Collections.Generic;
   3: using System.ComponentModel;
   4: using System.Text;
   5: using System.Web.UI;
   6: using System.Web.UI.HtmlControls;
   7: using System.IO;
   8: using System.Security.Cryptography;
   9: using Yahoo.Yui.Compressor;
  10:  
  11: namespace CSSManager
  12: {
  13:     [DefaultProperty("Text")]
  14:     [ToolboxData("<{0}:CSSManager runat=server></{0}:CSSManager>")]
  15:  
  16:     [ParseChildren(true, "cssFiles")]
  17:     public class CSSManager : Control, INamingContainer
  18:     {
  19:  
  20:         public const string FILE_PATH = "~/OutputCSS/{0}.css";
  21:  
  22:         public CSSManager() { }
  23:  
  24:         public List<HtmlGenericControl> CSSFiles
  25:         {
  26:             get;
  27:             set;
  28:         }
  29:  
  30:  
  31:         private StringBuilder combinedSheets = new StringBuilder();
  32:  
  33:         public string GetStyleSheet(string virtualPath)
  34:         {
  35:             string filePath = Context.Server.MapPath(virtualPath);
  36:             if (!string.IsNullOrEmpty(filePath))
  37:             {
  38:                 return File.ReadAllText(filePath);
  39:             }
  40:             return string.Empty;
  41:  
  42:         }
  43:  
  44:         public string CalculateFileHash(string filePaths)
  45:         {
  46:             MD5CryptoServiceProvider csp = new MD5CryptoServiceProvider();
  47:             byte[] pathBytes = csp.ComputeHash(System.Text.UTF8Encoding.UTF8.GetBytes(filePaths));
  48:             return BitConverter.ToUInt64(pathBytes, 0).ToString();
  49:  
  50:         }
  51:  
  52:         public void WriteFile(string textToWrite, string fileHash)
  53:         {
  54:             File.WriteAllText(Context.Server.MapPath(string.Format(FILE_PATH, fileHash)), textToWrite);
  55:         }
  56:  
  57:         public string CreateDatedFilePath(string filePath)
  58:         {
  59:             FileInfo fi = new FileInfo(Context.Server.MapPath(filePath));
  60:             if (fi.Exists)
  61:             {
  62:                 return fi.LastWriteTimeUtc.Ticks.ToString() + "#" + filePath;
  63:             }
  64:             return string.Empty;
  65:  
  66:  
  67:         }
  68:  
  69:         protected override void CreateChildControls()
  70:         {
  71:  
  72:             if (this.DesignMode == true)
  73:             {
  74:                 foreach (var file in CSSFiles)
  75:                 {
  76:                     string rel = file.Attributes["rel"];
  77:  
  78:                     string href = file.Attributes["href"];
  79:  
  80:                     if (rel.ToLowerInvariant() == "stylesheet")
  81:                     {
  82:                         file.Attributes["href"] = this.ResolveClientUrl(Context.Server.MapPath(href));
  83:                         this.Controls.Add(file);
  84:                     }
  85:  
  86:                 }
  87:             }
  88:             StringBuilder filePaths = new StringBuilder();
  89:             int i = 0;
  90:             foreach (var file in CSSFiles)
  91:             {
  92:                 string tagName = file.TagName.ToLowerInvariant();
  93:                 if (tagName == "link")
  94:                 {
  95:                     string rel = file.Attributes["rel"];
  96:                     string href = file.Attributes["href"];
  97:  
  98:                     if (rel.ToLowerInvariant() == "stylesheet")
  99:                     {
 100:                         string styleSheet = GetStyleSheet(href);
 101:                         if (!string.IsNullOrEmpty(styleSheet))
 102:                             combinedSheets.Append(styleSheet);
 103:                         filePaths.AppendFormat("{0}#", CreateDatedFilePath(href));
 104:                     }
 105:                 }
 106:                 else if (tagName == "style")
 107:                 {
 108:                     string inner = file.InnerHtml;
 109:                     combinedSheets.Append(inner);
 110:                     filePaths.Append("CSSSTYLE_" + i);
 111:                     i++;
 112:                 }
 113:  
 114:             }
 115:             string minimizedStyles = CssCompressor.Compress(combinedSheets.ToString(), 0, CssCompressionType.Hybrid);
 116:             string fileHash = CalculateFileHash(filePaths.ToString());
 117:             WriteFile(minimizedStyles, fileHash);
 118:  
 119:             HtmlGenericControl gen = new HtmlGenericControl();
 120:             gen.TagName = "link";
 121:             gen.Attributes.Add("href", this.ResolveClientUrl(string.Format(FILE_PATH, fileHash)));
 122:             gen.Attributes.Add("rel", "stylesheet");
 123:             this.Controls.Add(gen);
 124:         }
 125:  
 126:     }
 127: }

Print | posted on Friday, April 24, 2009 1:59 PM | Filed Under [ ASP.NET Code Snippets ]

Feedback

Gravatar

# re: Little hack: CSS Combiner / Minifier ASP.NET Control

Nice post...

4/27/2009 4:35 AM | Jones

Gravatar

# re: Little hack: CSS Combiner / Minifier ASP.NET Control

A better way is to deliver your data from multiple domains, since this will make the users browser DL more files simultaneously.
See this post for more info: blogs.msdn.com/ie/archive/2005/04/11/407189.aspx

4/28/2009 8:20 AM | Bongo Bong

Gravatar

# re: Little hack: CSS Combiner / Minifier ASP.NET Control

This is very interesting, i have been looking into how to minimize the number of request to the server from a .net application, i willl try and implement something similar in one of my test projects... thanks

8/15/2009 3:13 AM | cheesy

Comments have been closed on this topic.

Powered by: