Building a Single Page Application in Sitecore MVC

13 June 2014

Introduction

In this article I explain how I went about building a proof of concept single page application with Sitecore MVC. I decided to go with SammyJs for the client-side routing, since it is a very light-weight library which was just enough to meet my needs. I’m sure it would be possible to use Backbone.js or AngularJS but these are both much bigger libraries and I had no need of the HTML templating features as all content would be delivered as HTML straight from Sitecore.

1) Using Unobtrusive Ajax and the Ajax.BeginForm()/BeginRouteForm() Extension Methods - Conclusion: Possible, but has limitations

This Solution at first glance seems great as its possible to use all the ‘out-of-the-box’ features which come with MVC i.e. unobtrusive ajax and the Ajax.BeginForm/Ajax.BeginRouteForm extension methods. With a plain MVC app it’s very simple to just define your forms with Ajax.BeginForm() specifying a target element to replace and letting the controller handle the logic of which partial view to serve up - the problem with this approach is that this all happens within the context of a single page, so if the user clicks back in their browser, they disappear off the page altogether! Attempting this approach with controller/view renderings in Sitecore is possible, but there are numerous gotchas, described by Martina Welander in these two excellent articles:
http://mhwelander.net/2014/05/30/posting-forms-in-sitecore-mvc-part-2-controller-renderings/
http://mhwelander.net/2014/05/28/posting-forms-in-sitecore-mvc-part-1-view-renderings/

2) Using Controller Renderings with Html.BeginForm and jQuery to do an AJAX post - Conclusion: The way to go!

The content structure I used for this is shown below. The page we browse is ‘SammyJsPoc’ which references a View Rendering in the presentation details. The view rendering contains a div which serves as our AJAX view container. The application dynamically loads the content from the items under the ‘Components’ folder into the ajax view container.

Content structure for a Single Page Application in Sitecore MVC

Setting up the component ‘renderings’

Since I had chosen SammyJs as the mechanism for loading content (and keeping the URL representative of the current view) this meant I had to make my component views browsable as stand-alone pages. For this I created an AJAX layout containing just a placeholder for my controller renderings. The benefit of this approach is that when SammyJs makes a GET request for a ‘rendering’ it will have the full sitecore rendering context available.

Setting up the Controller

I decided to keep all the ActionResult / PartialViewResult methods in one controller, but this is really a matter of taste. With my solution, the GET request would go via Sitecore meaning I could use RenderingContext.Current to initialise my views - this is what gets called by the SammyJs app. Now the next part is the interesting bit! Since it is quite tricky to get the RenderingContext for the POST requests in my situation, I decided to ignore Sitecore completely for the POST requests. I set up the SammyJs app to use jQuery.ajax to submit form data, then in my controller I have a separate ActionResult method to handle the POST. Its worth noting that using jQuery to post the form is very simple and requires no special code to map the submitted data back to the ViewModel - this is done for your automatically by ASP.NET MVC. In this method I make no reference to Sitecore at all and would use this to save form data. The controller logic can then decide which page to go to next, or what error message to display if there was a (server) validation error with the submitted data. Note that client-side errors would still be handled by jQuery unobtrusive validation. Here is a snippet from my controller showing a GET/POST pair:

[HttpGet]
public PartialViewResult Step1()
{
   var model = new Step1();
   model.Initialize(RenderingContext.Current.Rendering);
   return PartialView("/Views/Renderings/AjaxDemo/Step1.cshtml", model);
}

[HttpPost]
public ActionResult Step1(Step1 model)
{
   var result = new JsonResultModel
   {
      NextPage = "Step2"
   };
   if (model.FirstName != "jeff")
   {
      result.Errors.Add("FirstName", "First name must be jeff");
   }
   return Json(result);
}

In the code above you will notice that I’m returning a JSON-ified JsonResultModel class from the POST method. This class is as follows:

public class JsonResultModel
{
   public bool Success
   {
      get { return !Errors.Any(); }
   }

   public string NextPage { get; set; }
   public Dictionary Errors { get; set; }

   public JsonResultModel()
   {
      Errors = new Dictionary();
   }
}

The returned JSON object then tells the SammyJs app whether the POST was a success or failure. If successful, it uses the NextPage property to make a GET request for the next page. If an error occurs it uses the jQuery validation method validator.showErrors(result.Errors) which will display the validation errors in the current page.

Here is the complete listing for the SammyJs code I developed:

(function ($) {

    var app = $.sammy('#ajaxContainer', function () {

        var resetValidation = function() {
            $('form')
                .removeData('validator')
                .removeData('unobtrusiveValidation');
            $.validator.unobtrusive.parse('form');
        };

        this.get('#/:name', function (context) {
            var name = this.params['name'];
            $('#ajaxContainer').load(window.location.pathname + '/Components/' + name, resetValidation);
        });

    });

    $(function() {
        app.run('#/Step1');

        $('#ajaxContainer')
            .on('submit', 'form', function () {
                $.ajax({
                    type: "POST",
                    url: $(this).attr('action'),
                    data: $(this).serialize(),
                    success: function(data) {
                        if (data.Success) {
                            location.hash = '/' + data.NextPage;
                        } else {
                            var validator = $("form").validate();
                            validator.showErrors(data.Errors);
                        }
                    }
                });
                return false;
            });
    });

})(jQuery);

When we browse to the start-point for our application (http://localhost/SammyJsPoc) the sammy application kicks-in app.run('#/Step1'); and we see that the URL in our browser changes to localhost/SammyJsPoc#/Step1 - in the background the application makes a request to /SammyJsPoc/Components/Step1 and loads this content into our container div. In my example clicking the submit button posts back to the Start1 controller action which returns a JSON response telling the sammy app to redirect to the next page or display a validation error. Throughout this whole life-cycle, there is no full page refresh, but the URL of the page keeps in sync so that if the user clicks back/forward/refresh the application will maintain state.

Gotchas/Things to consider

1) You will probably want to do something to prevent your views from getting indexed by Google. This could be done with a tag in the ajax layout, or an exclusion rule in your robots.txt file.

2) In my controller code, I don't do any persistence of data - you would need to add the appropriate code here. The POST controller methods would need to have code to save user data and the GET methods would need code to load data, if present, or just display an empty form otherwise.

3) What about the Page Editor I hear you cry... Well since our component 'renderings' are actually stand-alone pages, there's no reason you couldn't use the navigation tool in the Page Editor ribbon to navigate to these page to edit them. You might also need something within the javascript to check Sitecore.PageModes.PageEditor and disable the sammy routing in this case.

Conclusion

Although this is a very simple example, this technique would provide the flexibility to support quite complex decision and branching logic while maintaining browser history via the URL hash. This code has not been put into production but if you have done something similar in the past or have any views on this PoC, I would love to hear from you, so please do leave a comment below! Thanks for reading.

Tags: ASP.NET MVCSitecore MVC
comments powered by Disqus