ASP.net MVC – Defining Scripts in Partial Views

One limitation of ASP.net MVC that i’m not a fan of is the fact that you can not define scripts in a partial view and then have them rendered at the bottom of the page. In a view this can be done using a Scripts section and then calling RenderSection in you layout. The problem though is you can not use sections in partial views. This is apparently by design but i don’t think this make a lot of sense. Consider a page that contains a right margin. in that margin you can have a varying number if widgets such as a Twitter Feed that can be loaded into that area of the page. It makes sense to create these as partial views.  What doesn’t make sense to me is that the page should not need to know about the inner workings of the twitter feed. Its possible that you could setup the widgets to be customisable by the user so there would be a good chance that widget is never loaded. To me it makes more sense to contain all the javascript that is required for that widget to function within the partial view (or better just a script link to a JS that is defined in the partial so its only used when needed)

So i did some digging and found a way around this by effectively writing your own section for rendering partial scripts. I’ve made some adjustment and bug fixes to end up with what is below. Its made up of a couple of parts

A Html helper that is used to define your scripts in your partial views. This stores it in HttpContext.Items so that it can be accessed later in the workflow when its generating the Layout, allowing us to render the script at the bottom of the page after any dependencies it may have.

 public static class HtmlHelperExtensions
 {
 /// <summary>
 /// Adds a partial view script to the Http context to be rendered in the parent view
 /// </summary>
 public static MvcHtmlString Script(this HtmlHelper htmlHelper, Func<object, HelperResult> template)
 {
 htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;
 return MvcHtmlString.Empty;
 }
}

With the usage

@Html.Script(
 @<script type="text/javascript">
 $(document).ready(function() {
 //Initialise twitter widget here
 });
 </script>
)

A Html helper that is used to Read the scripts stored in HttpContext.Items and Render your scripts in the layout

 public static class HtmlHelperExtensions
 {
 /// <summary>
 /// Renders any scripts used within the partial views
 /// </summary>
 /// <returns></returns>
 public static IHtmlString RenderPartialViewScripts(this HtmlHelper htmlHelper)
 {
 foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
 {
 if (key.ToString().StartsWith("_script_"))
 {
 var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object, HelperResult>;
 if (template != null)
 {
 htmlHelper.ViewContext.Writer.Write(template(null));
 }
 }
 }
 return MvcHtmlString.Empty;
 }
}

With the usage in your layout

  <div class="page">
 <main>
 @RenderBody()
 </main>
 </div>
 @Scripts.Render("~/jquery")
 @Html.RenderPartialViewScripts()

This works great, except when you’re loading a partial over an Ajax request. Because your Layout is never hit you won’t get your javascript with it. So i added a 3rd piece to the puzzle that appends the scripts to the response of the ajax request

A Filter to append the scripts to the end of an ajax request

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web.Mvc;
using System.Web.WebPages;

namespace MyWebApp
{
 public class RenderAjaxPartialScriptsResponseFilter : MemoryStream
 {
 private readonly Stream _response;
 private readonly ActionExecutingContext _filterContext;

 public RenderAjaxPartialScriptsResponseFilter(Stream response, ActionExecutingContext filterContext)
 {
 _response = response;
 _filterContext = filterContext;
 }

 public override void Write(byte[] buffer, int offset, int count)
 {
 _response.Write(buffer, offset, count);
 }

 public override void Flush()
 {
 var scriptsHtml = GetScripts();
 var buffer = Encoding.UTF8.GetBytes(scriptsHtml);
 _response.Write(buffer, 0, buffer.Length);
 base.Flush();
 }

 private string GetScripts()
 {
 string html = "";
 var itemsToRemove = new List<object>();
 foreach (object key in _filterContext.HttpContext.Items.Keys)
 {
 if (key.ToString().StartsWith("_script_"))
 {
 var template = _filterContext.HttpContext.Items[key] as Func<object, HelperResult>;
 if (template != null)
 {
 html += (template(null));
 itemsToRemove.Add(key);
 }
 }
 }
 foreach (var key in itemsToRemove)
 _filterContext.HttpContext.Items.Remove(key);

 return html;
 }
 }

 /// <summary>
 /// Appends partial view scripts to the html response of an AJAX request
 /// </summary>
 public class RenderAjaxPartialScriptsAttribute : ActionFilterAttribute
 {
 public override void OnActionExecuting(ActionExecutingContext filterContext)
 {
 if (filterContext.HttpContext.Request.IsAjaxRequest())
 {
 var response = filterContext.HttpContext.Response;
 if (response.Filter != null)
 response.Filter = new RenderAjaxPartialScriptsResponseFilter(response.Filter, filterContext);
 }
 }
 }
}

Which you you can declare on the actions that will be called via AJAX like this


[RenderAjaxPartialScripts]
 public ActionResult ChangeWidget(string name)
 {

//do stuff to get widget
return PartialView(viewModel);


 }

 

Advertisements
  1. #1 by Simon Ordo on November 18, 2015 - 1:29 am

    Any idea how to do this in MVC vNext , i.e. MVC 6?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: