Dynamic Script Registering Component in ASP.NET MVC

One of the performance enhancements recommended in tools like YSlow and others is to put your JavaScript script files at the bottom of the page. The great difficulty in a web forms or MVC web application with doing that is scripts at the bottom of the page are typically added in the master page. Any pages or views can make use of a special section just for scripts, but partial views or user controls can’t take advantage of that.

I’m about to illustrate a solution that will do just that. It’s setup for MVC, but could be easily adapted for web forms by making the helpers as controls. You have to understand that MVC is a top down structure. So as views or partials render, they render their contents in place. The common approach is to have a section for scripts:

site.master:


  // master page common scripts


@* for any custom scripts in the view *@
@RenderSection("scripts", false)

The view can add a @section scripts { } to take advantage of this, but the partial view cannot. Additionally, everything is rendered in top down format; if a partial view at the top of your page has a script like:


$(function() { alert("In the partial view"); });

The problem with this script is that the final rendered view (when scripts are defined at the bottom) will look like:


$(function() { alert("In the partial view"); });


.
.
.
view content
. . // master page common scripts // view scripts

Since it’s top down, it’s also dependent on order, so scripts defined have no dependency (lacking a dependency feature like angular or script JS).

The following solution was meant to give more control over placement of defined scripts. Scripts are defined in a wrapper, and queued up for later rendering. The solution I’m providing is simple, but easily enhanced to add dependencies in scripts, custom ordering, and more features.

To give an idea of what we need to do, the system will achieve something like the following:

view.cshtml:

@Html.AddScript("ViewInit", () => @$(function() { .. }));

partial view:

@Helper Script()
{
   
     ..
   
}

@Html.AddScript("PartialInit", (m) => Script());

master:

@Html.PlaceScripts()

The idea is to add scripts via a method, which queues them up to be rendered, and writes them out at the point the PlaceScripts method is called. Note that for partial views, I’m using a little trick I noted earlier, whereby using a helper is the only way to circumvent exceptions that occur when using the action-based approach.

AddScript essentially adds the definitions to the HttpContext.Items collection, a common trick for storing singleton-like data elements that are available across the application. Since HttpContext.Items is available to add and then render scripts, it’s a great place to share content. The AddScript method shoves in data:

public void AddScript(HtmlHelper html, string key, Action fn)
{
    var http = html.ViewContext.HttpContext;
	var list = http.Items["_Scripts"] as Dictionary;
	
	if (list == null)
		list = new Dictionary();
	
	list[key] = fn;
	
	http.Items["_Scripts"] = list;
}

This method checks to see if we have a dictionary in the items collection; if not, it creates it, pushes in our item, and updates the collection. Similar, our other method, which takes a helper result, uses another overload to store the string result for easy rendering:

public void AddScript(this HtmlHelper html, string key, HelperResult fn)
{
	this.AddScript(html, key, fn(html.ViewData.Model).ToHtmlString());
}

public void AddScript(this HtmlHelper html, string key, string script)
{
    var http = html.ViewContext.HttpContext;
	var list = http.Items["_Scripts"] as Dictionary;
	
	if (list == null)
		list = new Dictionary();
	
	list[key] = script;
	
	http.Items["_Scripts"] = list;
}

We could easily add dependencies and ordering requirements to this solution if we needed; for now, let’s finish this out by showing how to grab the contents and render them:

public void PlaceScripts(this HtmlHelper html)
{
var http = html.ViewContext.HttpContext;
var list = http.Items[“_Scripts”] as Dictionary;

if (list == null)
return;

using writer = new HtmlTextWriter(html.ViewContext.Writer)
{
foreach (var entry in list)
{
if (typeof(entry.Value) is string) {
writer.WriteLine(entry.Value);
}
else {
//This is an action; the action will be
//rendered in the current output stream, and
//do not need to be directly written
entry.Value(html.ViewData.Model);
}
}
}

}

This would need greatly enhanced for dependencies and sorting; right now, we take the list in order and render it out. For HTML results or just plain string definitions, it gets rendered to the current response stream. Actions get processed the same way, but MVC handles that internally, rather than your code.

And voila, we are done; now we can define scripts in the view and partial views and expect them to be rendered where we want them placed, and have a component that can be easily enhanced to do much more.