Friday, October 26, 2012

Razor @Sections, Javascript and the conundrum of EditorTemplates

With the exception of when using Modernizr I always try to adhere to the web performance rules of css at the top of the page and javascript at the bottom. The razor @RenderSection(string name, bool required) helper class and the @Section keyword certainly helps with that goal within view pages.

I recently came across this problem in a project for partial views. For instance if I create a DateTime.cshtml to represent an EditorTemplate for DateTime properties on View Models. I can't just inject the relevant scripts for the jQuery UI Date picker extension using the @section Scripts method like you can do within normal views.

So how can a developer adhere to best practices for stylesheets and script files? By using an extension method.

Let's start with adding a couple of extension methods, Resource and RenderResources as described below.

public static class ResourceType
{
public const string Css = "css";
public const string Js = "js";
}
As you can see the Resource method takes a signature of Func<object, dynamic> template which simply can be @ so when template is executed the script tag is written into the page. The other parameter type is the type of resource being rendered "css" or "js", I've created a static helper class to remove the magic string.

public static IHtmlString Resource(this HtmlHelper htmlHelper, Func<object, dynamic> template, string type)
{
if (htmlHelper.ViewContext.HttpContext.Items[type] != null) ((List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[type]).Add(template);
else htmlHelper.ViewContext.HttpContext.Items[type] = new List<Func<object, dynamic>>() { template };
return new HtmlString(String.Empty);
}
public static IHtmlString RenderResources(this HtmlHelper htmlHelper, string type)
{
if (htmlHelper.ViewContext.HttpContext.Items[type] != null)
{
List<Func<object, dynamic>> resources = (List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[type];
foreach (var resource in resources)
{
if (resource != null) htmlHelper.ViewContext.Writer.Write(resource(null));
}
}
return new HtmlString(String.Empty);
}

In the layout:

//at the top of _Layout.cshtml
@RenderSection("Styles",false)
@Html.RenderResources(ResourceType.Css)
//at the bottom of _Layout.cshtml
@RenderSection("Scripts",false)
@Html.RenderResources(ResourceType.Js)
view raw _Layout.cshtml hosted with ❤ by GitHub

In the partial :

//for style sheets
@Html.Resource(@<link href="@Url.Content("~/Content/productSummary.css")" rel="stylesheet" />, ResourceType.Css)
//for script files
@Html.Resource(@<script src="@Url.Content("~/js/productSummary.js")" type="text/javascript" ></script>, ResourceType.Js)
view raw List.cshtml hosted with ❤ by GitHub

I haven't tested with Asp .net bundle as yet but with little or no tweaking that should work as well.