A Simple ScriptManager for ASP.NET MVC

The 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.