This article will (hopefully) provide a helpful solution for handling the differences between standard HTTP requests and Xml Http Requests, or Ajax, while calling an action from a Monorail view. The intent is to preserve separation of concerns between our view and controller code; specifically, to be able to code our actions without being coupled to the way those actions are invoked.
Abstract
Requests made via Xml Http may require different behavior by the called actions or may otherwise require different presentation results; i.e., layouts typically wouldn't be desired in a 'popup' window. A common way to ascertain the request type (ajax or standard) is to search the HttpContext request headers collection for the implemented ajax libraries' key/value pair. For example, prototype & jquery add a header 'X-Requested-With' with the value 'XMLHttpRequest'. A primitive solution might set a property on a service or base controller class.
There are two key problems with this solution. First, there currently isn't a standard on headers used by all the ajax libraries. Second, some browsers (ie, Firefox) don't persist XHTTP headers after redirects. This can cause unexpected results when querying our IsAjax property if the value isn't persisted between redirects in a single unit of work (for lack of a better term).
The code presented here resolves all these issues and allows you to deal with various ajax library settings transparently. First, we take advantage of Monorail's IMonorailExtension interface to encapsulate our Ajax evaluation code. Next, we use a filter to show how to modify view behaviour at runtime.
Creating the extension
Goals:
- We may be using more than one library, so let's allow a way to tell our extension what headers could possibly be present to determine whether the request is ajax or not.
- Let's store our result in Flash to be accesible and take advantage of it's built-in redirect persistence
First, let's provide the following ways of registering our config in monorail configuration extensions section.
Configuration - Basic
This uses the default ajax headers found in prototype and jquery.
<monorail smtpHost="yoursmtphost" useWindsorIntegration="true"> <viewEngine viewPathRoot="views" xhtmlRendering="true" customEngine="Castle.MonoRail.Views.Brail.BooViewEngine, Castle.MonoRail.Views.Brail"/> <extensions> <extension type="App.Controller.Extensions.AjaxRequestExtension, App.Controller"/> </extensions> </monorail>
This allows us to specify what header to look for to determine if ajax request.
Configuration - Inline header assignment
<monorail smtpHost="yoursmtphost" useWindsorIntegration="true"> <viewEngine viewPathRoot="views" xhtmlRendering="true" customEngine="Castle.MonoRail.Views.Brail.BooViewEngine, Castle.MonoRail.Views.Brail"/> <extensions> <extension type="App.Controller.Extensions.AjaxRequestExtension, App.Controller" headerKey="X-Requested-With" headerValue="XMLHttpRequest" /> </extensions> </monorail>
Configuration - Multiple headers
This allows us to specify multiple headers that may indicate an ajax request.
<monorail smtpHost="yoursmtphost" useWindsorIntegration="true"> <viewEngine viewPathRoot="views" xhtmlRendering="true" customEngine="Castle.MonoRail.Views.Brail.BooViewEngine, Castle.MonoRail.Views.Brail"/> <extensions> <extension type="App.Controller.Extensions.AjaxRequestExtension, App.Controller"> <headers> <header key="X-Requested-With" value="XMLHttpRequest" /> <header key="XCustom" value="XValue" /> </headers> </extensions> </monorail>
Now that we have our configuration, let's implement IMonorailExtension and let Monorail handle the rest for us.
First, the code:
using System; using System.Collections.Generic; using Castle.Core.Configuration; using Castle.MonoRail.Framework; using Castle.MonoRail.Framework.Configuration; namespace App.Controller.Extensions { /// <summary> /// This extension evaluates incoming requests and by default persists the result between redirects in /// Flash['is.ajax']. /// </summary> /// To successfully install this extension you must at least register /// it on the <c>extensions</c> node and (optionally) the headers within the <c>extension</c> node. /// <code> /// <monorail> /// <extensions> /// <extension type="Cei.Phoenix.Web.Controller.Extensions.AjaxRequestExtension, Cei.Phoenix.Web.Controller" /> /// </extensions> /// </monorail> /// </code> /// You may specify the headers to search for the ajax library you are implementing either inline: /// <code> /// <monorail> /// <extensions> /// <extension type="Cei.Phoenix.Web.Controller.Extensions.AjaxRequestExtension, Cei.Phoenix.Web.Controller" headerKey="X-Requested-With" headerValue="XMLHttpRequest" /> /// </extensions> /// </monorail> /// </code> /// Or with a collection of headers: /// <code> /// <monorail> /// <extensions> /// <extension type="Cei.Phoenix.Web.Controller.Extensions.AjaxRequestExtension, Cei.Phoenix.Web.Controller" > /// <header key="X-Requested-With" value="XMLHttpRequest" /> /// <header key="MyCustomHeader" value="MyValue" /> /// </extension> /// </extensions> /// </monorail> /// </code> public class AjaxRequestExtension : IMonoRailExtension { private const string defaultHeaderKey = "X-Requested-With"; private const string defaultHeaderValue = "XMLHttpRequest"; public static string IsAjaxKey = "is.ajax"; private IDictionary<string, string> headers; #region IMonoRailExtension Members public void SetExtensionConfigNode(IConfiguration node) { headers = new Dictionary<string, string>(); if (node.Attributes["headerKey"] != null) { headers.Add(node.Attributes["headerKey"], node.Attributes["headerValue"]); } else { IConfiguration headersConfig = node.Children["headers"]; if (headersConfig != null) { foreach (IConfiguration headerConfig in headersConfig.Children) { string key = headerConfig.Attributes["key"]; string value = headerConfig.Attributes["value"]; headers.Add(key, value); } } else { headers.Add(defaultHeaderKey, defaultHeaderValue); } } } public void Service(IMonoRailServices serviceProvider) { ExtensionManager manager = (ExtensionManager) serviceProvider.GetService(typeof (ExtensionManager)); IMonoRailConfiguration config = (IMonoRailConfiguration) serviceProvider.GetService(typeof (IMonoRailConfiguration)); manager.PreControllerProcess += PreControllerProcess; manager.PostControllerProcess += PostControllerProcess; } #endregion protected virtual void PreControllerProcess(IEngineContext context) { ProcessContext(context); } protected virtual void PostControllerProcess(IEngineContext context) { CleanUp(context); } /// <summary> /// Template for evaluating and setting the header discovery within a request. /// </summary> /// <param name="engineContext"></param> protected virtual void ProcessContext(IEngineContext engineContext) { bool shouldInspect = ShouldInspectHeadersForAjax(engineContext); bool isAjax = false; if (!shouldInspect) { isAjax = PreviousRequestWasAjax(engineContext); } else { isAjax = RequestHasAjaxHeader(engineContext); } SetIsAjax(engineContext, isAjax); } /// <summary> /// If previous ajax evaluation results are persisted (by default in Flash) then no further evaluation will /// be done to accomodate redirects within an ajax call. /// </summary> /// <param name="engineContext"></param> /// <returns></returns> protected virtual bool PreviousRequestWasAjax(IEngineContext engineContext) { bool isAjax = (bool)engineContext.Flash[IsAjaxKey]; return isAjax; } /// <summary> /// Determines if the headers should be searched for ajax headers. Firefox doesnt persist /// Xhttp headers between redirects, requiring us to persist the evaluation on our own. /// </summary> /// <param name="engineContext"></param> /// <returns></returns> protected virtual bool ShouldInspectHeadersForAjax(IEngineContext engineContext) { return !engineContext.Flash.ContainsKey(IsAjaxKey); } /// <summary> /// Sets the final evaluation on whether the request is ajax or not. By default persists /// the findings in Flash['is.ajax']. /// </summary> /// <param name="engineContext"></param> /// <param name="isAjax"></param> protected virtual void SetIsAjax(IEngineContext engineContext, bool isAjax) { engineContext.Flash[IsAjaxKey] = isAjax; } /// <summary> /// Searches request headers for at least one of the key/value pairs specified in config (optional) /// or the default 'X-Requested-With/XMLHttpRequest' pair. /// </summary> /// <param name="engineContext"></param> /// <returns></returns> protected virtual bool RequestHasAjaxHeader(IEngineContext engineContext) { foreach (KeyValuePair<string, string> pair in headers) { string header = engineContext.Request.Headers.Get(pair.Key); if (header != null && header.Trim() == pair.Value.Trim()) { return true; } } return false; } /// <summary> /// Cleans up the flash entry if the last response wasn't a redirect. /// </summary> /// <param name="engineContext"></param> protected virtual void CleanUp(IEngineContext engineContext) { if (!engineContext.Response.WasRedirected) { engineContext.Flash.Remove(IsAjaxKey); } } } }
Let's explain this extension and how we might use it.
The SetExtensionNode simply determines how you have configured the extension in the monorail section (above).
The Service implementation of IMonorailExtension simply registers with the ExtensionManager's Pre- and Post-ControllerProcess events (trunk only).
ProcessContext is a templated method for allowing you to override its steps to make it accessible in your code. As mentioned previously, we must persist the initial ajax evaluation between redirects to be consistent and make up for browser/ajax library request differences. This means it is evaluated only once and then persisted until cleaned up in the CleanUp() method.
Finally, we store our results in a flash item 'is.ajax'. This can be accessed directly or wrapped in a context adapter service to be friendlier:
/// <summary> /// Determines whether the current request is an XHTTP request. /// </summary> /// <value></value> public bool IsAjax { get { return (bool)MonoRailHttpHandlerFactory.CurrentEngineContext.Flash["is.ajax"]; } }
The CleanUp method clears the Flash entry since it should not persist except between redirects.
Common Usage
Views rendered during an ajax request often should not include the layout set on the controller, but dealing with layout issues within actions can muddy their intent. So let's take advantage of a Filter implementation to consume our ContextAdapter:
Here is my AjaxFilter which is assigned on the base controller in my project using the Filter attribute:
using System; using Castle.MonoRail.Framework; namespace Cei.Phoenix.Web.Controller.Filters { public class AjaxFilter : Filter { private readonly IContext appContext; public AjaxFilter(IContext context) { appContext = context; } protected override bool OnBeforeAction(IEngineContext context, IController controller, IControllerContext controllerContext) { if (appContext.IsAjax) { controllerContext.LayoutNames = new string[] {};//when using branch controller.LayoutName = null;//when using current trunk } return true; } } }
This is stripping out the layout just before the action is called. That way we can write actions without being concerned HOW they are invoked.
Please note that in the code above IContext is being resolved automatically by WindsorIntegration in Monorail.
Also, the code you are consuming will determine how the layout is removed, as indicated in the comments.
Finally, to decorate my controller I use:
[Filter(ExecuteEnum.Always,typeof(AjaxFilter),ExecutionOrder = 0)] public abstract class ControllerBase : SmartDispatcherController { //mo' code }
Conclusion
This hopefully presents a scalable solution for you to deal with the differences in request types transparently while allowing for a greater separation of concerns.
