Getting MVC components to communicate

Every so often, the move from WebForms style projects to MVC ones throws up a challenging question. An issue which I came across recently, is how do you cope with a situation where two independent components on a page need to exchange data? In WebForms projects there we could connect them together via the Layout’s Code Behind, and in front-end situations JavaScript can do a similar job for us. But the situation requires back-end code and we’re using MVC it’s a bit more of a challenge…

Why might we need this?

The situation I was trying to deal with was that a component which presented a paginated set of links needed to send data to a component which rendered metadata about pagination:

Component Communication

Once the paginated list component has calculated the data it’s showing, it needs to inform the metadata component, so that the <link rel="next" /> and <link rel="prev" /> can display the right values. If the whole page was one giant component (as in traditional MVC), that would be easy enough to do, but when your page is broken up into components in the Sitecore MVC style, it’s a bit more challenging.

So what can we do?

The first thing we need is a way for two components to exchange data. MVC doesn’t offer us anything much here, but basic ASP.Net offers the HttpContext.Current.Items collection. If we wrap up some “send” and “receive” behaviour around this collection then we have a way for one component to send data, and another to receive it. A very simple class to mediate this interaction might look like:

public static class CommunicationConduit
{
    public static readonly string MetadataConduitKey = "Metadata_Conduit";

    public static void SendMetadata(string nextLink, string prevLink)
    {
        StringBuilder markup = new StringBuilder();

        if(!string.IsNullOrWhiteSpace(nextLink))
        {
            markup.AppendFormat("<link rel=\"next\" href=\"{0}\"/>", nextLink);
        }

        if(!string.IsNullOrWhiteSpace(prevLink))
        {
            markup.AppendFormat("<link rel=\"prev\" href=\"{0}\"/>", prevLink);
        }

        HtmlString html = new HtmlString(markup.ToString());

        HttpContext.Current.Items[MetadataConduitKey] = html;
    }

    public static HtmlString ReceiveMetadata()
    {
        HtmlString result = null;

        if(HttpContext.Current.Items.Contains(MetadataConduitKey))
        {
            result = HttpContext.Current.Items[MetadataConduitKey] as HtmlString;
        }

        if(result == null)
        {
            result = new HtmlString(string.Empty);
        }

        return result;
    }
}

So the controller code for the filter component can do its calculations can then send out the data. In abstract terms, a controller method generating the data might do something like:

public ActionResult GenerateResultPage(int page)
{
   var results = calculateResultPage(page);

   var nextLink = calculateNextLink();
   var prevLink = calculatePrevLink();

   CommunicationConduit.SendMetadata(nextLink, prevLink);

   return View(results);
}

And the metadata rendering component can make use of the other method to display the data. If the metadata is a simple view rendering then all it needs is:

@using Sitecore.Mvc
@{
    Layout = null;
}
<!-- other metadata fields -->
@CommunicationConduit.ReceiveMetadata()

Execution order is important…

Unfortunately the only way the code above can work is if we can ensure that the UI component can call SendMetadata() before the metadata component calls ReceiveMetadata().

The situation where both of your components exist in the same Sitecore Placeholder, this is pretty easy. You just have to ensure that the metadata component comes after the UI component in the layout definition. You can do that physically, by ordering the components in the Presentation Details dialog, or in Experience Editor. But that relies on editors not accidentally messing it up – and it’s not really fair to expect non-technical authors to understand the need to order components like that. One approach that you could take to ensure that they can’t mess it up is to put the placeholder binding into code. In MVC sites, the mvc.getXmlBasedLayoutDefinition pipeline can be used to inject a component. Code along these lines can add a component to the Layout XML being processed:

public class MetadataInjector : GetXmlBasedLayoutDefinitionProcessor
{
    public static readonly string PlaceholderName = "NameOfThePlaceholder";

    private string UniqueBindingID = "{DC9B51A9-5897-4F2C-8410-E0B228FC54DE}";
    private string DeviceID = "{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}";
    private string MetadataRenderingID = "{C8EB6246-CC62-47CB-9843-17EBAEF7F319}";

    public override void Process(GetXmlBasedLayoutDefinitionArgs args)
    {
        XElement device = args.Result
            .Elements("d")
            .Where(e => e.Attribute("id").Value == DeviceID)
            .FirstOrDefault();

        if (device != null)
        {
            device.Add(
                new XElement("r",
                    new XAttribute("uid", UniqueBindingID),
                    new XAttribute("id", MetadataRenderingID),
                    new XAttribute("ph", PlaceholderName)
                )
            );
        }
    }
}

This just manually adds a rendering to the end of the Layout XML when it’s run. It can be injected into the end of the pipeline with a config patch. Note that you need to make sure this patch runs after any of the MVC-related patches get included:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getXmlBasedLayoutDefinition>
        <processor patch:after="processor[@type='Sitecore.Mvc.ExperienceEditor.Pipelines.Response.GetXmlBasedLayoutDefinition.SetLayoutContext, Sitecore.Mvc.ExperienceEditor']"
                   type="ExampleCode.MetadataInjector, ExampleCode.CrossComponentComms"/>
      </mvc.getXmlBasedLayoutDefinition>
    </pipelines>
  </sitecore>
</configuration>

But what do you do if your components are in different placeholders?

Well the framework for MVC doesn’t really have a way to control the execution order between placeholders. When Sitecore processes a page request, it finds the appropriate Layout View, and processes that. The framework around Razor processes each of the lines of the layout file in turn – each line of mark-up and each helper gets run in order. Hence the first call to Html.Sitecore().Placeholder() on the page always gets run first. Unless you want to put your <body/> element before your <head/> element, that’s not particularly helpful here.

One thing that can help us, however, is that the helper code for defining placeholders is a function. It’s defined as:

public class SitecoreHelper
{
    public virtual HtmlString Placeholder(string placeholderName);
}

That means we can cheat a bit, and control the execution order of placeholders by running the helpers in our required order and storing the values returned for later:

@using Sitecore.Mvc
@{
    Layout = null;

    HtmlString main = Html.Sitecore().Placeholder("MAIN");
    HtmlString head = Html.Sitecore().Placeholder("HEAD");
}
<!DOCTYPE html>
<html>

<head>
    <title>@Html.Sitecore().Field("title", new { DisableWebEdit = true })</title>
    @head
</head>

<body>
    <h1>@Html.Sitecore().Field("title")</h1>
    <h2>Cross component comms layout</h2>
    <div>
        @main
    </div>
</body>

</html>

Now Razor will run the “MAIN” placeholder’s component before “HEAD” placeholder’s – but it will still render the results in the right order. And that means our components can exchange data correctly.

Success!

Some thanks are due here…

When I first started looking at this problem I had a really helpful conversation on the Sitecore Chat Slack with Martina Whelander and Mark Cassidy which started me off down the research path leading to this approach. Although my final approach has changed a bit from the original discussion, their advice was a really helpful for getting started with this. Thank you both!


Edited to add:
There’s an important caveat to note here, as mentioned by Dražen Janjiček in the comments below. If you are not careful about caching your controls, this model can fall down when your communicating components don’t actually get executed when the page renders. You’ll need to consider whether your particular data and approach can work with caching, and what sort of caching is relevant.

Edited again:
This article by Chris Perks describes an alternative approach that might make more sense in some circumstances. Take a look.

Advertisements

9 thoughts on “Getting MVC components to communicate

  1. Personally I prefer events to pass data between rendering components, this works fine with MVC (to some extent). The problems are like you said the order of components but even more how to get the data from the callback handler to your view that needs to output it (the action already executed and returned the view). With WebForms the callback handler would update some control. This isn’t possible with MVC. Rendering caching is also another problem. So in the end, nice approach 🙂

  2. Nice article. Another angle to consider is using the view’s Layout property and have an “outer” view with the html->head and a call to RenderBody() to do inside out rendering. This way the inner view (the body) renders first into a buffer, then the outer view (html tag+head) renders and inserts the inner view HTML from the buffer where the RenderBody() is called…. I loved the way you changed placeholder rendering order – neat trick.

  3. Pingback: SUGCON 2016 in Kopenhagen – Rückblick auf Tag 2 › comspace-Blog
  4. Pingback: A workaround for missing ViewData in Sitecore MVC | Chris Perks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s