Introduction
JavascriptHelper is an MVC component that facilitates including JavaScript and CSS on a page. It manages dependencies, allows you to include JavaScript from wherever it is required (e.g., views, partial views, layouts, helpers, etc.), and allows you to specify exactly where to include the JavaScript and CSS.
Background
After working several years with the Castle Monorail MVC framework, I decided to try ASP.NET MVC to see if it had caught up to Monorail. The transition seemed to go rather smoothly, but one area where I was surprised to find how clumsily it was handled, was the management of JavaScript files. Basically, if some part of a page, say a helper or partial page, needed a particular JS file, you had one of two choices.
The first option is to have the part itself write the script tag. This allows the part to operate as a “black box” – just drop it in and it works – But it means that there will be script tags loading files scattered throughout the page, and that the part needs to know your folder structure where you keep your JavaScript files. And it needs to know if you want the file loaded from your website or from a CDNS like Googleapi.com. And, since there’s a good chance it will depend on jQuery, you have to make sure that jquery.js is loaded first, at the top of the page, despite “best practices” which say script files should be loaded at the bottom of the page. Then, let’s say, two different partial views on the same page use the same JS file, you need a way of making sure it’s only included once. Plus, there’s a good chance it will also depend on its own CSS file being loaded, which doubles the problems above. As a way to address this, Microsoft (well, PluralSite’s training videos on Microsoft’s site) recommends putting the script tags in a @section named Scripts, and rendering that in the layout, which helps but only addresses some of these problems.
Alternately, you can break the black box, and manually add the needed script
and CSS link tags in your layout. This allows you to group the files tag together in the proper places, CSS at the top, JS at the bottom. But, you must know all the JS files all components of the page need, including all dependencies they have. And if you are putting these in a layout file, then you’ll need to put all the JS files needed for all pages anywhere on the site.
Wouldn’t it be great if there was a way to automatically figure out just the files we need for a certain page, and include just those, without us having to do a lot of thinking about it. Isn't that the type of thing we invented computers for?
Monorail also lacked such a manager, but Monorail has neither the major corporate sponsor nor the large user community base of ASP.NET MVC, where I figured someone would have written one. I wrote a manager like this for Monorail, so I guess that someone is going to be me… which leads us to the JavascriptHelper.
Goals
My goals for the JavascriptHelper were the following:
- To have all needed JS files grouped together, in a single spot I specify (presumably at the bottom of the layout)
- To be able to specify in a view, partial view, or helper that a JS file is needed.
- To have all JS files that file depends on included automatically.
- To have files included only once, regardless of how many pieces requested them.
- To be able to associate a needed CSS file, and have it similarly included where I specify in the layout.
- To be able to specific a needed file by a simple name.
- To be able to easily update the file a particular name refers to (So, if I replace, say MetaFlex-1.5.3.js with MetaFlex-1.6.0.js I only need to change it in one spot, and every page that uses it changes).
- To be able to use one version of a file during development/debugging and easily switch to another (presumably mini-ified) for production.
- To be able to use a local version of a file during development/debugging (when I’m often off-line), and easily switch to Google’s CDN for production.
- To be able to gather a collection of JS files into a single, combined, mini-fied file using the MVC4 bundling/compression APIs.
- To be able to specify a block of JS code in a view, partial view or helper and have all the code blocks grouped together.
- To be able to say if a code block should be included once per page, or repeatedly.
Usage
Let’s say for example that I have a partial view that uses the jQuery UI slider control (which I’ll specify some unique id value). Plus it has JS code which needs to be called to initialize it. Say this
is in the form of a method and a call to that method using that id. Now, let’s say that we want several sliders on that page, and use that partial view several times, so we’ll only need the jQuery UI scripts and the method definition once, but we’ll need each separate call to the function. To handle this, you’d need to add to you partial view, the following:
JScripts.Std("slide");
JScripts.AddScript("MySlider", "function HideSlider(id) { $(id).Hide();}")
JScripts.AddScript("HideSlider('#"+ myId + "');")
“JScripts.Std()
” says that “slider” is one of the standard JavaScript files which we have defined what it’s traits and dependencies are. Actually, that could be a comma-separated list of them, so you can state everything you need in one line. I originally envisioned only a few “standard” files – jQuery, jQuery UI, et al – but soon realized assigning a keyword for every script file used made life easier. How a file is pre-defined, as well as where we get a Script object, will be discussed in the next section.
The first “JScripts.AddScript()
” call defines the function that we need. The second
AddScript
called calls that function, using the id specific to that instance
of the partial view. So, if our page has three instances of this partial view, we’ll need the function definition only once, but the call to it three times. This is handled
by given a name to the snippet that need not be repeated. All blocks with the same name are rendered only once (actually, blocks with the same name as an existing block
are ignored, so make sure that you only use a particular name for one script block).
Then in the Layout, we just add the lines:
<head>
@JScripts.InsertCss();
</head>
<body>
/* other content for the page */
@JScripts.InsertScripts();
<html>
This will produce an output of:
<head>
<link rel="stylesheet" type="text/css" href="content/css/ui.base.css" />
<link rel="stylesheet" type="text/css" href="content/css/ui.core.css" />
<link rel="stylesheet" type="text/css" href="content/css/ui.slider.css" />
</head>
<body>
/* other content for the page */
<script type="text/javascript" src="http://www.codeproject.com/Scripts/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="http://www.codeproject.com/Scripts/ui/jquery.ui.core.js"></script>
<script type="text/javascript" src="http://www.codeproject.com/Scripts/ui/jquery.ui.widget.js"></script>
<script type="text/javascript" src="http://www.codeproject.com/Scripts/ui/jquery.ui.mouse.js"></script>
<script type="text/javascript" src="http://www.codeproject.com/Scripts/ui/jquery.ui.slider.js"></script>
<script type="text/javascript">
function HideSlider(id) { $(id).Hide();}
HideSlider('#sl12321');
HideSlider('#sl14315');
HideSlider('#sl68953');
</script>
</body>
<html>
That is, unless we are using MVC4 and have turned on bundling/compression, in which case, it will look more like:
<head>
<link rel="stylesheet" type="text/css" href="content/css/home_index?u=1234e6f7g9838759503u"/>
</head>
<body>
/* other content for the page */
<script type="text/javascript" src="Scripts/home_index?u=123e432d543f765a654"> </script>
<script type="text/javascript">
function HideSlider(id) { $(id).Hide();}
HideSlider('#sl12321');
HideSlider('#sl14315');
HideSlider('#sl68953');
</script>
</body>
<html>
Setup
Creating a JScripts object
Presently, the JavascriptHelper is distributed as a single source file. (But that may change in the future. See the "Next Steps" section at the end). Just include it in your project.
The “JScripts” object needs to be created differently depending on where it is being used. (You can, of course, call it anything you like.
I use “JScripts” with a capital J & S, so that it corresponds to @Html. Actually, I was using @Script, but Microsoft decided to use that name starting with MVC4RC).
When used in a view, create it like this:
@{ var JScripts = NovelTheory.Component.JavascriptHelper.Create(this); }
When used inside a @helper method or an HtmlHelper extension method:
var JScripts = NovelTheory.Component.JavascriptHelper.Create(WebPageContext.Current);
Configuration XML file
Finally, we reach the part which is at the heart of the JavascriptHelper, the
jslibraries.xml file. It’s a bit complex, but the default I’ve set up is probably good enough, with perhaps only a few minor
tweaks, and once fully configured to your taste, you could easily standardize on that for your whole enterprise. It is, however, simple enough that a NuGet package could update it when being added to a project. And the good news is that I’ve also create a XSD schema file for it, so you can have IntelliSense to help you along.
The root element is <libraries> and it has four optional attributes:
“localjspath=
” which gives the relative location of the folder where you put your JS files. The default is “~/Scripts” which is the ASP.NET MVC default.
“selfJsPath=
” which gives the relative location of the folder under which you put your view-specific JS files. They follow the same structure as view files, so if you request the JS file for /Home/Index, the view file would be /Views/Home/Index.cshtml, and the JS file used would be
/Scripts/Views/Home/Index.js. Defaults to “~/Script/Views”
“useDebugScripts=
” when set to “true
” forces use of debug versions of JS files when available. Default is false. Debug scripts are always used when a debugger is attached.
“transform=
” which controls bundling and compression and takes one of three options: “None”, “BundleOnly”, and “Compress”. They should be self-explanatory. The default is “None”, so you must add the option to see any effect.
<library name="jquery" version="1.6.2" useGoogle="false"
pathname="jquery-1.6.1.min.js" debugPath="jquery-1.6.1.js" transform="compress" />
Within the <libraries>
element, there is a collection of <library>
elements – one for each standard
JavaScript file you’ll be using-- which have two required and several optional attributes:
“name=
” which gives a unique name for a particular javascript file. It is required and is used by the helper and throughout the xml file to refer to that file.
There is one special library name, “base
” which I’ll explain below.
“pathname=
” which gives the path, relative to the localjspath described above, and filename of this file. This can also be a fully-qualified URL if it’s a remote file. It is required. (unless useGoogle is true, or name=”base”)
“debugPath=
” which, like the pathname attribute, gives the relative path to a
JavaScript file to be used when debugging. This is optional and defaults to the value given for the pathname. The idea here is the you set the debugPath to the full, commented version, and the pathname to the mini-fied version, and the helper figures out which to use.
“dependsOn
” which is a list of comma separated names of files, which must be loaded before this one. This is the most powerful feature, but also the most confusing, so we’ll expand upon it in a moment. It is optional and defaults to no dependencies
“alias=
” which is a list of comma separated alternate names this file could be identified as. I added this because I could never remember if it’s “accordion” or “accordian”. This is optional and defaults to no aliases.
“css=
” which gives the name of the css element (see below) associated with this file.
“useGoogle=
” which, when set to true, will generate a request to the googleapi.com CDN to load the file. Optional, defaults to false. If true, “version” attribute is required.
“version=
” is used only when “useGoogle” is true. Gives the version of the file to load from Google.
Also within the <libraries>
element (at the same level as the <library>
elements), there is the <css> group.
The
<css>
element has only one attribute “
localcsspaat
h” which gives the relative location of the folder where you put your CSS files. The default is “~/Content” which is the ASP.NET MVC default.
Within the <css> element, there are multiple <sheet> elements, one for each css file associated with a js file. They have two required attributes:
“name=
” Unique name used to identified the particular css file. Matches name given in the “css” attribute in the <library> element above. Can be the same as the name used for the js file in the <library> element.
“pathname=
” which gives the path, relative to the localcsspath described above, and filename of this file.
I realized that sometimes you’ll want a JavaScript file loaded on every page (before you say “jQuery!” remember, that’s handled by the dependencies). OK, what you really want to a particular CSS file loaded on every page. JavascriptHelper now offers a way to handle that and include them in
bundling. Define a <library> entry with the name of “base”. You don’t need to specify a path name for it, but includes a
dependsOn
attribute which specifies the JS files you always want loaded. Then add a <sheet> element named “base” which has a pathname of your standard CSS file (“site.css” if you
follow the MVC4 defaults).
Example
<library name="jquery" version="1.6.1" useGoogle="false" pathname="jquery-1.6.1.min.js"
debugPath="jquery-1.6.1.js" />
<library name="uicore" dependsOn="jquery" pathname="ui/jquery.ui.core.js"/>
<library name="uiwidget" dependsOn="uicore" pathname="ui/jquery.ui.widget.js"/>
<library name="mouse" dependsOn="uiwidget" pathname="ui/jquery.ui.mouse.js"/>
<library name="datepicker" dependsOn="uiwidget"
pathname="ui/jquery.ui.datepicker.js" css="ui" />
<library name="slider" dependsOn="uiwidget,mouse"
pathname="ui/jquery.ui.slider.js" css="ui" />
Starting with the first element, we specified a JavaScript file, which we’ll be calling “jquery”. When we ask for “jquery”, we’ll normally get “jquery-1.6.1.min.js” , unless we’re debugging, in which case, it will load “jquery-1.6.1.js”. It will load these from the website, but, if, as I put it into production, I flip the useGoogle file to true, then it will load it from http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.js
(Note that the “name” value is used to the URL here. Normally, it can just be any unique string. But, this is the only place where the actual name used is significant. This is wrong, and will be fixed in some future release).
Next we want to define the complete componentized version of jQuery UI, so that we can load just the parts we want. First, we declare the UI core as depending on jQuery. Then we declare widget component as depending on the core. And the mouse component as depending on the widgets.
Then we can add the individual components themselves. “datepicker” as depending on the widgets; “slider” as depending on the widget & the mouse components. Since the mouse already depends on widget, it wasn't really necessary to specify both here, but there’s no harm. So, if you request to use “slider” on your page, then the helper makes sure that jQuery, the UI core, UI Widgets, mouse & slider components are all included, in the proper order. (Note, there is no checking for circular dependencies, so be careful how you specify the
dependsOn
attribute.)
Both are also associated with the “ui” css file, which brings us to that section:
<css localcsspath="~/content/css/Views">
<sheet name="ui" pathname="ui/themes/Redmond/jquery-ui-1.8.13.custom.css" />
<sheet name="cluetip" pathname="jquery.cluetip.css" />
</css>
Both of the jQuery UI components above give “ui” as their CSS file (the theme roller doesn’t create individual CSS files) so if either (or both) is used, that file is included.
CSS files don’t have a separate dependency tree, but all associated CSS files for every dependent js file up the tree are included, so if you wanted to use the separate
jQuery UI CSS files, you’d associate “ui.core.css” with uicode”, “ui.base.css” with “uiwidget” and each component with its own CSS file, and all the needed files will be included.
Bundling and Compression
Bundling was added to ASP.NET MVC after the original release of JavascriptHelper. Sparing you the ugly details of how I got this to work, using it is simple. Turn it on. It just works. Essentially, you use it exactly like the non-bundling version just described. All bundling & compression is now just handled automatically behind your back. It’s turned on and off using the “transform” attribute of the <libraries> element in the
jslibraries.xml file.
Ugly details that shouldn't matter to you, but I need to vent
First of all, as far as I can tell, MVC bundling in the beta (not this code; the bundling code provided by Microsoft in the beta), is just broken. The first indication of this is that the "RegisterTemplateBundles()
” method loads a very specific set of JS files, with their filenames hard-coded right in the IL code. I’m certain that’s going to be changed before the RTM version.
It also appears that any two bundles given the same virtual file name (regardless of the page it’s on, the content of the bundle or the query string parameter) will get the content of the first bundle created with that name back. If you say “Well, that’s to be expected”, remember that the default bundling code created by the Wizard uses the same virtual file name for every page. That Wizard-generated code also puts every JS file in your ~/Scripts folder into the bundle, so every page uses the same bundle anyway. The problem only shows up when you try to control the bundle contents more closely. When I deviated from the default, I started seeing that the second page I viewed only got the JS files that the first page had requested.
I discovered this when testing a scenario which Microsoft didn’t seem to plan for – a selection of JS files used on a page includes both local files to be bundled, and external files which are not, and with the external files in the middle of the list, requiring two separate bundles on one page.
Say for instance, you are loading jQuery off of a CDN (like the one Microsoft runs), or you are using a WebService where the service wants you to load the API JS file directly from their site (like Microsoft’s Bing Maps), then you’ll have JS files, which would be handled by the JavascriptHelper, which should not be part of the bundle, but may, through the dependency tree, show up between local files. In that case, you’ll need one bundle to be loaded before the external files, and a second to be loaded after them. JavascriptHelper gets this to work, automatically.
So, as you might have guessed, Microsoft rewrote a lot of this for MVC4 RC, and with any rewrites, some code written for the predecessor broke.
So, along with mitigating the first problem listed (they moved the standard included files out of the assembly and put them in Wizard-generated source code, namely, the
BundleConfig.cs file placed in the App_Start folder), they also started using different names for different bundles. However, now they seem to have gone the other way entirely, created, by default, several named "bundles" each containing only one file. JavascriptHelper creates as few bundles as needed.
API
The API is fairly simple:
Std(string)
- accepts a comma-separated list of script file ids, either a name or an alias as defined in a <library> element in the
jslibraries.xml file.
It will also accept “self
” to load a script & CSS file based on the name of the view, i.e., if Script.Std(“self”)
is used in /Home/Index, then it will load
/Scripts/Views/Home/Index.js and /Content/Css/Home/index.css (if they exist on the file system).
AddScript(string id, string script)
- accepts a block of script as text. Multiple calls with the same id are rendered only once.
AddScript(string script)
- accepts a block of script as text. Each call is rendered. All script from either AddScript methods is rendered in a block at the script insertion point.
AddOnReadyScript(string script)
--accepts a block of script as text, which is appended to the script run on page ready. All are rendered, wrapped in a jQuery document ready event function, at the point of
InsertOnReady()
.
InsertScripts()
-- Renders all script files & script blocks.
InsertOnReady()
-- Renders all script block intended for startup, wrapped in a jQuery on ready function.
InsertCss()
-- Renders all css files
What’s the next step?
Now that I’ve made this public, it’s really just a beta. I think there’s still a bit more to be done before it’s ready to be “Production-Ready v1.0”. And I need some feedback….
- First of all, how exactly should it be packaged? I’ve been just added to source file to my project, which is simple, but not very elegant. The alternative would be to create an assembly for it, but it’s just one file, so that seem like overkill. I’d really like to create a NuGet package for this, but that question needs to be settled first.
- Or do we think bigger? Microsoft has open-sourced the MVC framework, and is now accepting pull-requests. Should it be deeply embedded into the MVC eco-system?
- Also, how is the API? Are the method names sufficiently intuitive?
- Is an XML file the best way to store the dependency information?
- The way JavascriptHelper handle CDNs is clearly lacking, largely due to inconsistencies in URL naming patterns among different libraries, even on the same network (and due to me only needing to get one file off of a CDN). This definitely needs to be expanded. I tried a few thought with the (unused) <cdn> elements at the bottom of the
jslibraries.xml file, but that went nowhere.
- Is there any feature that really needs to be added?
The code
The source code is provided with this article, along with a MVC4 demo application, however, future updates will be done on the github repository.
The source code is available (under the Apache license) from my GitHub library:
http://github.com/jamescurran/JavascriptHelper
There is also a CodePlex project for discussions and bugs reports: http://javascripthelper.codeplex.com/.
History