Friday, December 21, 2012

Automatic Script/CSS Compression/Rollup in Umbraco

Note: This code is specific to .NET Master Page templates.  Here are instructions for using MVC Views available in Umbraco 4.10+.

When using Umbraco you may have noticed that the Backoffice Administrative interface uses the Client Dependency Framework to roll up and compress all the different scripts and css resources into two calls.  This leads to a big improvement in responsiveness as the server doesn't need to download as much data nor complete as many requests to render a web page in the admin area.

The good new here is that you can use this same system in your actual site with very little setup.  A few simple step to configure your main site templates to use the Client Dependency Framework also.

First of all add the following block to the top of evert template in your site just after the first line, Master Tag.

<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %>

Next for all of you sites master templates add the following code someplace in the HEAD block.  Note that you don't want to put this on every templates, just those that are the top level templates.  The golden rule is that every page should call this one and only once.

<umb:ClientDependencyLoader runat="server" ID="ClientLoader">
  <Paths>
      <umb:ClientDependencyPath Name="Styles" Path="/Css" />
       <umb:ClientDependencyPath Name="Scripts" Path="/Scripts" />
  </Paths>
</umb:ClientDependencyLoader>

Now go through all your templates and change your Script and Css includes to the following sample blocks.

<umb:CssInclude runat="server" FilePath="jquery-ui-1.8.css" PathNameAlias="Styles" Priority="0" />
<umb:JsInclude runat="server" FilePath="jquery-1.8.3.js" PathNameAlias="Scripts" Priority="0" />
<umb:JsInclude runat="server" FilePath="jquery-ui-1.9.js" PathNameAlias="Scripts" />

Note that the Priority property is not required.  If set it will load those first in ascending numerical order then load any thing without a priority.  Also the system will detect duplicate inclusions of the same script/css file on the page and only include each one a single time.  You also won't see errors if a script is missing so watch for that as it can be hard to realize if you make a typo.

If you view the site now you will see two lines like this and if you inspect them they will contain all the compressed code.

<link href="/DependencyHandler.axd?s=RandmData&amp;t=Css&amp;cdv=41" type="text/css" rel="stylesheet">
<script src="/DependencyHandler.axd?s=RandmData&amp;t=Javascript&amp;cdv=41" type="text/javascript"></script>

A few other notes  This will not work when compilation/debug is set to True in the web.config.  You will need to disable debug mode to test it and also on the production environment.  Also when the code is compressed it is very hard to debug.  You will want to turn on debug mode on the development system when debugging CSS and Javascript issues or doing development.  The resources will just be loaded with individual script calls in this case.

Also once this is set up there is no reason to use minified scripts in your site anymore.  I suggest replacing them with the full versions for easier code review/editing.

On a final note if you want to include Scripts and CSS files from within macros you can do so still using the  Client Dependency Framework but it is a bit more complex.  I sugest adding the following helper methods in your App_Code directory.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using System.Web;
using ClientDependency.Core.Controls;
using ClientDependency.Core;

namespace JSP
{
    /// <summary>
    /// Client Dependency Loader helpers.
    /// </summary>
    public static class CDL
    {
        /// <summary>
        /// Custom version of the AddRange method that accepts no arguments and does not error out.
        /// </summary>
        /// <typeparam name="T">type</typeparam>
        /// <param name="self">parent collection</param>
        /// <param name="collection">new collection to add or null</param>
        public static void AddRangeOrNone<T>(this List<T> self, IEnumerable<T> collection)
        {
            if (collection != null)
                self.AddRange(collection);
        }

        #region Control Lookup
        /// <summary>
        /// Search recursivle down a control tree for all controls with the passed in Id
        /// </summary>
        /// <param name="self">Root control to search form.</param>
        /// <param name="id">Id to look for</param>
        /// <returns>List of all matched Controls.</returns>
        public static List<Control> SearchControl(this Control self, string id)
        {
            List<Control> results = new List<Control>();

            Control control = self.FindControl(id);

            if (control != null)
                results.Add(control);

            foreach (Control childControl in self.Controls)
            {
                results.AddRangeOrNone(childControl.SearchControl(id));
            }

            return results;
        }

        /// <summary>
        /// Search recursivle down a control tree for all controls of the passed in type
        /// </summary>
        /// <param name="self">Root control to search form.</param>
        /// <param name="type">Type to look for</param>
        /// <returns>List of all matched Controls.</returns>
        public static List<Control> SearchControl(this Control self, Type type)
        {
            List<Control> results = new List<Control>();

            foreach (Control childControl in self.Controls)
            {
                if (childControl.GetType() == type)
                    results.Add(childControl);

                results.AddRangeOrNone(childControl.SearchControl(type));
            }

            return results;
        }
        #endregion

        #region Client Dependencies
        /// <summary>
        /// Add another script to the dependency loader list to be compressed and combined with other resources.
        /// </summary>
        /// <param name="path">Path of the script to load from the pathNameAlias folder.</param>
        /// <param name="pathNameAlias">Folder alias to look for the script in.</param>
        public static void AddScript(string path, string pathNameAlias = "Scripts", int priority = 10)
        {
            Page page = (Page)HttpContext.Current.Handler;
            IEnumerable<ClientDependencyLoader> cdl = page.SearchControl("ClientLoader").Cast<ClientDependencyLoader>();

            if (cdl.Count() > 0)
            {
                cdl.First().RegisterDependency(priority, path, pathNameAlias, ClientDependencyType.Javascript);
            }
        }

        /// <summary>
        /// Add another stylesheet to the dependency loader list to be compressed and combined with other resources.
        /// </summary>
        /// <param name="path">Path of the stylesheet to load from the pathNameAlias folder.</param>
        /// <param name="pathNameAlias">Folder alias to look for the stylesheet in.</param>
        public static void AddStyle(string path, string pathNameAlias = "Styles")
        {
            Page page = (Page)HttpContext.Current.Handler;
            IEnumerable<ClientDependencyLoader> cdl = page.SearchControl("ClientLoader").Cast<ClientDependencyLoader>();

            if (cdl.Count() > 0)
            {
                cdl.First().RegisterDependency(10, path, pathNameAlias, ClientDependencyType.Css);
            }
        }
        #endregion
    }
}

Then you can use the following in a macro to add a resource. Note that this will not work on macros that use Cacheing, in those cases the scripts would need to be added to the templates instead.

@using JSP
@{
  CDL.AddScript("jquery-ui-1.9.js");
  CDL.AddCss("ContentTabbed.css");
}

No comments: