While building out the MVC Storefront, I find that I'm creating quite a large "Helper" library for doing common things, like registering script tags and CSS sheets. I decided to tighten this up a bit and created a helper I kind of like.
The Need
I want to try and reduce repetition as much as possible throughout the application, and to that end I put as much logic as I can into "Helpers" so I can reduce the <%...%> as much as possible on the page. ANY logic whatsoever makes the ViewPage harder to read, and my general rule of thumb is that if I see an "If" anywhere, it's ripe for a Helper. I know this rule doesn't always apply... but it's a good place to start.
The same logic applied to most UI elements - things like style sheets (if you have more than one), common images (like Product images), and script tags.
The Setup
I keep all my script tags in a directory called "scripts". Right now there are only 3 files in there:
Normally I would just make a simple function to return the script tags - something like:
public static string RegisterJS(this System.Web.Mvc.HtmlHelper helper, string scriptLib) { //get the directory where the scripts are string scriptRoot = VirtualPathUtility.ToAbsolute("~/Scripts"); string scriptFormat="<script src=\"{0}/{1}\" type=\"text/javascript\"></script>\r\n"; return string.Format(scriptFormat,scriptRoot,scriptLib); }
That's helpful, but I can do better :). I don't like "magic strings" and even though it's the name of a file - I'd rather use something else here. Also, if I have to use "Html.RegisterJS..." every line - I haven't really saved much.
The Code
To start, I'm going to create an Enum that I know will grow (you can put this anywhere - I use a file called "Enums" and am putting it in the Helpers folder), and I'll reference the names of my script libraries:
/// <summary> /// Enums which describe script libraries /// </summary> public enum ScriptLibrary { All, Default, MicrosoftAjax, MicrosoftAjaxMVC, }
Next, I buff out the RegisterJS function a bit...
/// <summary> /// A helper for registering script tags on an MVC View page. /// </summary> public static string RegisterJS(this System.Web.Mvc.HtmlHelper helper, ScriptLibrary scriptLib) { //get the directory where the scripts are string scriptRoot = VirtualPathUtility.ToAbsolute("~/Scripts"); string scriptFormat="<script src=\"{0}/{1}\" type=\"text/javascript\"></script>\r\n"; string scriptLibFile=""; string result = ""; System.Text.StringBuilder sb = new System.Text.StringBuilder(); //all of the script tags if (scriptLib == ScriptLibrary.All) { string[] scripts = Enum.GetNames(typeof(ScriptLibrary)); foreach (string script in scripts) { scriptLibFile = script + ".js"; sb.AppendFormat(scriptFormat, scriptRoot,scriptLibFile); } result = sb.ToString(); //just the defaults please } else if (scriptLib == ScriptLibrary.Default) { sb.AppendFormat(scriptFormat, scriptRoot,"MicrosoftAjax.js"); sb.AppendFormat(scriptFormat, scriptRoot,"MicrosoftAjaxMVC.js"); result = sb.ToString(); } else { //just the ones asked for scriptLibFile=Enum.GetName(typeof(ScriptLibrary),scriptLib)+".js"; result = string.Format(scriptFormat, scriptRoot,scriptLibFile); } return result; }
And there you are. Now I can reference my script libraries with one line, and I don't have to have a bulk of <script> tags everywhere:
<%=Html.RegisterJS(ScriptLibrary.Default) %>
I know the code above can be tightened some :) and I'd love to hear your ideas. I'm not sure how usable the "All" selection is - but I wanted to include so you could get the idea here... sort of a "thematic" way to register your scripts.
Always great to read your posts. I have to say that one of weekly highlights is awaiting and reading your next post of the ASP.NET MVC Storefront.
How about reducing server request by combinning all js file into a single response.
<script type="text/javascript" src="<%=Server.GetJS(...)%></script>
Unit tests?
Maybe instead of a plain enum, use the flags attribute so you can specify more than one library with a single call?
Rob, I am not sure how much value this is really adding?
Personally, I usually have 1, maybe 2, scripts files that should be included in every page of the site. So that goes in the MasterPage. Then there should be 0, 1, maybe 2 max, files that get included on a page by page basis that are specific to the functionality on that page. So you just added 20-25 lines of code that aren't really going to save me anything.
I actually like your original and simple solution better :)
Hi Rob,
On a completely unrelated note. Htf do i navigate your blog? I don't see any forward/back links to jump between consecutive posts, and there isn't a list of your n most recent posts. Do i have to go to your homepage to open a different post? Am I missing something?
On a related note... I do like using Enums instead of strings, they tend to convey more meaning. However, when it comes to registering javascript files the output sometimes needs to be ordered. For instance, i bet you'll need MicrosoftAjax.js to be output before MicrosoftAjaxMVC.js?
Also, with your code as it is, if you use ScriptLibrary.All you'll get tags for "All.js" and "Default.js" which i'm sure you didn't intend!
ScriptLibrary.All might just be bitwise of all other ScriptLibrary-s.
Nice idea, but using enums we cannot include scrips with characters like space, dot, minus etc.
Would be interesting how to deal with it.
@Ricky: the value is in not repeating yourself and readability :).
@tgmdbm: Good point :). Oops I knew there was a bug in there! RE navving - yah I need something in here. I'll fix that.
>>Nice idea, but using enums we cannot include scrips with characters like space, dot, minus<<<
Well yah - the idea here is you can append what you need when you need it. I took a shortcut with the enums (pulling the names) - you can do what you need to.
The main thing here is not having to "script up" every page - just have a central point.
I had an idea yesterday for a way to register scripts using ActionFilterAttributes. This way on your base controller you could have an attribute
[RegisterScript('~/js/jQuery.js')]
public class BaseController : Controller {}
[RegisterScript('~/js/dashboard.js')]
public class DashboardController : Controller
{
[RegisterScript('~/js/index.js')]
public ActionResult Index(){}
}
The RegisterScriptAttribute OnExecuting could stick each of these urls into HttpContext.Items. Then on Render your master page could use a helper class to render the scripts.
This would be nice cause you wouldn't have to include a whole lot of unnecessary javascript on each request.
What do you rekon?
Why not make ScriptLibrary an interface with a method like string[] GetScriptFileNames()? Then you and others can easily extend it and your extension method doesn't need to change every time you add a new script library. You can create static instances of each subclass elsewhere for convenience.
This would make your code follow OCP better than it currently does.
Talk about timing, I've been doing something REALLY similar lately. My approach varied slightly, however, I found that I've been creating my helper methods for specific purposes, like:
LoadRequiredScriptsAndStyles()
LoadEditorScriptsAndStyles()
LoadCalendarScriptsAndStyles()
So, the "required" one I use on the Master page, but the others get called only from the pages that need them. On a page with a big form? I call "LoadEditorScriptsAndStyles()", and I get all the scripts and styles I need to make my form to look and act how I want. Sometimes I end up using 2 or 3 jQuery plugins (and therefore have 2 or 3 script files). So anyway, just wanted to second this idea, because it's really been working for me lately.
@Jake: The problem is that the scripts are required by views, not bound to a controller action. MVC, you know ;)
Here's my version of your code after lots of LINQified refactoring.
public static class ScriptHelper
{
public enum ScriptLibrary
{
All,
Default,
MicrosoftAjax,
MicrosoftAjaxMVC,
}
private const string scriptFormat = "<script src=\"{0}/{1}\" type=\"text/javascript\"></script>\r\n";
private static readonly string[] defaultScripts = new string[] { "MicrosoftAjax", "MicrosoftAjaxMVC" };
private static readonly IDictionary<ScriptLibrary, Func<string, string>> functions = new Dictionary<ScriptLibrary, Func<string, string>>();
static ScriptHelper()
{
functions.Add(ScriptLibrary.All, GetAllScripts);
functions.Add(ScriptLibrary.Default, GetDefaultScripts);
}
public static string RegisterJS(this System.Web.Mvc.HtmlHelper helper, ScriptLibrary scriptLib)
{
string scriptRoot = VirtualPathUtility.ToAbsolute("~/Scripts");
return functions.ContainsKey(scriptLib) ? functions[scriptLib](scriptRoot) : GetScripts(scriptRoot, scriptLib);
}
private static string GetAllScripts(string scriptRoot)
{
return GetScriptReferences(scriptRoot, Enum.GetNames(typeof(ScriptLibrary)));
}
private static string GetDefaultScripts(string scriptRoot)
{
return GetScriptReferences(scriptRoot, defaultScripts);
}
private static string GetScripts(string scriptRoot, ScriptLibrary scriptLib)
{
return GetScriptReferences(scriptRoot, new string[] { Enum.GetName(typeof(ScriptLibrary), scriptLib) });
}
private static string GetScriptReferences(string scriptRoot, IEnumerable<string> scripts)
{
return scripts.Aggregate(string.Empty, (allScripts, script) => string.Concat(allScripts, string.Format(scriptFormat, scriptRoot, script + ".js")));
//return string.Join(string.Empty, scripts.Select(script => string.Format(scriptFormat, scriptRoot, script + ".js")).ToArray());
}
}
Well, how would you register new css or js's in one of the views? I mean, you may have subcontrollers/componentcontrollers which may need additional script library / css stuff which have to go to <head> of a page?
My way of doing it was to move the <head> tag under the <body> tag. Looks somehow ugly, there is a way to keep it in the body by string replace stuff(replacing {scripts} with the list of scripts forexample) but I really don't like it. Since html is a modified version of html, the order is not important most of the case(well it is important sometimes, the style of the page wont appear until page is completely loaded).
I just add items to a dictionary when RegisterJS is called and call RenderJS it in the header(which is after <body> tag).
Not perfect, but works for me, at least.