Script and CSS Management in ASP.NET MVC

If you are familiar with YSlow recommendations, I guess you know that it recommends to put your CSS files at the top(#5)  and JavaScript files at the bottom(#6) of the pages. Placing the CSS files at the top is not an issue but putting the JavaScript files at the bottom of the pages has some gotchas, specially in heavily ajaxed based site with Master and Content page environment. When developing a Web 2.0 ajax site it is obvious that we will be using quite a number of plug-ins/widgets along with the core framework like jQuery, ExtJS, Prototype etc and of course there will be our hand coded javascript files, depending upon the size and the functionalities, the number of files can vary. This is the screenshot just to show you the usages of javascript files in DotNetShoutout/KiGG.

JSFiles

Certainly we can merge all these files into one large file and put it in the page bottom but it wont be very optimal solution, it will make our page unnecessary heavy and add delays prior making the page usable. Some will argue that it is just for the first time as the browser will cache the file and the visitor does not have to download it again. Yes true, but for a new site where a large percentage of visitors is visiting it for the first time, it is really important that it is lightening fast and reducing the number and size of the external files we can make our pages to load faster. So, instead of downloading a single gigantic file, we preferred to divide and merge those files in terms of functionality into different file sets and add specific sets to each page. The Master page contains the core javascript framework(jQuery), common parts/initialization that are reused in the content pages and the content pages have its own specific file sets and initialization scripts. And this is where it starts throwing javascript exceptions. Consider the following simple scenario:

In your master page at the bottom you have added the Utility.js with the jQuery framework:

    <script type="text/javascript" src="<%= Url.Content("~/Scripts/jquery-1.3.2.min.js")%>"></script>
    <script type="text/javascript" src="<%= Url.Content("~/Scripts/utillity.js")%>"></script>
    <script type="text/javascript">
        $(document).ready(
                            function()
                            {
                                utility.init();
                            }
                        );
    </script>
</body>

And in content page that is using the above master page you have added the dummyObject.js file at the bottom of the content page:

    <script type="text/javascript" src="<%= Url.Content("~/Scripts/dummyObject.js")%>"></script>
    <script type="text/javascript">
        $(document).ready(
                            function()
                            {
                                dummyObject.init();
                            }
                        );
    </script>
</asp:Content>

If you run the above in VS, you will find that VS enters into debug mode with a message “object expected”  like the following:

VS-Exception

Can you guess what is the problem in the above? yes, before the jQuery framework is downloaded the browser encounter the $ which is not yet defined and starts shooting exceptions.This is a very common gotchas working with Master and Content Pages and it is not ASP.NET MVC specific, the Web Forms also falls into this trap. I guess this is the reason why we have to put the ASP.NET AJAX ScriptManager prior any ajaxable control when working with the Web Forms.

There are quite a few things, we would like to solve:

  • Ensure that all script tags are rendered first no matter where it is located (Master Page/Content Page/User Control) before the initialization (document.ready of jQuery).
  • Merge all initialization statements and put into a single initialization block. The ordering should be Master –> Content Page –> User Control, again no matter how deep the nesting is.
  • Merge all cleanup statements and put into a single cleanup block ($(window).unload of jQuery) same as above initialization but the ordering should be reverse - User Control –> Content Page – > Master Page.

Now meet my tiny little component AssetManagement which solves the above issues very elegantly.

First we will see how the above issues can be solved with the controls of AssetManagement. Yes, it contains very similar controls like ASP.NET Ajax, but the key differences are, you can place it anywhere in the page, it does not generate any extra server tags, just the script tag, no view state, no hidden control nothing, you can easily use it in your ASP.NET MVC application. For the above, we will first put the jQueryScriptManager in the master page like the following:

    <readZ:jQueryScriptManager id="scriptManager" runat="Server">
        <Scripts>
            <readZ:JavaScriptReference Path="~/Scripts/jquery-1.3.2.min.js"/>
            <readZ:JavaScriptReference Path="~/Scripts/utility.js"/>
        </Scripts>
        <OnPageLoad>
            utility.init();
        </OnPageLoad>
    </readZ:jQueryScriptManager>
</body>

Next, in the Content Page:

    <readZ:JavaScriptManagerProxy id="scriptManagerProxy" runat="Server">
        <Scripts>
            <readZ:JavaScriptReference Path="~/Scripts/dummyObject.js"/>
        </Scripts>
        <OnPageLoad>
            dummyObject.init();
        </OnPageLoad>
    </readZ:JavaScriptManagerProxy>
</asp:Content>

Now, when you run, it will generate the following html output:

<script type="text/javascript" src="Scripts/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="Scripts/utility.js"></script>
<script type="text/javascript" src="Scripts/dummyObject.js"></script>
<script type="text/javascript">
//<![CDATA[
jQuery(document).ready(function(){
            utility.init();
            dummyObject.init();
});
//]]>
</script>
</body>
</html>

You can also use the OnPageUnload property for cleanup. for example:

<readZ:jQueryScriptManager id="scriptManager" runat="Server">
    <Scripts>
        <readZ:JavaScriptReference Path="~/Scripts/jquery-1.3.2.min.js"/>
        <readZ:JavaScriptReference Path="~/Scripts/utility.js"/>
    </Scripts>
    <OnPageLoad>
        utility.init();
    </OnPageLoad>
    <OnPageUnload>
        alert('Cleanup for master page.');
    </OnPageUnload>
</readZ:jQueryScriptManager>

And

<readZ:JavaScriptManagerProxy id="scriptManagerProxy" runat="Server">
    <Scripts>
        <readZ:JavaScriptReference Path="~/Scripts/dummyObject.js"/>
    </Scripts>
    <OnPageLoad>
        dummyObject.init();
    </OnPageLoad>
    <OnPageUnload>
        alert('Cleanup for content page.');
    </OnPageUnload>
</readZ:JavaScriptManagerProxy>

It will generate:

<script type="text/javascript" src="Scripts/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="Scripts/utility.js"></script>
<script type="text/javascript" src="Scripts/dummyObject.js"></script>
<script type="text/javascript">
//<![CDATA[
jQuery(document).ready(function(){
            utility.init();        
            dummyObject.init();
});

jQuery(window).unload(function(){
            alert('Cleanup for content page.');
            alert('Cleanup for master page.');
});
//]]>
</script>

Okay, if you have promised that you will not use any server control in your ASP.NET MVC application anymore, here is the fluent version of the exact same thing:

Master Page:

<%= Html.jQuery().Scripts(
                            script =>
                            {
                                script.Source("http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js");
                                script.Source("~/Scripts/utility.js");
                            }
                         )
                 .OnPageLoad("utility.init();")
                 .OnPageUnload("alert('Cleanup for master page.');")
%>

Content Page:

<% Html.jQuery().Scripts(script => script.Source("~/Scripts/dummyObject.js"))
                 .OnPageLoad("dummyObject.init();")
                 .OnPageUnload("alert('Cleanup for content page.');");
%>

And it will generate the same output as the controls. Note that for Content Page we are only using <% but for the master page we are using <%= which means we want to dump the result. Also the fluent syntax is progressive interface (inspired by this excellent post of  Jan Van Ryswyck) which means once you call a method, it will show the only available methods in that context like the following:

AssetFluentSyntax

The component also contains an HttpHandler which you can use to combine multiple css and javascript files, the above codes can also refer these assets. To use the asset handler, you have to first define the assets in the web.config like the following:

<configSections>
    <section name="assetSettings" type="ReadZ.AssetManagement.AssetSettingsSection, ReadZ.AssetManagement" requirePermission="false"/>
</configSections>
<assetSettings version="1.0.0.0" cacheDurationInDays="365" compress="true">
    <assets>
        <clear/>
        <add
            name="css"
            contentType="text/css"
            directory="~/assets/css"
            files="site.min.css;ui.jquery.min.css;marItUp.min.css;colorPicker.min.css"
        />
        <add
            name="js2"
            contentType="application/x-javascript"
            directory="~/assets/scripts"
            files="OpenID.min.js;jquery-1.2.6.min.js;jquery.form.min.js;jquery.validate.min.js;ui.core.min.js;ui.tabs.min.js;ui.draggable.min.js;ui.resizable.min.js;ui.dialog.min.js;Utility.min.js;Search.min.js;Tag.min.js;Membership.min.js;Story.min.js;Analytics.min.js"
        />
        <add
            name="js3"
            contentType="application/x-javascript"
            directory="~/assets/scripts"
            files="ui.autocomplete.min.js;jquery.markitup.min.js;showdown.js;colorpicker.min.js;RichEditor.min.js;ImageCode.min.js;Comment.min.js"
        />
    </assets>
    <system.web>
        <pages>
            <controls>
                <add tagPrefix="readZ" namespace="ReadZ.AssetManagement" assembly="ReadZ.AssetManagement"/>
                <add tagPrefix="readZ" namespace="ReadZ.AssetManagement.Controls" assembly="ReadZ.AssetManagement"/>
            </controls>
            <namespaces>
                <add namespace="ReadZ.AssetManagement"/>
                <add namespace="ReadZ.AssetManagement.HtmlHelpers"/>
            </namespaces>
        </pages>
        <httpHandlers>
            <add verb="GET,HEAD" path="asset.axd" validate="false" type="ReadZ.AssetManagement.AssetHandler, ReadZ.AssetManagement"/>
        </httpHandlers>
    </system.web>
    <system.webServer>
        <handlers>
            <remove name="AssetHandler"/>
            <add name="AssetHandler" preCondition="integratedMode" verb="GET,HEAD" path="asset.axd" type="ReadZ.AssetManagement.AssetHandler, ReadZ.AssetManagement"/>
        </handlers>
    </system.webServer>
</assetSettings>

As you can see, each asset has an unique name, the content type and number of files with the location. When an asset is requested it will merge the specified files into a single response, cache and compress it before sending the response. You can define global version, cache duration and compression (line 4) which will apply to all, you can also override the global settings as per asset. When referring these assets in your code you can use:

For Control:

<readZ:jQueryScriptManager id="scriptManager" runat="Server">
    <Scripts>
        <readZ:JavaScriptReference AssetName="js2"/>
    </Scripts>
</readZ:jQueryScriptManager>

For Fluent Html:

<%= Html.jQuery().Scripts(script => script.Asset("js2")) %>

When referring the CSS, you can use:

<head runat="server">
    <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <link href="<%= Url.Asset("css")%>" rel="stylesheet" type="text/css"/>
</head>

The component has the extension methods for both UrlHelper and HtmlHelper, so that you can use it conveniently in your ASP.NET MVC views.

So far the codes that I have shown is for jQuery only, If you are thinking what about the others like ExtJS, prototype etc. The component has some nice extensibilities which you can use to support other popular libraries as well. For example, lets add the support for the ExtJS. First you have to create a class which implements the IScriptWrapper interface, next add codes for WrapOnPageLoad and WrapOnPageUnload methods like the following:

public class ExtJSScriptWrapper : IScriptWrapper
{
    public string WrapOnPageLoad(string scripts)
    {
        return string.IsNullOrEmpty((scripts ?? string.Empty).Trim()) ? string.Empty : string.Concat("Ext.onReady(function(){\r\n", scripts, "\r\n});");
    }

    public string WrapOnPageUnload(string scripts)
    {
        return string.IsNullOrEmpty((scripts ?? string.Empty).Trim()) ? string.Empty : string.Concat("Ext.EventManager.on(window, 'unload', function(){\r\n", scripts, "\r\n});");
    }
}

Control:

Next create a new control which inherits from base JavaScriptManager and create a new instance of the above wrapper in the constructor and that’s it.

public class ExtJSScriptManager : JavaScriptManager
{
    public ExtJSScriptManager() : base(new ExtJSScriptWrapper())
    {
    }
}

Now you will be able to use the ExtJSScriptManager like the same way as we did with jQueryScriptManager in the above.

Fluent Html:

First, create a class which inherits from base AbstractScriptHtmlHelper and create a new instance of the above wrapper in the constructor:

public class ExtJSScriptHtmlHelper : AbstractScriptHtmlHelper
{
    public ExtJSScriptHtmlHelper(HtmlHelper htmlHelper) : base(new ExtJSScriptWrapper(), htmlHelper)
    {
    }
}

Next create an extension method for the HtmlHelper which will return it.

public static class HtmlHelperExtension
{
    public static ExtJSScriptHtmlHelper ExtJS(this HtmlHelper helper)
    {
        return new ExtJSScriptHtmlHelper(helper);
    }
}

And that’s it, now you will be able to refer ExtJS in the mvc view.

The component also has the unit tests with the amazing duo (xUnit + Moq) and interestingly I was able to achieve 100% code coverage which is worth to check (Yes you will find some unnecessary tests, I did it intentionally to achieve more code coverage).

CodeCoverage

You can download the above sample code and the component from the bottom of this post.  If you have any feedback/bug report/enhancements, do let me know.

Happy YSlow scoring in your ASP.NET MVC App!!!

Download: Source Code

Shout it

17 Comments

  • This looks good but the tests are not included in the source code download

  • hello, great stuff, but is this MVC specific or can it also be used in webforms?

  • @Adrian: Really forgot to add the test project in zip. Now added.

  • @john: You can also use it in Webforms, but UpdatPanel, asp.net ajax web service and scripts in assembly is not supported currently.

  • Is there a way that the browser can use the integrated caching system for javascript and css files with this HttpHandler?

  • @Robert: It is completely *utilizing* browser compression & caching support and you can define it in the web.config, check the above where I have detailed it.

  • This is sweet! It bugged me that after the master and content pages rendered to the page there were multiple $(function(){}); tags.

  • DUDE THAT ROCKS!!!!!! I am putting that in my project right now - the timming couldn't be better too!

  • Hi Kazi, I have just integrated this into an application that I am building and have found that it caches the assets regardless of the cacheDurationInDays property. For example if I set the cacheDurationInDays="0" in the assetSettings I should be able to make changes to the css or js files and it should rebuild the asset on the server on each request. AssetProvider stores the assets in a static dictionary which means that when you call EnsureContent the content is already in memory and therefore doesn't rebuild the content..?

    Also have a couple suggestions UrlHelperExtension and ScriptWriter should use a &amp; instead of & when building the urls so that we can pass XHTML Strict validation.


    Sorry to sound negative I really love this project and would like to add my 2cents!

  • Hi Kazi, I have a seperate HttpHandlers registered for javascript and css. I noticed that you can go .. Html.jQuery().AssetHandler("/js.axd") but for the css one I added an overload to the UrlHelperExtension Asset method so that you can pass in the asset handler path.

    public static string Asset(this UrlHelper helper, string assetName, string assetHandler)
    {
    if (helper == null) throw new ArgumentNullException("helper");
    if (assetName == null) throw new ArgumentNullException("helper");

    var asset = AssetProvider.Current.GetAsset(assetName);
    string virtualPath = string.Concat(assetHandler ?? AssetHandler.DefaultPath, "?name=", assetName, "&amp;v=", AssetProvider.Current.GetVersion(assetName));
    return helper.Content(virtualPath);
    }

  • Great article.
    I'd add support for the assetSettings to be loaded from a separate file (like appSettings have).

    Cheers,

  • Hey, I implemented this into my mvc app, works like a charm on the dev machine (and really makes the code much better and simpler, thanks again).

    However, when deploying the solution on the production server, the compression stopped working.

    The asset.axd works great, caching and combining several files, but it doesn't gzip or deflate the stream.

    The application is deployed on IIS6.

    The strange thing is, that on my IIS5.1 on XP it works fine..

    Any idea what the problem might be?

    Cheers,

    Erik

  • @Jake: Checkout the part -2, I have addressed those with few changes.

    @Erik: I am not sure why it not compressing in IIS 6.0, ensure that Accept-Encoding header contains either gzip or deflate which any modern browser should do.

  • Hi!

    I've just looked at your code and I must congratulate you for the high quality, and also for the 100% code coverage ;-)
    By browsing the code I noticed two minor things in AssetProvider.cs:
    The static IAssetProvider Current getter could use a double check instead of systematically acquire a read lock, like this:
    var current = _current;
    if (current == null) {
    _providerLock.EnterUpgradeableReadLock();
    try {
    if (_current == null)
    {
    _providerLock.EnterWriteLock();
    _current = new AssetProvider();
    current = _current;
    _providerLock.ExitWriteLock();
    }
    finally {
    _providerLock.ExitUpgradeableReadLock();
    }
    }
    return current;

    I would also protect the lock in GetAssetHolder with a try..finally just in case...

    I plan to use it in our project and further extend it to meet our needs. Wouldn't you put your project on CodePlex to let others contribute more easily???

    Thanks a lot again for the great job,

    Morgan

  • Thanks for the Tip.

    Did not bother to verify it, usually when using a different provider, you are suppose to set it in the application start so the chances are very little of multi-threading write.

    Actually it is a part of a larger application that I am suppose to put in codeplex.

    Btw check the 2nd version which has some changes.

  • Very nice article. We tried to implement, it works in Master pages but didn't work in user controls & content pages. But the sample works perfect.

    Thanks,
    Rajan R.G

  • @Rajan: Would you pls let me know exactly where it is not working? Are you using control or html helper?

Comments have been disabled for this content.