Dustin Horne

Developing for fun...

Simple Asset Manager for MVC

Lately I've been using a lot of jQuery and other custom javascript in my projects.  I like to use external CDNs when possible to load my script files.  In particular, I like to use the asp.net CDN for use with jQuery.  For much of this work I can simply put my script references in the layout pages, however there are times when I want specific scripts for specific pages.  There are also times when I want to be able to upgrade a script to a newer version.  This may require modifying multiple views.  Additionally, it would require modifying my HTML/Razor for at least my layout view and re-uploading it to the server.

To better illustrate my point, let's say I were using a particular version of jQueryUI but I didn't put it in my Layout because it was only necessary for functionality on 10 of my views so I put the script reference in each of those views individually.  Now let's say a major bug or security vulnerability was discovered in jQueryUI that required me to upgrade to a new version.  Since I'm using the CDN, this would require me to update all of my script references to point to the new version of the script and reupload all of my views.  Even if it's only the layout view, this is an inconvenience.

In order to combat this, I created a simple asset manager to manage my scripts for me.  This allows me to store all of my scripts in one place.  I also use it for CSS files but you could use it for images or even hyperlinks.  Internally my asset manager uses a Dictionary<string, Asset> to store each asset.  Each asset needs to have a unique key.  Attempting to add a new item to the asset manager with a duplicate key name will cause the old asset to be overridden.  The asset manager itself is a static class.  Additionally, it contains an AssetPath method that is an extension for MVC's UrlHelper making it super simple to output the URL's for my assets.

Using a static class with a keyed dictionary also has another advantage.  I can change my script paths on the fly without modifying or restarting the application or uploading any files.  My Asset class has three values that it stores:  Key, Path, and IsLocal.  The IsLocal property is used by the asset manager when generating the URL for the asset (in determing whether or not to use the UrlHelper's .Content method).  This means if I want to change the jQuery script that's being used I simply register a new jQuery asset as follows:

AssetManager.RegisterAsset(new Asset("jQuery", "http://example.com/script/jquery.x.x.min.js", false));

As an example, let's say I registered the jQuery CDN address and I want to output that in my view.  Assuming I've registered my namespace in the web.config file or adding it to my view using the 'using' directive, I could simply reference the script as follows:

<script src="@Url.AssetPath("jQuery")" type="text/javascript"></script>

Since the AssetManager is static, I could simply re-register the jQuery asset with a new path and it will automatically be updated on all of my pages.  This simplifies the administration of your scripts.  For instance, you could store your scripts in a database, then retrieve them and populate the asset manager on application start (in your global.asax file).  In your admin area you could update both the database and the asset manager without requiring any file changes on your system.  Or, as another potential use you could use it to store only paths and use it for a theming system, so let's say you have a system with themes broken into folders.  The themes are "Ice Blue" and "Default".  You could register the paths as follows:

AssetManager.RegisterAsset(new Asset("Theme-Ice-Blue", "~/content/themes/iceBlue/", true));
AssetManager.RegisterAsset(new Asset("Theme-Default", "~/content/themes/default/", true));

Now, let's say you're storing your theme in a variable called currentTheme.  You could resolve your paths by using @Url.AssetPath(currentTheme) and add whatever asset you need (such as style.css).

The Code

Now that I've given a quick explanation of how you could use the asset manager, here is the code.  We begin by creating an Asset class to use for storing your asset information.  Asset could be created as an immutable struct as well, however you may want to create a more complex solution that would make reference types more convenient.  For instance, you may want to add a similar dictionary to Asset in order to store a list of dependent scripts.  You could then create an HtmlHelper extension that completely outputs your script tags for your script and dependent scripts.  In this case, each script is a single asset.  Here is the code for the Asset class:

namespace YourNamespace.Assets
{
    public class Asset
    {
        public readonly string Key;
        public readonly string Path;
        public readonly bool IsLocal;

        public Asset(string key, string path, bool isLocal)
        {
            Key = key;
            Path = path;
            IsLocal = isLocal;
        }
    }
}


Replace "YourNamespace.Assets" with the proper namespace for your solution.  Now that we have a class for storing our asset information, we can setup our AssetManager class to do the work for us.  Let's start by creating our AssetManager as follows:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Web.Mvc;

namespace YourNamespace.Assets
{
    public static class AssetManager
    {
        private static readonly Dictionary<string, Asset> _assets = new Dictionary<string, Asset>();
    }
}

This is the basic shell for our AssetManager.  It's a static class with a private internal dictionary to manager our Assets.  While I could have made the dictionary public, I did not want it to be modified outside of the constraints of the AssetManager.  I do provide an Assets method that returns a new ReadOnlyDictionary for the list of Assets but I don't worry about the assets being modified since they are essentially immutable in that the only place the Key, Path and IsLocal values can be specified is within the constructor.

Now let's look at the AssetManager methods.  I use an AssetPath method that returns the path for a specified asset.  This is also an extension to UrlHelper which allows us to use @Url.AssetPath("jQuery") without our code to return the path.  This also supplies our method with a reference to the current UrlHelper object so we can format any local URL's.  I also have RegisterAsset, UnregisterAsset, and Clear methods which are self explanatory.

Additionally, I've implemented a static constructor to automatically register a default jQuery version as well as the VSDOC file (for intellisense).  This is only for demonstrative purposes and I wouldn't recommend referencing the external resource from within your constructor as you have no control over it's location or existence, but it shows you once again how to register an asset to the AssetManager.  Here is the entire code for the AssetManager:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Web.Mvc;

namespace YourNamespace.Assets
{
    public static class AssetManager
    {
        private static readonly Dictionary<string, Asset> _assets = new Dictionary<string, Asset>();

        /// <summary>
        /// Returns a read-only collection of the currently registered assets.
        /// </summary>
        /// <returns></returns>
        public static ReadOnlyCollection<Asset> Assets
        {
            get { return new ReadOnlyCollection<Asset>(_assets.Values.ToList<Asset>()); }
        }

        static AssetManager()
        {
            //CSS Definitions
            RegisterAsset(new Asset("site.css", "~/content/site.css", true));

            //jQuery Scripts
            RegisterAsset(new Asset("jQuery", "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js", false));
            RegisterAsset(new Asset("jQuery-vsdoc", "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1-vsdoc.js", false));
        }

        /// <summary>
        /// UrlHelper to get the URL for the supplied asset from the Asset Manager (LocalBlender.Core)
        /// </summary>
        /// <param name="helper"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        public static string AssetPath(this UrlHelper helper, string key)
        {
            Asset asset;
            if (_assets.TryGetValue(key, out asset))
            {
                return asset.IsLocal ? helper.Content(asset.Path) : asset.Path;
            }

            return string.Empty;
        }

        /// <summary>
        /// Register an asset with the asset manager.  Any existing asset with the same key will be replaced.
        /// </summary>
        /// <param name="asset">Asset to register</param>
        public static void RegisterAsset(Asset asset)
        {
            if (_assets.ContainsKey(asset.Key))
            {
                _assets[asset.Key] = asset;
                return;
            }

            _assets.Add(asset.Key, asset);
        }

        /// <summary>
        /// Unregisters an asset from the Asset Manager
        /// </summary>
        /// <param name="key">Asset key</param>
        public static void UnregisterAsset(string key)
        {
            _assets.Remove(key);
        }

        /// <summary>
        /// Removes all registered assets from the asset manager
        /// </summary>
        public static void Clear()
        {
            _assets.Clear();
        } 
    }
}

The last thing you need to do is make sure your Assets namespace is available from within your pages.  Expand your "Views" folder and find the Web.config file inside the Views folder.  Find the Namespaces section inside the Pages section and add your asset manager namespace.  It should be similar to the follwoing:

<pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
        <add namespace="YourNamespace.Assets"/>
      </namespaces>
    </pages>

And that's it, you can now start using the AssetManager inside your views.