Less for .Net (or .Less, or DotLess, or Less, etc.) is a pretty cool way to make super-wet (or super-lubed?) stylesheets a little bit more, eh, DRY. To give you a quick taste:
You can declare css variables:
@nice-blue: #5B83AD;
@light-blue: @nice-blue + #111;
#header { color: @light-blue; }
You can declare “mixins”:
.bordered {
border-top: dotted 1px black;
border-bottom: solid 2px black;
}
#menu a {
color: #111;
.bordered;
}
Here’s my favorite. You can use operators:
@base: 5%;
@filler: @base * 2;
@other: @base + @filler;
color: #888 / 4;
background-color: @base-color + #111;
height: 100% / 2 + @filler;
Has your appetite been “dried”? If so, you can check out the official documentation here. Also, a handful of other MVC guys have betrayed me and expunged their own blog posts that (much more eloquently) divulge the intricacies of using Less in ASP.Net, most notably Phil Haacked and K. Scott Allen. Feel free to click on those links and never return to my blog, okay, love you, miss you, bye.
Moving On
Now that we’re alone, and now that we know some of the cool features of Less, we can move on to the meat of this post. Below, we’ll be building off of a previous post, where we created a ZipController that handled the writing of our javascript and stylesheet files. In particular, we’ll be adding functionality to our ZipController that will allow for the inclusion (and “translation”) of .less files. For example, we’ll be able to write this:
<head>
<% Html.Style("/public/styles/master.css"); %>
<% Html.Style("/public/styles/company.less"); %>
<%= Html.CompressCss(Url) %>
</head>
And, as a result, a single cached, mashed .css file will be created for our page.
Okay. To begin, let's create a simple FileData class. You could also use IO.FileInfo for this task, but let's keep this class light and flexible for now:
namespace Fat.Mvc.Helpers.IO
{
public class FileData
{
public string Path { get; set; }
public string Text { get; set; }
}
}
Let's also create an IContextCache interface:
using System;
namespace Fat.Mvc.Helpers.Caching
{
public interface IContextCache
{
void Insert(string key, object value, string pathDependency, TimeSpan slidingExpiration);
void Insert(string key, object value, string[] pathDependencies, TimeSpan slidingExpiration);
object Get(string key);
}
}
And a ContextCache class, which we'll use as a wrapper class for testable access to the HttpRuntime.Cache. I probably should have created this class (and the interface) in my last post, or in my second-to-last post, but alas, laziness and stupidity and the sexual chocolate of love all got to me first:
using System;
using System.Web;
using System.Web.Caching;
namespace Fat.Mvc.Helpers.Caching
{
public class ContextCache : IContextCache
{
Cache cache;
public ContextCache(Cache cache)
{
this.cache = cache;
}
public void Insert(string key, object value, string pathDependency, TimeSpan slidingExpiration)
{
cache.Insert(key, value, new CacheDependency(pathDependency), Cache.NoAbsoluteExpiration, slidingExpiration);
}
public void Insert(string key, object value, string[] pathDependencies, TimeSpan slidingExpiration)
{
cache.Insert(key, value, new CacheDependency(pathDependencies), Cache.NoAbsoluteExpiration, slidingExpiration);
}
public object Get(string key)
{
return cache.Get(key);
}
}
}
Last but not least, here's the updated ZipController. I know that the file is getting a little "large" (lo siento), so I'll offer up a demo download at the end of this tutorial:
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Caching;
using System.Text;
using System.Security.Cryptography;
using System.Linq;
using Fat.Mvc.Helpers.IO;
using Fat.Mvc.Helpers.Caching;
using System.IO;
using System.IO.Compression;
using dotless.Core;
using dotless.Core.configuration;
namespace Fat.Mvc.Web.Controllers
{
public class ZipController : Controller
{
IFileBase file;
IContextCache cache;
const int cacheForMins = 60;
public ZipController() : this(null, null) { }
public ZipController(IFileBase fileBase, IContextCache cache)
{
this.file = fileBase ?? new FileBase();
this.cache = cache ?? new ContextCache(HttpRuntime.Cache);
}
byte[] GetBytesFromCache(string key)
{
object fromCache = cache.Get(key);
if (fromCache == null) return null;
try
{
return fromCache as byte[];
}
catch
{
//the object in the cache wasn't a byte array
//that's sad and perplexing...
return null;
}
}
public virtual void Image(string path)
{
var server = HttpContext.Server;
string decodedPath = server.UrlDecode(path);
string mappedPath = server.MapPath(decodedPath);
byte[] imageBytes = GetBytesFromCache(mappedPath);
if (imageBytes == null)
{
imageBytes = file.ReadAllBytes(mappedPath);
cache.Insert(
path,
imageBytes,
mappedPath,
TimeSpan.FromMinutes(cacheForMins));
}
Response.AddFileDependency(mappedPath);
Response.ContentType = "image/jpeg";
Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetExpires(Cache.NoAbsoluteExpiration);
Response.Cache.SetLastModifiedFromFileDependencies();
Response.OutputStream.Write(imageBytes, 0, imageBytes.Length);
Response.Flush();
}
string MD5Fingerprint(string s)
{
var bytes = Encoding.Unicode.GetBytes(s.ToCharArray());
var hash = new MD5CryptoServiceProvider().ComputeHash(bytes);
// concat the hash bytes into a long string
return hash.Aggregate(new StringBuilder(32),
(sb, b) => sb.Append(b.ToString("X2"))).ToString();
}
void Text(string path, string type, Action filter)
{
//decode and map paths in path string
var server = HttpContext.Server;
string decodedPath = server.UrlDecode(path);
string[] paths = decodedPath.Split('|').Select(m => server.MapPath(m)).ToArray();
//determine whether response can be gzipped
string encodingHeader = (Request.Headers["Accept-Encoding"] ?? string.Empty).ToUpperInvariant();
bool gzip = (encodingHeader != null && (encodingHeader.Contains("GZIP")));
string encoding = gzip ? "gzip" : "utf-8";
//create a cache key for the returned bytes
string key = MD5Fingerprint(decodedPath + encoding);
byte[] bytes = GetBytesFromCache(key);
if (bytes == null)
{
//no byte array in the cache
//time to open the files and read the bytes from the files
using (var stream = new MemoryStream())
using (var wstream = gzip ? (Stream)new GZipStream(stream, CompressionMode.Compress) : stream)
{
var allFileText = new StringBuilder();
foreach (string filePath in paths)
{
var fileData = new FileData
{
Path = filePath,
Text = this.file.ReadAllText(filePath)
};
if (filter != null) filter(fileData);
allFileText.Append(fileData.Text);
allFileText.Append(Environment.NewLine);
}
byte[] utf8Bytes = Encoding.UTF8.GetBytes(allFileText.ToString());
wstream.Write(utf8Bytes, 0, utf8Bytes.Length);
wstream.Close();
bytes = stream.ToArray();
cache.Insert(key, bytes, paths, TimeSpan.FromMinutes(cacheForMins));
}
}
Response.AddFileDependencies(paths);
Response.ContentType = type;
Response.AppendHeader("Content-Encoding", encoding);
Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetExpires(Cache.NoAbsoluteExpiration);
Response.Cache.SetLastModifiedFromFileDependencies();
Response.OutputStream.Write(bytes, 0, bytes.Length);
Response.Flush();
}
ILessEngine lazyEngine;
ILessEngine engine
{
get
{
if (lazyEngine == null)
{
var factory = new EngineFactory();
lazyEngine = factory.GetEngine(DotlessConfiguration.DefaultWeb);
}
return lazyEngine;
}
}
void CssFilter(FileData file)
{
string ext = Path.GetExtension(file.Path);
if (ext == ".less")
{
var source = new LessSourceObject
{
Content = file.Text,
Key = file.Path,
Cacheable = true
};
file.Text = engine.TransformToCss(source);
}
}
public void Script(string path)
{
Text(path, "application/x-javascript", null);
}
public void Style(string path)
{
Text(path, "text/css", n => CssFilter(n));
}
}
}
A few new additions here. First, a new project reference to dotless.Core.dll has been included. Second, the parameter Action<FileData> filter has been added to the Text method. Third, we created a filter action for stylesheet files, CssFilter. This filter checks the extension of each CSS file. If the file has a ".less" extension then the text from the file is transformed into readable css; if the file has any other extension, the text is read as-is.
Tryouts
Let's try this puppy out. To begin, create a regular master.css stylesheet:
body
{
/*basic styling for most coder blogs*/
background-color:Red;
color:Blue;
}
Next, create a simple .less file, test.less:
@yellow: #ffff12;
@blue: #12ffff;
.bordered {
width:100px;
height:100px;
border-top: dotted 1px black;
border-bottom: solid 2px black;
}
.yellow-div
{
background-color:@yellow;
.bordered;
}
And, just for good measure, add one more .less file, test2.less:
@import "/public/styles/test.less";
.blue-div
{
.bordered;
background-color:@blue;
}
Finally (praise Jesus for brevity), create your viewpage:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Index</title>
<% Html.Style("/public/styles/master.css"); %>
<% Html.Style("/public/styles/test2.less"); %>
<%= Html.CompressCss(Url) %>
</head>
<body>
<div class="yellow-div">Donald T.</div>
<div class="blue-div">Rosie O.</div>
</body>
</html>
Run your project and check out the the results:
A beautiful webpage, if I might say so myself. Plus, even with the on-the-fly .less ZipController translation, we'll still running at sufficient warp speed:
That said, I don't know how well this will perform on much larger .less files. If you know, or if you have the time or the willpower to test out such a scenario, let me know the results by dropping me a line or by commenting on this post.
Demo Download
I promised a downloadable demo, so here 'tis.
Download the ZipController .Less Demo Here.
Read More
You can leave a response, or trackback from your own site.
One Response to “Translating .Less Files Into .Css Files On The Fly”
Leave a Reply





[...] also handles .less files. For example, add “howdy.chirp.less” to your project, and you’ll get both a [...]