The Problem
A while back, while working in the MVC framework, I was longing for the ClientScriptManager.RegisterClientScriptInclude() and RegisterClientScriptBlock() functionality of traditional ASP.Net web forms. We were doing a lot of work in jQuery, both using standard plug-ins like AutoComplete and writing our own stuff and so there were a lot of scripts to manage.
Moreover, we were using templates as described by Brad Wilson. Some of these templates depend on scripts and often the templates appear several times on one page. This of course leads to the exact situation that RegisterClientScriptInclude() and RegisterClientScriptBlock() are intended to solve: how to keep page components that render their own script tags, and appear several times on one page, from rendering duplicate script tags, and worse, duplicate script blocks and function definitions.
The Challenge
So what is a developer to do when your framework doesn’t support you? Well, not to be defeated, I did some digging into the MVC and ASP.Net frameworks and came up with a solution that essentially does the same thing as the ClientScriptManager.
What I needed was:
- A means during the rendering process to collect the scripts that need rendering.
- A means after the rendering process is done to insert the collected scripts at any arbitrary location in the rendered output.
Collecting the scripts was easy, so I set out in search of how to modify the response stream post rendering.
The Solution
What I found was Http Response Filters. You can read the same article I did, so I won’t repeat the details, but it does exactly what I wanted. It lets you modify the response stream any way you like, just before it goes back to the browser.
So with a promising solution to the hard part in hand, I solved the easy part with a pair of extension methods to the ubiquitous HtmlHelper, one to collect the content to be rendered (and eliminate duplicates) and one to insert markers into the response stream that the filter would then replace, post rendering, with the collected content.
So without further adieu, here is…
The Code
Tests First
Following TDD, I started out with a test that I want to pass.
[TestFixture]
public class IncludeRegistrationTests
{
[Test]
public void Registration_of_a_javascript_url_should_render_as_a_script_reference()
{
var htmlHelper = new HtmlHelper(null, null);
htmlHelper.RegisterInclude("MyJavascript.js");
var renderedHtml = IncludeRegistration
.RenderIncludes(IncludeType.JavaScript);
Assert.That(
renderedHtml,
Is.EqualTo("\r\n<script type=\"text/javascript\" src=\"MyJavascript.js\" />\r\n"));
}
}
public enum IncludeType { JavaScript, Css }
public static class IncludeRegistration
{
private static string _include;
public static void RegisterInclude(this HtmlHelper htmlHelper, string include)
{
throw new NotImplementedException();
}
public static string RenderIncludes(IncludeType includeType)
{
throw new NotImplementedException();
}
} |
Several things can been inferred about how I want includes to work from this test and the stub code needed to allow it to compile.
- I want to be able to register includes anywhere that I have access to the HtmlHelper using the syntax Html.RegisterInclude(…).
- I was to just register a url, without all the boilerplate HTML and…
- If the registered URL ends in “.js”, I want the renderer to automatically render it as a valid script referenece.
What if the url does not end in “.js”? Well that getting into future, yet unwritten tests, but my intention is that, for example, a url ending “.css” would renderer as a style sheet reference.
Get to Green
My first pass at getting the test looks like this:
public static void RegisterInclude(this HtmlHelper htmlHelper, string include)
{
htmlHelper.ViewContext.HttpContext.Items["Include"] = include;
}
public static string RenderIncludes(IncludeType includeType)
{
var httpContext = ServiceLocator.Current.GetInstance<HttpContextBase>();
var include = httpContext.Items["Include"];
return string.Format("\r\n<script type=\"text/javascript\" src=\"{0}\" />\r\n", include);
} |
As you can see I am using the HttpContext.Items collection as the storage location for my registered includes. This is the appropriate thread safe, request-scoped placed to cache things in ASP.Net. For those not familiar with this technique, more can be read about it here.
I am also using the Microsoft Patterns and Practices ServiceLocator facility to access the HttpContext in situations where I don’t have an HtmlHelper (as will be the case from within the http response filter).
You might think I could just call HttpContext.Current. And in the real runtime environment I could, but not inside a test harness. In the context of an automated test, we have to provide the HttpContext. Which causes our test to now look something like this:
[TestFixture]
public class IncludeRegistrationTests
{
private HtmlHelper _htmlHelper;
private HttpContextBase _httpContext;
[SetUp]
public void SetupBeforeEachTest()
{
//Create a mock HttpContext that supports
//getting and setting values in its Items collection.
_httpContext = MockRepository.GenerateMock<HttpContextBase>();
_httpContext.Stub(x => x.Items).Return(new Dictionary<object, object>());
//Wrap the HttpContext in a ViewContext and give it to the HtmlHelper
var viewContext = MockRepository.GenerateStub<ViewContext>();
viewContext.HttpContext = _httpContext;
//We don't need this mock but we can't pass null to HtmlHelper()
var viewData = MockRepository.GenerateMock<IViewDataContainer>();
_htmlHelper = new HtmlHelper(viewContext, viewData);
//Also register the HttpContext with the service locator.
var serviceLocator = new MyIocContainer();
serviceLocator.RegisterInstance(_httpContext);
ServiceLocator.SetLocatorProvider(() => serviceLocator);
}
[Test]
public void Registration_of_a_javascript_url_should_render_as_a_script_reference()
{
_htmlHelper.RegisterInclude("MyJavascript.js");
var renderedHtml = IncludeRegistration
.RenderIncludes(IncludeType.JavaScript);
Assert.That(
renderedHtml,
Is.EqualTo("\r\n<script type=\"text/javascript\" src=\"MyJavascript.js\" />\r\n"));
}
} |
Now the purpose for using the Microsoft Patterns and Practices ServiceLocator interface is that it abstracts the code from being tied to any particular IoC Container. So you can take this code and use it with your container of choice. Since I don’t need anything fancy and didn’t feel like adding unneeded dependancies, I just wrote my own container for our purposes here:
public class MyIocContainer : IServiceLocator
{
private readonly Dictionary<Type, Object> _cache = new Dictionary<Type, object>();
public void RegisterInstance<TService>(TService instance)
{
_cache[typeof(TService)] = instance;
}
#region IServiceLocator Implementation
public TService GetInstance<TService>()
{
return (TService)_cache[typeof(TService)];
}
//Implemenation of remaining interface members omitted for brevity.
//They all throw a NotImplementedException.
#endregion
} |
Next Steps
So now the test passes. But obviously this implementation has some issues; every time RegisterInclude() is called, it is going to overwrite the previous registration for one. For another, passing in something other than a javascript url would not have the desired effect. But at least this gets our test passing. Which is good enough for now. I want to get a full vertical slice of functionality working before I broaden out with more features.
So lets move on to the RenderIncludes function…
(Part 2 Coming Soon…)