Quick-start to MVC4 Bundling

After using MVC4’s bundling in a couple of web apps I got to a simple recipe:

  1. Add the NuGet package Microsoft.AspNet.Web.Optimization (http://nuget.org/packages/Microsoft.AspNet.Web.Optimization/1.0.0)
  2. On your Global.asax.cs file, add the following call on Application_Start:
    BundleConfig.RegisterBundles(BundleTable.Bundles);
  3. Create a class called BundleConfig in your App_Start folder, with a method named RegisterBundles:
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
        }
    }
  4. If you want to use optimizations on your debug environment, add the following line on top of the RegisterBundles method:
    BundleTable.EnableOptimizations = true; // force to show on debug mode
  5. Add your bundles to the RegisterBundles method, such as:
    bundles.Add(new StyleBundle("~/css/global").Include("~/Content/site.css"));
  6. If you are like me and you use virtual paths for css bundles that don’t match the real location of the files, you will have problems using the StyleBundle with relative image URIs. To solve this without changing your original css files, you can create a custom IBundleTransform that resolves the image path. My solution is largely based on this stackoverflow answer: http://stackoverflow.com/a/13019337/732733
    public class RelativePathResolverTransform : IBundleTransform
    {
    	public void Process(BundleContext context, BundleResponse response)
    	{
    		response.Content = String.Empty;
    
    		Regex pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);
    		// open each of the files
    		foreach (FileInfo cssFileInfo in response.Files)
    		{
    			if (cssFileInfo.Exists)
    			{
    				// apply the RegEx to the file (to change relative paths)
    				string contents = File.ReadAllText(cssFileInfo.FullName);
    				MatchCollection matches = pattern.Matches(contents);
    				// Ignore the file if no match 
    				if (matches.Count > 0)
    				{
    					string cssFilePath = cssFileInfo.DirectoryName;
    					string cssVirtualPath = RelativeFromAbsolutePath(context.HttpContext, cssFilePath);
    
    					foreach (Match match in matches)
    					{
    						// this is a path that is relative to the CSS file
    						string relativeToCSS = match.Groups[2].Value;
    						// combine the relative path to the cssAbsolute
    						string absoluteToUrl = Path.GetFullPath(Path.Combine(cssFilePath, relativeToCSS));
    
    						// make this server relative
    						string serverRelativeUrl = RelativeFromAbsolutePath(context.HttpContext, absoluteToUrl);
    
    						string quote = match.Groups[1].Value;
    						string replace = String.Format("url({0}{1}{0})", quote, serverRelativeUrl);
    						contents = contents.Replace(match.Groups[0].Value, replace);
    					}
    				}
    				// copy the result into the response.
    				response.Content = String.Format("{0}\r\n{1}", response.Content, contents);
    			}
    		}
    	}
    
    	private static string RelativeFromAbsolutePath(HttpContextBase context, string path)
    	{
    		var request = context.Request;
    		var applicationPath = request.PhysicalApplicationPath;
    		var virtualDir = request.ApplicationPath;
    		virtualDir = virtualDir == "/" ? virtualDir : (virtualDir + "/");
    		return path.Replace(applicationPath, virtualDir).Replace(@"\", "/");
    	}
    }
  7. To make it easier to use, I also added a custom Bundle:
    public class ImageRelativeStyleBundle : StyleBundle
    {
        public ImageRelativeStyleBundle(string virtualPath)
            : base(virtualPath)
        {
            Transforms.Add(new RelativePathResolverTransform());
        }
    
        public ImageRelativeStyleBundle(string virtualPath, string cdnPath)
            : base(virtualPath, cdnPath)
        {
            Transforms.Add(new RelativePathResolverTransform());
        }
    }
  8. You can use it like this:
    bundles.Add(new ImageRelativeStyleBundle("~/css/jqueryui").Include
    (
    	"~/Content/themes/base/jquery.ui.core.css",
    	"~/Content/themes/base/jquery.ui.resizable.css",
    	"~/Content/themes/base/jquery.ui.selectable.css",
    	"~/Content/themes/base/jquery.ui.accordion.css",
    	"~/Content/themes/base/jquery.ui.autocomplete.css",
    	"~/Content/themes/base/jquery.ui.button.css",
    	"~/Content/themes/base/jquery.ui.dialog.css",
    	"~/Content/themes/base/jquery.ui.slider.css",
    	"~/Content/themes/base/jquery.ui.tabs.css",
    	"~/Content/themes/base/jquery.ui.datepicker.css",
    	"~/Content/themes/base/jquery.ui.progressbar.css",
    	"~/Content/themes/base/jquery.ui.theme.css"
    ));
  9. To use bundles in your views, I recommend adding the System.Web.Optimization namespace to /Views/Web.config, on the namespaces section:
    <namespaces>
    	...
    	<add namespace="System.Web.Optimization" />
    </namespaces>
  10. Now you just need to call Styles.Render(“~/css/jqueryui”) for the example above to work. For javascript you can use the Scripts.Render(“virtualpath”) call. If you need to specify custom attributes to your element, such as the media attribute for print css versions, you can build the element yourself and use the Styles.Url method, like so:
    <link href="@Styles.Url("~/css/print")" rel="stylesheet" type="text/css" media="print" />

I’ve successfully (but also painfully) integrated MVC4’s bundling and minification with dotless, for LESS CSS support. I’ll explain the necessary steps on the next blog post.



Update
In this post’s comments, Kenneth commented on the lack of support for debug mode when using Styles.Url in a link tag. To solve this issue, I’ve created some methods that add this functionality.

Notes: Because the Style class and its methods are static I can’t just overload their behavior nor add another extension method, so instead I added a separate class that you could use instead of Style. I took advantage of the DEBUG compilation constant to avoid some string manipulation on a production/release environment.

public static class Bundles
{
    public static IHtmlString Render(string path, object htmlAttributes)
    {
        return Render(path, new RouteValueDictionary(htmlAttributes));
    }

    public static IHtmlString Render(string path, IDictionary<string, object> htmlAttributes)
    {
        var attributes = BuildHtmlStringFrom(htmlAttributes);

#if DEBUG
        var originalHtml = Styles.Render(path).ToHtmlString();
        string tagsWithAttributes = originalHtml.Replace("/>", attributes + "/>");
        return MvcHtmlString.Create(tagsWithAttributes);
#endif

        string tagWithAttribute = string.Format(
            "<link rel=\"stylesheet\" href=\"{0}\" type=\"text/css\"{1} />", 
            Styles.Url(path), attributes);

        return MvcHtmlString.Create(tagWithAttribute);
    }

    private static string BuildHtmlStringFrom(IEnumerable<KeyValuePair<string, object>> htmlAttributes)
    {
        var builder = new StringBuilder();

        foreach (var attribute in htmlAttributes)
        {
            builder.AppendFormat(" {0}=\"{1}\"", attribute.Key, attribute.Value);
        }

        return builder.ToString();
    }
}

Usage:

@Bundles.Render("~/css/print", new { media = "print" })

4 Thoughts on “Quick-start to MVC4 Bundling

  1. The sucky part about using the Styles.Url approach to add the media=”print” tag is that the reference is no longer changed depending on whether you’re running with debug=true or debug=false. You will simply always get the minified version (when really debug=true should give you the UN-minified version).

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Post Navigation