Reinoud van Dalen

May 23, 2015

Wrapping your Sitecore renderings in a container

Ever needed to wrap al your components with a container div or something else if a certain condition is met? I know I have and I have made several solutions with different approaches, but this time I think I got it right: RenderingWrappers!

Example

Once I worked on a site with a nice setup of components and options and I needed to have control over all components whether they could be indexed or not. So I needed a checkbox option per component saying ‘no index’. If true then I needed to wrap the component between these 2 tags:

<!--BEGIN-NOINDEX-->
component content
<!--END-NOINDEX-->

Renderingparameters

It was an easy choice to store this ‘no index’ option in the rendering parameters. I create a ‘CrawlerParameters’ template which I could either directly apply or let existing parameter templates inherit.

Straight forward

The obvious solution now would be to check the value of the parameter in your view or usercontrol and be like:

@using Sitecore.Mvc
@using Sitecore.Mvc.Extensions

@if (Html.Sitecore().CurrentRendering.Parameters["no index"].ToBool())
{
<!--BEGIN-NOINDEX-->
}

But apart from being hideous this also delivers you quite a lot of work because you might find yourself copying this code, and with each cut an paste we all know a kitten is being drowned in angels tears.

A custom ViewEngine

I’ve seen and maintained a solution where a custom ViewEngine was written to support this. Though I found it a useful exercise to create a custom ViewEngine it required all components to be a ControllerRendering, which might not always be the case. So my search continued.

Wrappers to the rescue

Then I stumbled upon these hidden gems in Sitecore: IMarker and Wrappers. You can find them in the Sitecore.Mvc.ExperienceEditor.Presentation namespace. Sitecore uses RenderPlaceholderProcessors to add these Markers within Wrappers to a collection of disposables. It enables Sitecore to write that nice chrome data markup around your renderings and placeholders when in PageEdit mode. So to write this ourselves we would need:

  • A RenderRenderingProcessor in the mvc.renderRendering pipeline (before the ExecuteRenderer processor) -  This custom processor will add the custom IMarker implementation inside a Wrapper to the Disposables collection
  • An IMarker implementation which will write the 2 html snippets in case the no index option is applied

I tested this and it seemed to work. Below you can see the code. Mind that I wrote this code whilst using Glass. Later in this post you will find the final solution without Glass.

public class AddIndexableWrapper : RenderRenderingProcessor
{
public override void Process(RenderRenderingArgs args)
{
var marker = GetMarker();
if (marker == null) return;
args.Disposables.Add(new Wrapper(args.Writer, marker));
}

protected virtual IMarker GetMarker()
{
var luceneWebSearchOptions = ParameterHelper.GetParameters();
return luceneWebSearchOptions == null ? null : new IndexableMarker(luceneWebSearchOptions);
}
}
public class IndexableMarker : Sitecore.Mvc.ExperienceEditor.Presentation.IMarker
{
private readonly ILuceneWebSearchOptions _searchOptions;

public IndexableMarker(ILuceneWebSearchOptions searchOptions)
{
_searchOptions = searchOptions;
}

public string GetStart()
{
return _searchOptions.IsAllowedToCrawl ? string.Empty : "<!--BEGIN-NOINDEX-->";
}

public string GetEnd()
{
return _searchOptions.IsAllowedToCrawl ? string.Empty : "<!--END-NOINDEX-->";
}
}

This worked because I wasn’t using caching while I was doing some tests with this feature. Earlier this week I was working on a real life solution where this wasn’t working. And it took me a while before I figured out what was wrong. I could see the marker GetStart and GetEnd method were being called but I wasn’t seeing the GetEnd result in the html.

Writer is getting flushed with caching enabled

So I figured out what happens and here’s what I found:

  1. We call @Html.Sitecore().Placeholder(“phkey”)
  2. A StringWriter is passed to the mvc.renderPlaceholder pipeline
  3. PerformRendering calls the mvc.renderRendering pipeline and passes the writer
  4. StartRecordingOutput creates a new RecordingTextWriter, uses the passed writer as innerwriter and adds a GenericDisposable with writer.Flush as action to the Disposables
  5. Other processors write to the new writer (such as the markers and rendering)
  6. AddRecordedHtmlToCache then takes what is written and stores it in cache
  7. The renderPlaceholder disposes and calls the Dispose method on all Disposables
  8. The writer.Flush action is called from the GenericDisposable added in step 4. Amongst others the text is written to the Innerwriter
  9. My custom IMarker writes something to the RecordingTextWriter
  10. @Html.Sitecore().Placeholder calls writer.ToString() which actually points to the Innerwriter, and since no Flush is called after the IMarker has added text, it will be missed

Conclusion:

  • Yes the GetEnd() of my Marker is getting called to late (should be before the Flush()
  • But even if it were so, the second time we load the page we’d have the same problem because AddRecordedHtmlToCache has already added the content to the cache way before that

There are more actions and processors in these pipelines but these were the ones that matter in my search for truth. I must say being able to debug line by line using dotPeek and its symbolserver really helped me out here.

The solution

Well then, if we would add some of our own processors we could still use the IMarker and Wrappers. In this example I will add a processor for adding 1 wrapper and another processor to dispose them all just before the AddRecordingOutput processor.

I actually created 4 classes:

public class RenderingWrapper : Wrapper
{
public RenderingWrapper(TextWriter writer, IMarker marker) : base(writer, marker)
{
}
}

Not adding something here, just making it easy to distinguish my custom wrappers in the processors.

public class IndexableRenderingMarker : IMarker
{
public string GetEnd()
{
return "<!--END-NOINDEX-->";
}

public string GetStart()
{
return "<!--BEGIN-NOINDEX-->";
}
}

Really straightforward marker for testing this method.

public class AddIndexableWrapper : RenderRenderingProcessor
{
public override void Process(RenderRenderingArgs args)
{
var marker = GetMarker();
if (marker == null) return;
args.Disposables.Add(new RenderingWrapper(args.Writer, marker));
}

public IMarker GetMarker()
{
var renderingContext = RenderingContext.CurrentOrNull;
return renderingContext == null || renderingContext.Rendering.Parameters["allowed to index"].ToBool()
? null
: new IndexableRenderingMarker();
}
}

The processor responsible for adding the Wrapper and Marker. Note that I add the custom RenderingWrapper. Also food for thought: might be a good option to use a custom pipeline to add multiple markers.

public class EndRenderingWrappers : RenderRenderingProcessor
{
public override void Process(RenderRenderingArgs args)
{
foreach (IDisposable wrapper in args.Disposables.OfType<RenderingWrapper>())
{
wrapper.Dispose();
}
}
}

The processor responsible for writing the GetEnd() method results before results are pushed to cache.

Now all thats left to do is get those processors in the right spot. By patching before ExecuteRenderer and before AddRecordedHtmlToCache

Of course I tested this method and it works perfectly. I think I will work out a more complete solution with a custom pipeline for adding multiple wrappers instead of just one. But for now I think this looks promising enough.

Hope this helps anyone else.

TAGS: sitecore renderings


Comments
Kevin Brechbühl

Hi Renoid, nice post and great idea :) I've written a post with a different approach some time ago: http://ctor.io/wrapping-a-view-rendering-to-automatically-add-additional-markup/ But I'm not sure if this handles all cases yours does... Cheers, Kevin


Kam Figy

Synthesis also does this for diagnostics, in a third way: https://github.com/kamsar/Synthesis/blob/master/Source/Synthesis.Mvc/Pipelines/GetRenderer/RenderingDiagnosticsInjector.cs


Reinoud van Dalen

Nice! It's great to have flavors to choose :) So thanks for letting me/us know.