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!
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-->
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.
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.
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.
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:
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.
So I figured out what happens and here’s what I found:
Conclusion:
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.
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.
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.