Created
June 29, 2012 16:51
-
-
Save johnnyreilly/3019159 to your computer and use it in GitHub Desktop.
HTML Helper for creating / rendering scripts when working in ASP.NET MVC (heavily based on Michael Ryans original work; see links enclosed)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Web; | |
using System.Web.Mvc; | |
using System.IO; | |
using System.Text; | |
namespace DummyMvc.Helpers | |
{ | |
/// <summary> | |
/// This helper makes creation of script tags within a view straightforward. | |
/// If the specified file is missing an error is raised at runtime. A v= suffix | |
/// is added to URLs using the VersionHelper to make caching behave as desired | |
/// both during development (where we don't want to cache as we make changes to code) | |
/// and during production (where we do want to cache to improve the user loading experience) | |
/// | |
/// There are 2 approaches available using this helper. | |
/// | |
/// 1. Straight script tag creation using the Script method. | |
/// | |
/// A usage of: | |
/// | |
/// @Html.Script("~/Scripts/jquery.js") | |
/// | |
/// Would immediatedly render a tag as below: (please note the v= suffix to the URL) | |
/// | |
/// <script src="/Scripts/jquery.tools.min.js?v=634765611410213405" type="text/javascript"></script> | |
/// | |
/// 2. Script bundling | |
/// | |
/// This approach allows you to build up the scripts to be rendered across multiple | |
/// views / partial views / layout etc and then have them rendered in one hit (ideally | |
/// just prior to the body tag being rendered as at this point the screen will be visually | |
/// set up for the user and any script loading that causes blocking should not be a presentational | |
/// issue). This is heavily based on Michael Ryans work; see links below. | |
/// | |
/// [Links] | |
/// http://frugalcoder.us/post/2009/06/29/Handling-Scripts-in-ASPNet-MVC.aspx | |
/// http://stackoverflow.com/questions/5598167/mvc3-need-help-with-html-helper | |
/// | |
/// [Usage] | |
/// --- From each view / partial view --- | |
/// @Html.AddClientScript("~/Scripts/jquery.js") | |
/// @Html.AddClientScript("~/Scripts/jquery-ui.js", dependancies: new string[] {"~/Scripts/jquery.js"}) | |
/// @Html.AddClientScript("~/Scripts/site.js", dependancies: new string[] {"~/Scripts/jquery.js"}) | |
/// @Html.AddClientScript("~/Scripts/views/myview.js", dependancies: new string[] {"~/Scripts/jquery.js"}) | |
/// | |
/// --- From the main Master/View (just before last Body tag so all "registered" scripts are included) --- | |
/// @Html.ClientScripts() | |
/// </summary> | |
public static class ScriptExtensions | |
{ | |
#region Public | |
/// <summary> | |
/// Render a Script element given a URL | |
/// </summary> | |
/// <param name="helper"></param> | |
/// <param name="url">URL of script to render</param> | |
/// <returns></returns> | |
public static MvcHtmlString Script( | |
this HtmlHelper helper, | |
string url) | |
{ | |
return MvcHtmlString.Create(MakeScriptTag(helper, url)); | |
} | |
/// <summary> | |
/// Add client script files to list of script files that will eventually be added to the view | |
/// </summary> | |
/// <param name="scriptPath">path to script file</param> | |
/// <param name="dependancies">OPTIONAL: a string array of any script dependancies</param> | |
/// <returns>always MvcHtmlString.Empty</returns> | |
public static MvcHtmlString AddClientScript(this HtmlHelper helper, string scriptPath, string[] dependancies = null) | |
{ | |
//If script list does not already exist then initialise | |
if (!helper.ViewContext.HttpContext.Items.Contains("client-script-list")) | |
helper.ViewContext.HttpContext.Items["client-script-list"] = new Dictionary<string, KeyValuePair<string, string[]>>(); | |
var scripts = helper.ViewContext.HttpContext.Items["client-script-list"] as Dictionary<string, KeyValuePair<string, string[]>>; | |
//Ensure scripts are not added twice | |
var scriptFilePath = helper.ViewContext.HttpContext.Server.MapPath(DetermineScriptToRender(helper, scriptPath)); | |
if (!scripts.ContainsKey(scriptFilePath)) | |
scripts.Add(scriptFilePath, new KeyValuePair<string, string[]>(scriptPath, dependancies)); | |
return MvcHtmlString.Empty; | |
} | |
/// <summary> | |
/// Add a script tag for each "registered" script associated with the current view. | |
/// Output script tags with order depending on script dependancies | |
/// </summary> | |
/// <returns>MvcHtmlString</returns> | |
public static MvcHtmlString ClientScripts(this HtmlHelper helper) | |
{ | |
var url = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection); | |
var scripts = helper.ViewContext.HttpContext.Items["client-script-list"] as Dictionary<string, KeyValuePair<string, string[]>> ?? new Dictionary<string, KeyValuePair<string, string[]>>(); | |
//Build script tag block | |
var scriptList = new List<string>(); | |
if (scripts.Count > 0) | |
{ | |
//Check all script dependancies exist and throw an exception if any do not | |
var distinctDependancies = scripts.Where(s => s.Value.Value != null) | |
.SelectMany(s => s.Value.Value) | |
.Distinct() | |
.Select(s => GetScriptFilePath(helper, s)) //Exception will be thrown here if file does not exist | |
.ToList(); | |
var missingDependancies = distinctDependancies.Except(scripts.Select(s => s.Key)).ToList(); | |
if (missingDependancies.Count > 0) | |
{ | |
throw new KeyNotFoundException("The following dependancies are missing: " + Environment.NewLine + | |
Environment.NewLine + | |
string.Join(Environment.NewLine, missingDependancies)); | |
} | |
//Serve scripts without dependancies first | |
scriptList.AddRange(scripts.Where(s => s.Value.Value == null).Select(s => s.Value.Key)); | |
//Get all scripts which have dependancies | |
var scriptsAndDependancies = scripts.Where(s => s.Value.Value != null) | |
.OrderBy(s => s.Value.Value.Length) | |
.Select(s => s.Value) | |
.ToList(); | |
//Loop round adding scripts to the scriptList until all are done | |
do | |
{ | |
//Loop backwards through list so items can be removed mid loop | |
for (var i = scriptsAndDependancies.Count - 1; i >= 0; i--) | |
{ | |
var script = scriptsAndDependancies[i].Key; | |
var dependancies = scriptsAndDependancies[i].Value; | |
//Check if all the dependancies have been added to scriptList yet | |
bool currentScriptDependanciesAdded = !dependancies.Except(scriptList).Any(); | |
if (currentScriptDependanciesAdded) | |
{ | |
//Move script to scriptList | |
scriptList.Add(script); | |
scriptsAndDependancies.RemoveAt(i); | |
} | |
} | |
} while (scriptsAndDependancies.Count > 0); | |
} | |
//Generate a script tag for each script | |
var scriptsToRender = scriptList.Select(s => MakeScriptTag(helper, s)).ToList(); | |
#if DEBUG | |
scriptsToRender.Insert(0, "<!-- BEGIN - HtmlHelperScriptExtensions.ClientScripts() -->"); | |
scriptsToRender.Insert(scriptsToRender.Count, "<!-- END - HtmlHelperScriptExtensions.ClientScripts() -->"); | |
#endif | |
//Output script tag block at point in view where method is called (by returning an MvcHtmlString) | |
return (scriptsToRender.Count > 0 | |
? MvcHtmlString.Create(string.Join(Environment.NewLine, scriptsToRender)) | |
: MvcHtmlString.Empty); | |
} | |
/// <summary> | |
/// Add client script block to list of script blocks that will eventually be added to the view | |
/// </summary> | |
/// <param name="key">unique identifier for script block</param> | |
/// <param name="scriptBlock">script block</param> | |
/// <param name="dependancies">OPTIONAL: a string array of any script block dependancies</param> | |
/// <returns>always MvcHtmlString.Empty</returns> | |
public static MvcHtmlString AddClientScriptBlock(this HtmlHelper helper, string key, string scriptBlock, string[] dependancies = null) | |
{ | |
//If script list does not already exist then initialise | |
if (!helper.ViewContext.HttpContext.Items.Contains("client-script-block-list")) | |
helper.ViewContext.HttpContext.Items["client-script-block-list"] = new Dictionary<string, KeyValuePair<string, string[]>>(); | |
var scriptBlocks = helper.ViewContext.HttpContext.Items["client-script-block-list"] as Dictionary<string, KeyValuePair<string, string[]>>; | |
//Prevent duplication | |
if (scriptBlocks.ContainsKey(key)) return MvcHtmlString.Empty; | |
scriptBlocks.Add(key, new KeyValuePair<string, string[]>(scriptBlock, dependancies)); | |
return MvcHtmlString.Empty; | |
} | |
/// <summary> | |
/// Output all "registered" script blocks associated with the current view. | |
/// Output script tags with order depending on script dependancies | |
/// </summary> | |
/// <returns>MvcHtmlString</returns> | |
public static MvcHtmlString ClientScriptBlocks(this HtmlHelper helper) | |
{ | |
var scriptBlocks = helper.ViewContext.HttpContext.Items["client-script-block-list"] as Dictionary<string, KeyValuePair<string, string[]>> ?? new Dictionary<string, KeyValuePair<string, string[]>>(); | |
//Build script tag block | |
var scriptBlockList = new List<string>(); | |
if (scriptBlocks.Count > 0) | |
{ | |
//Check all script dependancies exist and throw an exception if any do not | |
var distinctDependancies = scriptBlocks.Where(s => s.Value.Value != null) | |
.SelectMany(s => s.Value.Value) | |
.Distinct() | |
.ToList(); | |
var missingDependancies = distinctDependancies.Except(scriptBlocks.Select(s => s.Key)).ToList(); | |
if (missingDependancies.Count > 0) | |
{ | |
throw new KeyNotFoundException("The following dependancies are missing: " + Environment.NewLine + | |
Environment.NewLine + | |
string.Join(Environment.NewLine, missingDependancies)); | |
} | |
//Serve script blocks without dependancies first | |
scriptBlockList.AddRange(scriptBlocks.Where(s => s.Value.Value == null).Select(s => s.Value.Key)); | |
//Get all script blocks which have dependancies | |
var scriptBlocksAndDependancies = scriptBlocks.Where(s => s.Value.Value != null) | |
.OrderBy(s => s.Value.Value.Length) | |
.Select(s => s.Value) | |
.ToList(); | |
//Loop round adding scripts to the scriptList until all are done | |
do | |
{ | |
//Loop backwards through list so items can be removed mid loop | |
for (var i = scriptBlocksAndDependancies.Count - 1; i >= 0; i--) | |
{ | |
var scriptBlock = scriptBlocksAndDependancies[i].Key; | |
var dependancies = scriptBlocksAndDependancies[i].Value; | |
//Check if all the dependancies have been added to scriptList yet | |
bool currentScriptBlockDependanciesAdded = !dependancies.Except(scriptBlockList).Any(); | |
if (currentScriptBlockDependanciesAdded) | |
{ | |
//Move script to scriptList | |
scriptBlockList.Add(scriptBlock); | |
scriptBlocksAndDependancies.RemoveAt(i); | |
} | |
} | |
} while (scriptBlocksAndDependancies.Count > 0); | |
} | |
//Generate a script tag for each script | |
var scriptBlocksToRender = scriptBlockList.Select(s => string.Format("<script type=\"text/javascript\">{0}{1}</script>", Environment.NewLine, s)).ToList(); | |
#if DEBUG | |
scriptBlocksToRender.Insert(0, "<!-- BEGIN - HtmlHelperScriptExtensions.ClientScriptBlocks() -->"); | |
scriptBlocksToRender.Insert(scriptBlocksToRender.Count, "<!-- END - HtmlHelperScriptExtensions.ClientScriptBlocks() -->"); | |
#endif | |
//Output script tag block at point in view where method is called (by returning an MvcHtmlString) | |
return (scriptBlocksToRender.Count > 0 | |
? MvcHtmlString.Create(string.Join(Environment.NewLine, scriptBlocksToRender)) | |
: MvcHtmlString.Empty); | |
} | |
#endregion | |
#region Private | |
/// <summary> | |
/// Take a URL, resolve it and a version suffix. In Debug this will be based on DateTime.Now to prevent caching | |
/// on a development machine. In Production this will be based on the version number of the appplication. | |
/// This means when the version number is incremented in subsequent releases script files should be recached automatically. | |
/// </summary> | |
/// <param name="helper"></param> | |
/// <param name="url">URL to resolve and add suffix to</param> | |
/// <returns></returns> | |
private static string ResolveUrlWithVersion(HtmlHelper helper, string url) | |
{ | |
#if DEBUG | |
var suffix = DateTime.Now.Ticks.ToString(); | |
#else | |
var suffix = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); | |
#endif | |
var urlWithVersionSuffix = string.Format("{0}?v={1}", url, suffix); | |
var urlResolved = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection).Content(urlWithVersionSuffix); | |
return urlResolved; | |
} | |
/// <summary> | |
/// Create the string that represents a script tag | |
/// </summary> | |
/// <param name="helper"></param> | |
/// <param name="url"></param> | |
/// <returns></returns> | |
private static string MakeScriptTag(HtmlHelper helper, string url) | |
{ | |
var scriptToRender = DetermineScriptToRender(helper, url); | |
//Render script tag | |
var scriptTag = new TagBuilder("script"); | |
scriptTag.Attributes["type"] = "text/javascript"; //This isn't really required with HTML 5 as this is the default. As it does no real harm so I have left it for now. http://stackoverflow.com/a/9659074/761388 | |
scriptTag.Attributes["src"] = SharedExtensions.ResolveUrlWithVersion(helper, scriptToRender); | |
var scriptTagString = scriptTag.ToString(); | |
return scriptTagString; | |
} | |
/// <summary> | |
/// Author : John Reilly | |
/// Description : Get the script that should be served to the user - throw an exception if it doesn't exist and minify if in release mode | |
/// </summary> | |
/// <param name="helper"></param> | |
/// <param name="url"></param> | |
/// <param name="minifiedSuffix">OPTIONAL - this allows you to directly specify the minified suffix if it differs from the standard | |
/// of "min.js" - unlikely this will ever be used but possible</param> | |
/// <returns></returns> | |
private static string DetermineScriptToRender(HtmlHelper helper, string url, string minifiedSuffix = "min.js") | |
{ | |
//Initialise a list that will contain potential scripts to render | |
var possibleScriptsToRender = new List<string>() { url }; | |
#if DEBUG | |
//Don't add minified scripts in debug mode | |
#else | |
//Add minified path of script to list | |
possibleScriptsToRender.Insert(0, Path.ChangeExtension(url, minifiedSuffix)); | |
#endif | |
var validScriptsToRender = possibleScriptsToRender.Where(s => File.Exists(helper.ViewContext.HttpContext.Server.MapPath(s))); | |
if (!validScriptsToRender.Any()) | |
throw new FileNotFoundException("Unable to render " + url + " as none of the following scripts exist:" + | |
string.Join(Environment.NewLine, possibleScriptsToRender)); | |
else | |
return validScriptsToRender.First(); //Return first existing file in list (minified file in release mode) | |
} | |
#endregion | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
SharedExtensions.ResolveUrlWithVersion(line 295) and GetScriptFilePath(line 109) is still not replaced with new api in your latest revision, so code is breaking.