Sunday, 25 February 2018

Return JSON from Sitecore rendering controller

As well as displaying the appropriate HTML and content, a Sitecore rendering controller can also be used to handle to form posts. As such you can have a single controller responsible for rendering the mark-up of a form and handling the POST request that's triggered when the form is submitted.

With this setup, I wanted to add some progressive enhancement and use the same form processing logic to handle an AJAX request. Using JavaScript we could hijack the form submission and make an AJAX request using the Fetch API. In the controller action method we can detect that an AJAX request is being made, and if so return a small JSON response, instead of the standard redirect back to the page to display a confirmation message.

This is quite a common pattern with ASP.Net MVC, making use of the Request.IsAjaxRequest() method available in the controller.

(As a quick aside, in order to use this method in an AJAX request triggered via the Fetch API, it's necessary to add a header that's used for the AJAX detection. It's also necessary to add the credentials option, to ensure cookies are passed, so we can make use of the CSRF protection offered by the ValidateAntiforgeryToken attribute).

        method: 'POST',
        body: data,
        credentials: 'include',
        headers: {
            'X-Requested-With': 'XMLHttpRequest'

When examining the response returned though, I was disappointed to discover that rather than the short JSON payload, what was actually coming back in the response was a big chunk of HTML, then the JSON, then further HTML. The reason of course was that we aren't dealing here with a standard MVC controller, rather a Sitecore one responsible for a rendering. This is executed as part of an existing page lifecycle, and so the JSON is getting output as content, within the context of a page output, as explained clearly here in Martina Welander's blog post.

To resolve this, I didn't really want to have to use a separate controller, defined with a custom route, to handle the AJAX requests - as we lose then the simplicity of having one location to post to from the client side, and one set of processing code. Instead I took the following approach:

  • On form processing, detect the AJAX request in the controller action method responsible for handling the form submission using Request.IsAjaxRequest()
  • Create an anonymous object representing the JSON response to return and serialize to a string
  • Pass that string to a custom ActionResult, which utilises HttpContext.Server.TransferRequest to end processing of the full page and pass the value to a new controller configured with a custom route
  • That controller then returns the JSON response, on it's own, with the appropriate content type

The relevant code is shown below, firstly the custom ActionResult:

    using System.Collections.Specialized;
    using System.Web.Mvc;

    public class TransferToJsonResult : ActionResult
        public TransferToJsonResult(string serializedJson)
            SerializedJson = serializedJson ?? string.Empty;

        public string SerializedJson { get; }

        public override void ExecuteResult(ControllerContext context)
            // Create a header to hold the serialized JSON value
            var headers = new NameValueCollection
                    ["SerializedJson"] = SerializedJson

            // And pass in the transfer request so it's available to the controller
            // that picks up generating the response.
            context.HttpContext.Server.TransferRequest("/JsonResponse", false, null, headers);

And then the controller that control is transferred to, in order to return the JSON response:

    using System.Web.Mvc;

    public class JsonResponseController : Controller
        public ContentResult Index()
            // Retrieve JSON to render from header
            var serializedJson = Request.Headers["SerializedJson"] ?? string.Empty;

            Response.ContentType = "application/json";
            return Content(serializedJson);

And finally an excerpt form the processing logic that handles the transfer behaviour:

    if (Request.IsAjaxRequest())
       var serializedJson = JsonConvert.SerializeObject(new { success = true });
       return new TransferToJsonResult(serializedJson);

No comments:

Post a Comment