Tuesday, April 28, 2009 4:00 AM
kazimanzurrashid
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.
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:
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:
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).
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
Filed under: Asp.net, MVC, DotNetShoutout, ASPNETMVC, KiGG, ASP.NET MVC, Unit Test, JavaScript