Making Single-Page App Work in Sitecore MVC

girl-reading-on-ipad

When you’re developing a single-page app in Sitecore you’ll most likely have to partially render a page and inject the rendered markup into your SPA. When you find yourself in this situation you might resort to using Devices, or you might use regular expressions. Either way will require you to request the page first, but the technique I’m gonna show you will only render a page-item’s placeholder to return the markup of any ‘rendering’ placed in it.

Obviously, you will be using a javascript framework, like AngularJS or EmberJS, to build your SPA. Therefore, the request to get the partial-page rendering has to be done thru an API. So for this tutorial we’ll create a pseudo Web API using an MVC controller because we need to access certain Context objects that are not available in a Web API Controller.

The Controller

Let’s create an MVC controller that accepts the id of the page item and the name of the placeholder, and emulates the page-item rendering.

public class PartialRenderingController : Controller
{
    public ActionResult GetPartialRendering(string itemId, string placeholder)
    {
        ID id;
        if (!ID.TryParse(itemId, out id))
            return new HttpStatusCodeResult(400,
                "Item ID is not a valid Guid.");

        var item = Sitecore.Context.Database.GetItem(id);

        if(item == null)
            return new HttpStatusCodeResult(400,
                $"An item with id '{itemId}' does not exist.");

        // These would be null if not in MVC controller
        PageContext.Current.Item = item;
        Sitecore.Context.Item = item;

        using (var sw = new StringWriter())
        {
            ContextService.Get()
                .Push<ViewContext>(
                    new ViewContext(
                        ControllerContext,
                        PageContext.Current.PageView,
                        ViewData, TempData, sw));

            var html = RenderPlaceholder(placeholder);

            return new FileContentResult(
                Encoding.UTF8.GetBytes(html), "text/html");
        }
    }
}

Rendering the placeholder

Here’s where the magic happens. Once we have a ViewContext we will run the default pipeline processor ‘mvc.renderPlaceholder’ to render the placeholder.

public static string RenderPlaceholder(string placeholder)
{
    var sb = new StringBuilder();

    using (var sw = new StringWriter(sb))
    {
        var args = new RenderPlaceholderArgs(placeholder, sw)
        {
            PageContext = PageContext.Current
        };

        try
        {
            CorePipeline.Run("mvc.renderPlaceholder", args);
        }
        catch
        {
            // ignored
        }

        return sb.ToString();
    }
}

FYI: Since updating to Sitecore 8.2 I’ve been getting a runtime error when running the pipeline processor. Hence, I had to wrap it in a try-catch block. But this shouldn’t affect the result as far as I know. Feel free to comment about a better workaround.

Registering the Route

Though attribute routing is working for 8.2 I would advise against it for this purpose, since the good people from Sitecore told me that it does not support the initialization of the PageContext object. Therefore, we’re still gonna use the good ol’ MapRoute method.

First we need to create a pipeline processor that will register the controller route. I’m assuming that your main content’s placeholder is called ‘main’ so we’ll add it as a default.

public class RegisterPartialRenderingRoute
{
    public virtual void Process(PipelineArgs args)
    {
        RouteTable.Routes.MapRoute(
            name: "GetRendering",
            url: "api/partialrendering/{itemId}/{placeholder}",
            defaults: new {
                    controller = "PartialRendering",
                    action = "GetPartialRendering",
                    placeholder = "main" }
            );
    }
}

Then we’ll create a custom config file to register the processor. Make sure it will be run before ‘Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc’, and make sure the config will be patched after the default ones; so the config file’s directory path should be similar to this: App_Config/Include/Z.MyProject

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor
           type="MyProject.RegisterPartialRenderingRoute, MyProject"
           patch:before="*[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Great, we’re done!

Be aware that this solution only works for outermost placeholders and it won’t work when you pass a path to an inner placeholder (e.g. “/main/tabs/first-tab”). For that you may have to reverse-engineer and replace the default mvc.renderPlaceholder pipeline processor.

Happy programming!

Advertisements