When I started doing more complicated things with ASP.NET MVC it was using Razor. In some ways that was unfortunate because some of these things were actually a little easier in prior versions. It starts to get complicated when you start composing partial views and multiple javascript files. First some Javascript files depend on other javascript files. And secondly partial views need certain scripts to be included that the main page doesn’t necessarily know about. The problem is that Razor doesn’t really deal with these things very well.
For this blog entry I am going to focus on getting JavaScript files included from partial views. This question has been asked numerous times on Stack Overflow
- http://stackoverflow.com/questions/863436/is-it-bad-practice-to-return-partial-views-that-contain-javascript
- http://stackoverflow.com/questions/912755/include-javascript-file-in-partial-views
- http://stackoverflow.com/questions/4707982/how-to-include-javasscript-from-a-partial-view-in-asp-net-mvc3
- http://stackoverflow.com/questions/5376102/mvc-partial-views-and-unobtrusive-jquery-javascript
- http://stackoverflow.com/questions/7556400/injecting-content-into-specific-sections-from-a-partial-view-asp-net-mvc-3-with
- http://stackoverflow.com/questions/11098198/is-it-ok-to-put-javascript-in-partial-views
In fact, I bet if you put all of the questions in 1 it would have quite a point total. But it is spread into so many slightly different questions that it is tough to quantify.
So first to define the problem. The ideal place for scripts is right before the close of the body tag. The default template’s master/layout view contains a scripts section for this purpose. Unfortunately sections can only be defined, not added to. So that means that the main view is the only one that can place script files in that section. It can get very awkward if there are script files that are very specific to the partial view, especially if the main view includes a number of partials. Basically the master view has to maintain the list of scripts needed by the entire tree of partial views.
Let’s make the problem more concrete. Let’s say I have three main view that include a partial view. That partial view uses another partial view. I change the leaf partial view so that I need some JavaScript. I have to find out where all of the views are that include me (but of course no view includes me directly), and add the script to those views. In short – YUCK.
While researching a solution to the problem, I came across a couple of promising solutions, namely:
http://stackoverflow.com/questions/5433531/using-sections-in-editor-display-templates/
And
http://forloop.co.uk/blog/managing-scripts-for-razor-partial-views-and-templates-in-asp.net-mvc
The first didn’t take into account paths, and the second was way too complicated in terms of how to use them, so I came up with this nice simple hybrid of the two solutions.
Here is an example of its use
Either at the top of the file or the web config, need to use the namespace
@using PartialsWithScripts.Helpers
To include a script in a partial view simple add it like so:
@{ Html.MyAddScriptFile("~/Scripts/App/contact.js"); }
Here is the code
public static class ScriptHelpers
{
const string ScriptContextKey = "ScriptContext";
public static void AddScript(this HtmlHelper htmlHelper, string path)
{
var scriptContext = GetScriptContext(htmlHelper);
scriptContext.Add(path);
}
public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
{
var httpContext = htmlHelper.ViewContext.HttpContext;
var scriptContext = httpContext.Items[ScriptContextKey] as HashSet<string>;
if (scriptContext != null)
{
var builder = new StringBuilder();
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext,
htmlHelper.RouteCollection);
foreach (var scriptFile in scriptContext)
{
builder.AppendLine("<script type='text/javascript' src='"
+ urlHelper.Content(scriptFile) + "'></script>");
}
return new MvcHtmlString(builder.ToString());
}
return MvcHtmlString.Empty;
}
private static HashSet<string> GetScriptContext(HtmlHelper htmlHelper)
{
var httpContext = htmlHelper.ViewContext.HttpContext;
var scriptContext = httpContext.Items[ScriptContextKey] as HashSet<string>;
if (scriptContext == null)
{
scriptContext = new HashSet<string>();
htmlHelper.ViewContext.HttpContext.Items[ScriptContextKey] = scriptContext;
}
return scriptContext;
}
}