A Simple ScriptManager for ASP.NET MVC
Aug 13, 2009 • ASP.NETThe ASP.NET AJAX ScriptManager makes it really easy to include JavaScript references and register JavaScript blocks into the rendered Page output of an ASP.NET WebForms application. However nice the ScriptManager
control is, it’s still just a WebForms control for use with ASP.NET AJAX; thus it’s use isn’t really supported with ASP.NET MVC. Also, to make things just a little more difficult, ASP.NET MVC doesn’t have it’s own ScriptManager
implementation. This brings me to the point of posting this.
I have worked out a really simple ScriptManager
component for use with ASP.NET MVC, and I think it works really nice to help simplify the effort of including JavaScript blocks and references in a page.
Setting up the SimpleScriptManager
for use
To use the SimpleScriptManager
with ASP.NET MVC you must first Import the SimpleScriptManager
namespace into your Master Page. Then you must place a single line of code in the Master Page file at the location you want to Render the Script Includes and Blocks to the Page. In order for it to work properly, the Render code needs to be place at the very end of the Master Page; preferably just before the closing Body tag.
Here’s a really short example Master Page file with the SimpleScriptManager
namespace imported and the call to SimpleScriptManager().Render()
located at the very end of the page just before the closing Body tag.
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<%-- The SimpleScriptManager Namespace must be Imported to be able to use the Html.SimpleScriptManager Extension --%>
<%@ Import Namespace=`SimpleScriptManager` %>
<DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" />title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
<%-- Render all the Scripts to the Page --%>
<%-- Must be located at the very end of the Master Page to work properly --%>
<% Html.SimpleScriptManager().Render(); %>
</body>
</html>
This may look a little strange to you since you may be used to placing all your JavaScript Blocks and Script Includes at the top of the page within the <HEAD>
tags. However, in order for the SimpleScriptManager
to work property the call to Render to the page MUST be located at the end of the Master Page file. This allows any other server controls, user controls or pages to add Script Blocks and Includes at any time during the process or building/rendering the page, and then at the end of the Master Page (when the page is just about finished being rendered) the SimpleScriptManager().Render()
method is called and the scripts are all rendered out to the page at that time. If the SimpleScriptManager().Render()
method is called prior to all other components on the Page, then any Script Blocks or Includes added to the SimpleScriptManager
after Render is called will not be included within the final rendering of the Page that gets sent to the client.
Using the SimpleScriptManager
The SimpleScriptManager
has only two fairly simple methods: ScriptInclude
and Script
.
“SimpleScriptManager.ScriptInclude” Method
To add a simple Script Include within the page, you just call the ScriptManager.ScriptInclude
method and pass in the Location / Url of the JavaScript file to include within the page. The Script Location / Url can be either an Absolute or Virtual (“App Relative”) Url.
<% Html.SimpleScriptManager().ScriptInclude("~/Scripts/jquery-1.3.2.js"); %>
<% Html.SimpleScriptManager().ScriptInclude("http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"); %>
You can also pass in a Key
for the specific Script Include you’re registering. This key is a unique identifier used within your application for the specified Script Include, and it allows you to ensure that only a single include/reference to that specific script will get rendered within the Page.
<% Html.SimpleScriptManager().ScriptInclude("jquery", "~/Scripts/jquery-1.3.2.js"); %>
For instance the second example of ScriptInclude
above specifies the Key of jquery
. You would be able to include this ScriptInclude
call within any User Controls and/or Pages within your application that require that the jquery-1.3.2.js
script be included within the page to work, and no matter how many of those controls are rendered to the page, the script would only have a single include/reference rendered to the Page.
I know this isn’t a very good example of adding a script reference that may only be needed within a couple pages of an application, since you’ll most likely want jQuery included within every Page of your Application. To do this you’ll just add the ScriptInclude
call to top of the Master Page file itself. However, I’m sure you get the idea I’m trying to reference on how to “optionally” include a script reference only when it’s needed, instead of including it within every single page of your application by adding it within the Master Page file.
“SimpleScriptManager.ScriptInclude” Method to Add Web Resource References
One of the things necessary when building Custom Server Controls (instead of just User Controls) is the fact that they reside within an Assembly and contain scripts as Embedded Web Resources. This can cause issues when adding Script Include references for these controls since you need to load the script from the Embedded Web Resource into the Page.
However, this is really simple to do with an additional ScriptInclude
method overload that uses generics to specify the Assembly to find the Embedded Web Resource within, plus the full resource name to include. There is also a method overload that accept a unique “Key” for the script just like the above ScriptInclude
example.
To use these overloads of the ScriptInclude
method you must add a reference to the SimpleScriptManager
namespace within your custom control. Also, your Custom Control/Component must inherit from the ViewUserControl class so that it gets access to the HtmlHelper object through the Html property.
Here’s a really simple example of this:
// The SimpleScriptManager Namespace must be Imported to be able to use the Html.SimpleScriptManager Extension
using SimpleScriptManager;
// Specify that the "Embedded Resource" is to be a "Web Resource"
[assembly: System.Web.UI.WebResource("EmbeddedScriptResourceTest.TestScriptOne.js", "text/javascript")]
namespace EmbeddedScriptResourceTest
{
public class TestScriptOneControl : System.Web.Mvc.ViewUserControl
{
public string Message { get; set; }
public override void RenderControl(System.Web.UI.HtmlTextWriter writer)
{
base.RenderControl(writer);
// By specifying a Key when adding the ScriptInclude below, we are ensuring that the script only gets included
// within the Page once, no matter how many instances of this control are rendered to the Page.
this.Html.SimpleScriptManager().ScriptInclude(
"TestScriptOneKey",
"EmbeddedScriptResourceTest.TestScriptOne.js");
}
}
}
SimpleScriptManager.Script
Method
To add a Script Block in to the Page you just call the SimpleScriptManager.Script
method and pass it a String that contains the JavaScript code to include within the Page.
<% Html.SimpleScriptManager().Script("alert('Hello!');"); %>
You can also pass in a “Key” that uniquely identifies this specific Script Block. Just as with the ScriptInclude
method, this allows you to specify that you only want this particular Script Block to be included within the Page only once no matter how many times any components within the Page specify it to be added.
<% Html.SimpleScriptManager().Script("ScriptKey", "alert('Hello!');"); %>
SimpleScriptManager.Script
Method using a Lambda Expression
I also included the ability to pass the SimpleScriptManager.Script
method a Lambda Expression that will output the desired JavaScript code to the Page. This is something that makes it a little easier to add some Script to the Page and still be able to keep any code formatting in place (for readability) without requiring you to build it within a big, long String within the Page or User Control.
<% Html.SimpleScriptManager().Script( () => { %>
$(function(){
alert('Hello!');
});"
<% }); %>
This method also supports the ability to pass in a “Key” to specify you only want this script to be included within the Page a single time.
How SimpleScriptManager
Works
Besides the SimpleScriptManager
being included as an Extension Method to the HtmlHelper object; it also “attaches” itself to the HttpContext.Items
Dictionary the first time Html.SimpleScriptManager()
is called and then any subsequent calls just add any Script Includes or Blocks to that same SimpleScriptManager
instance. Then when you call the Render
method it writes out the entire Html code necessary to Render all the Script Includes and Blocks to the Page.
This is actually a fairly simple design, and the code that “attaches” the SimpleScriptManager
to the HttpContext
is includes within the HtmlHelper
Extension Method itself; the rest of the code is contained within the actual SimpleScriptManager
object.
Full SimpleScriptManager
Code
SimpleScriptManagerExtension.cs
// Copyright (c) 2009 Chris Pietschmann ()
// All rights reserved.
// Licensed under the Microsoft Public License (Ms-PL)
// http://opensource.org/licenses/ms-pl.html
using System.Web.Mvc;
namespace SimpleScriptManager
{
public static class SimpleScriptManagerExtensions
{
private static readonly string simpleScriptManagerKey = `SimpleScriptManager`;
public static SimpleScriptManager SimpleScriptManager(this HtmlHelper helper)
{
// Get SimpleScriptManager from HttpContext.Items
// This allows for a single SimpleScriptManager to be created and used per HTTP request.
var scriptmanager = helper.ViewContext.HttpContext.Items[simpleScriptManagerKey] as SimpleScriptManager;
if (scriptmanager == null)
{
// If SimpleScriptManager hasn't been initialized yet, then initialize it.
scriptmanager = new SimpleScriptManager(helper);
// Store it in HttpContext.Items for subsequent requests during this HTTP request.
helper.ViewContext.HttpContext.Items[simpleScriptManagerKey] = scriptmanager;
}
// Return the SimpleScriptManager
return scriptmanager;
}
}
}
SimpleScriptManager.cs
// Copyright (c) 2009 Chris Pietschmann ()
// All rights reserved.
// Licensed under the Microsoft Public License (Ms-PL)
// http://opensource.org/licenses/ms-pl.html
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
namespace SimpleScriptManager
{
public class SimpleScriptManager
{
private HtmlHelper htmlHelper;
private Dictionary<string, string> scriptIncludes = new Dictionary<string, string>();
private Dictionary<string, string> scripts = new Dictionary<string, string>();
private Dictionary<string, Action> scriptsActions = new Dictionary<string, Action>();
/// <summary>
/// SimpleScriptManager Constructor
/// </summary>
/// <param name="helper">The HtmlHelper that this SimpleScriptManager will use to render to.</param>
public SimpleScriptManager(HtmlHelper helper)
{
// Store reference to the HtmlHelper object this SimpleScriptManager is tied to.
this.htmlHelper = helper;
}
/// <summary>
/// Adds a script file reference to the page.
/// </summary>
/// <param name="scriptPath">The URL of the script file.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager ScriptInclude(string scriptPath)
{
return this.ScriptInclude(Guid.NewGuid().ToString(), scriptPath);
}
/// <summary>
/// Adds a script file reference to the page.
/// </summary>
/// <param name="key">A unique identifier for the script file.</param>
/// <param name="scriptPath">The URL of the script file.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager ScriptInclude(string key, string scriptPath)
{
if (!this.scriptIncludes.ContainsKey(key))
{
// Check if the scriptPath is a Virtual Path
if (scriptPath.StartsWith("~/"))
{
// Convert the Virtual Path to an Application Absolute Path
scriptPath = VirtualPathUtility.ToAbsolute(scriptPath);
}
this.scriptIncludes.Add(key, scriptPath);
}
return this;
}
/// <summary>
/// Adds a script file reference to the page for an Embedded Web Resource.
/// </summary>
/// <typeparam name="T">The Type whos Assembly contains the Web Resource.</typeparam>
/// <param name="key">A unique identifier for the script file.</param>
/// <param name="resourceName">The name of the Web Resource.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager ScriptInclude<T>(string key, string resourceName)
{
return this.ScriptInclude(key, getWebResourceUrl<T>(resourceName));
}
/// <summary>
/// Adds a script file reference to the page for an Embedded Web Resource.
/// </summary>
/// <typeparam name="T">The Type whos Assembly contains the Web Resource.</typeparam>
/// <param name="resourceName">The name of the Web Resource.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager ScriptInclude<T>(string resourceName)
{
return this.ScriptInclude(getWebResourceUrl<T>(resourceName));
}
/// <summary>
/// Adds a script block to the page.
/// </summary>
/// <param name="javascript">The JavaScript code to include in the Page.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager Script(string javascript)
{
return this.Script(Guid.NewGuid().ToString(), javascript);
}
/// <summary>
/// Adds a script block to the page.
/// </summary>
/// <param name="key">A unique identifier for the script.</param>
/// <param name="javascript">The JavaScript code to include in the Page.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager Script(string key, string javascript)
{
if (!this.scripts.ContainsKey(key) && !this.scriptsActions.ContainsKey(key))
{
this.scripts.Add(key, javascript);
}
return this;
}
/// <summary>
/// Adds a script block to the page.
/// </summary>
/// <param name="javascript">The JavaScript code to include in the Page.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager Script(Action javascript)
{
return this.Script(Guid.NewGuid().ToString(), javascript);
}
/// <summary>
/// Adds a script block to the page.
/// </summary>
/// <param name="key">A unique identifier for the script.</param>
/// <param name="javascript">The JavaScript code to include in the Page.</param>
/// <returns>Returns the SimpleScriptManager</returns>
public SimpleScriptManager Script(string key, Action javascript)
{
if (!this.scripts.ContainsKey(key) && !this.scriptsActions.ContainsKey(key))
{
this.scriptsActions.Add(key, javascript);
}
return this;
}
/// <summary>
/// Renders the SimpleScriptManager to the Page
/// </summary>
public void Render()
{
var writer = this.htmlHelper.ViewContext.HttpContext.Response.Output;
// Render All Script Includes to the Page
foreach (var scriptInclude in this.scriptIncludes)
{
writer.WriteLine(String.Format("<script type='text/javascript' src='{0}'></script>", scriptInclude.Value));
}
// Render All other scripts to the Page
if (this.scripts.Count > 0 || this.scriptsActions.Count > 0)
{
writer.WriteLine("<script type='text/javascript'>");
if (this.scripts.Count > 0)
{
foreach (var script in this.scripts)
{
writer.WriteLine(script.Value);
}
}
if (this.scriptsActions.Count > 0)
{
foreach (var script in this.scriptsActions)
{
script.Value();
}
}
writer.WriteLine("</script>");
}
}
private static MethodInfo _getWebResourceUrlMethod;
private static object _getWebResourceUrlLock = new object();
private static string getWebResourceUrl<T>(string resourceName)
{
if (string.IsNullOrEmpty(resourceName))
{
throw new ArgumentNullException("resourceName");
}
if (_getWebResourceUrlMethod == null)
{
lock (_getWebResourceUrlLock)
{
if (_getWebResourceUrlMethod == null)
{
_getWebResourceUrlMethod = typeof(System.Web.Handlers.AssemblyResourceLoader).GetMethod(
"GetWebResourceUrlInternal",
BindingFlags.NonPublic | BindingFlags.Static);
}
}
}
return "/" + (string)_getWebResourceUrlMethod.Invoke(null,
new object[] { Assembly.GetAssembly(typeof(T)), resourceName, false });
}
}
}
##
Conclusion
At first it seemed that the easiest way to get similar functionality to this was to use the ASP.NET AJAX ScriptManager control; however that control requires that it be embedded within a <form runat="server"></form>
tag, and that just doesn’t really work with ASP.NET MVC. Actually the methods to get the ASP.NET AJAX ScriptManager to work with ASP.NET MVC are just plain “Hacks” and they made me feel like I wasn’t being True to the new ASP.NET MVC Platform.
In the end, I’m very happy that I was able to work out an extremely simple solution to this problem that will definitely help when building out ASP.NET MVC Web Applications.
If you have any feedback on this, please leave a comment.