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...
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.
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()
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!
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:↑ Back to top
This article by Chris Perks describes an alternative approach that might make more sense in some circumstances. Take a look.