Skip to content

Instantly share code, notes, and snippets.

@GomesNayagam
Forked from mikaelo/ScriptExtensions.cs
Last active December 10, 2015 15:28
Show Gist options
  • Save GomesNayagam/4454410 to your computer and use it in GitHub Desktop.
Save GomesNayagam/4454410 to your computer and use it in GitHub Desktop.
fixed the compilation issue at two places which still calls the old method GetScriptFilePath and SharedExtension
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 => DetermineScriptToRender(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"] = 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