Wow it has been ages since I last wrote a blog post so it is about time that I get started again.
A colleague of mine asked me a question the other day about how I would implement a page(experience)-editable accordion spot where each element in the accordion is a rendering item. Kind of like shown below where the content in each accordion element is a rendering with its own datasource. You should be able to personalize and test each rendering individually.
There is a lot of solutions to achieve this but to keep it close to the Sitecore API and to make it work in the Experience Editor I thought how about implementing an accordion placeholder? That is a placeholder that automatically wraps each rendering within it, in <li> elements within an outer <ul> element.
Back in the webforms days this would have been tricky to do but now with the MVC API it is really easy. No control tree that has to be built up, just a glorified text writer.
First we reflect our way into the placeholder extension method that renders out a normal Sitecore placeholder.
1 2 3 4 5 6 7 8 9 10 |
public virtual HtmlString Placeholder(string placeholderName) { Assert.ArgumentNotNull((object) placeholderName, "placeholderName"); using (ContextService.Get().Push<ViewContext>(this.HtmlHelper.ViewContext)) { StringWriter stringWriter = new StringWriter(); PipelineService.Get().RunPipeline<RenderPlaceholderArgs>("mvc.renderPlaceholder", new RenderPlaceholderArgs(placeholderName, (TextWriter) stringWriter, this.CurrentRendering)); return new HtmlString(stringWriter.ToString()); } } |
This code simply calls the mvc.renderPlaceholder pipeline. Then if we take a closer look at the last processor in this pipeline called Sitecore.Mvc.Pipelines.Response.RenderPlaceholder.PerformRendering
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace Sitecore.Mvc.Pipelines.Response.RenderPlaceholder { public class PerformRendering : RenderPlaceholderProcessor { public override void Process(RenderPlaceholderArgs args) { Assert.ArgumentNotNull((object) args, "args"); this.Render(args.PlaceholderName, args.Writer, args); } protected virtual void Render(string placeholderName, TextWriter writer, RenderPlaceholderArgs args) { foreach (Rendering rendering in this.GetRenderings(placeholderName, args)) PipelineService.Get().RunPipeline<RenderRenderingArgs>("mvc.renderRendering", new RenderRenderingArgs(rendering, writer)); } .... } } |
As you can see this processor simply iterates over the renderings placed in the placeholder and render them by calling the renderRendering pipeline.
The really cheap and dirty solution would be simply to override this processor and given some condition (placeholder name starts with accordion) throw in a <ul> and <li> tags around each rendering and job’s done.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PerformAccordionRendering : PerformRendering { private const string AccordionPlaceholderKey = "accordion"; protected virtual void Render(string placeholderName, TextWriter writer, RenderPlaceholderArgs args) { if (!placeholderName.StartsWith(AccordionPlaceholderKey)) { base.Render(placeholderName, writer, args); return; } writer.Write("<ul>"); foreach (Rendering rendering in this.GetRenderings(placeholderName, args)) { writer.Write("<li>"); PipelineService.Get().RunPipeline("mvc.renderRendering", new RenderRenderingArgs(rendering, writer)); writer.Write("</li>"); } writer.Write("</ul>"); } } |
I prefer pragmatism and clean code over cheap and dirty so I would like to take the concept a bit further just for fun.
Making it prettier
The RenderPlaceholderArgs inherit from good old PipelineArgs that has a customdata dictionary so we have a way of passing data to the pipeline.
First we create a simple interface called IRenderPlaceholder that has a method Render.
1 2 3 4 |
public interface IRenderPlaceHolder { void Render(TextWriter writer, IEnumerable<Rendering> renderings); } |
Then we override the PerformRendering processor and implement a check if the custom data dictionary contains a key for a specific placeholder renderer and then we use ReflectionUtil to look up this type and try to cast it to our IRenderPlaceholder interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class PerformRendering : Sitecore.Mvc.Pipelines.Response.RenderPlaceholder.PerformRendering { public const string RenderPlaceholderTypeKey = "rt"; protected override void Render(string placeholderName, TextWriter writer, RenderPlaceholderArgs args) { if (!args.CustomData.ContainsKey(RenderPlaceholderTypeKey)) { base.Render(placeholderName, writer, args); return; } var renderer = (IRenderPlaceHolder)ReflectionUtil.CreateObject((Type)args.CustomData[RenderPlaceholderTypeKey]); Assert.IsNotNull(renderer, "Could not instantiate custom placeholder renderer for placeholder " + placeholderName); renderer.Render(writer, GetRenderings(placeholderName, args)); } } |
Finally we patch our new processor into the renderPlaceholder pipeline instead of Sitecore.Mvc.Pipelines.Response.RenderPlaceholder.PerformRendering
1 2 3 4 5 6 7 8 9 10 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <mvc.renderPlaceholder > <processor patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderPlaceholder.PerformRendering, Sitecore.Mvc']" type="[NAMESPACE].PerformRendering, [ASSEMBLY]" /> </mvc.renderPlaceholder> </pipelines> </sitecore> </configuration> |
Note: Ensure that this patch is read in last. Either by placing the include config file in a folder beneath /App_Config/Include or by prefixing the filename with zzz.
Next up we create our AccordionPlaceholderRenderer that implements our IRenderPlaceholder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class RenderAccordionPlaceholder : IRenderPlaceHolder { public void Render(TextWriter writer, IEnumerable<Rendering> renderings) { writer.Write("<ul>"); foreach (Rendering rendering in renderings) { writer.Write("<li>"); PipelineService.Get().RunPipeline<RenderRenderingArgs>("mvc.renderRendering", new RenderRenderingArgs(rendering, writer)); writer.Write("</li>"); } writer.Write("</ul>"); } } |
Then we write a new html helper extension method that renders out an accordion placeholder by adding the typename to the custom data dictionary.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static class AccordionPlaceholderExtensions { public static HtmlString AccordionPlaceholder(this HtmlHelper helper, string placeholderName) { Assert.ArgumentNotNull(placeholderName, "placeholderName"); using (ContextService.Get().Push<ViewContext>(helper.ViewContext)) { var stringWriter = new StringWriter(); var args = new RenderPlaceholderArgs(placeholderName, stringWriter, helper.Sitecore().CurrentRendering); args.CustomData.Add(PerformRendering.RenderPlaceholderTypeKey, typeof(RenderAccordionPlaceholder)); PipelineService.Get().RunPipeline("mvc.renderPlaceholder", args); return new HtmlString(stringWriter.ToString()); } } } |
And voila, all rendering items that are placed within this placeholder now renders out in a list 🙂
Tips, I’ve received some feedback with some handy tips for the accordion placeholder.
- Turn off the accordion Javascript in page edit mode so it is always fully expanded. We typically put a class on body indicating if the page is in page edit mode.
- When in page edit mode add some margin to the ul and li elements so these can be clicked in the experience editor. Otherwise you’ll need to navigate up the hierarchy by clicking an inner element and using the navigation on the floating toolbar
A little bit of bonus..
Now you can also easily make an extended placeholder that wraps all it’s rendering items in let’s say a <section> element:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class RenderSectionPlaceholder : IRenderPlaceHolder { public void Render(TextWriter writer, IEnumerable<Rendering> renderings) { foreach (Rendering rendering in renderings) { writer.Write("<section>"); PipelineService.Get().RunPipeline<RenderRenderingArgs>("mvc.renderRendering", new RenderRenderingArgs(rendering, writer)); writer.Write("</section>"); } } } |
Or a placeholder that only render out 3 renderings at random when not in page edit mode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class RenderThreeRandomSpotsPlaceholder : IRenderPlaceHolder { public void Render(TextWriter writer, IEnumerable<Rendering> renderings) { if (Sitecore.Context.PageMode.IsPageEditor) RenderAllRenderings(writer, renderings); else RenderThreeRandomRenderings(writer, renderings.ToList()); } private void RenderThreeRandomRenderings(TextWriter writer, IList<Rendering> renderings) { if (renderings.Count <= 3) RenderAllRenderings(writer, renderings); var randomizer = new Random(); for (var i = 0; i < 3; i++) { var index = randomizer.Next(renderings.Count); PipelineService.Get().RunPipeline("mvc.renderRendering", new RenderRenderingArgs(renderings[index], writer)); renderings.RemoveAt(index); } } private void RenderAllRenderings(TextWriter writer, IEnumerable<Rendering> renderings) { foreach (Rendering rendering in renderings) { PipelineService.Get().RunPipeline<RenderRenderingArgs>("mvc.renderRendering", new RenderRenderingArgs(rendering, writer)); } } } |
And so on and so forth. Just make a new HtmlHelper extension method for each type or implement a generic method where the type is IRenderPlaceholder.
That was it, I hope to blog some more the coming weeks, I’ve been missing it.
Anders Laub Christoffersen
Anders has been working with Sitecore for over a decade and has in this time been the lead developer and architect on several large scale enterprise solutions all around the world. Anders was appointed the title of Sitecore Technical MVP in 2014 and has been re-appointed the title every year since then.
- Web |
- More Posts
Couldn’t you just use the Item Renderer feature of Sitecore?
So our approach to this problem would be to create an Accordion Module that allows you to pick a datasource. This datasource would point to a folder of Sitecore Items or you could select from a multilist or whatever. Each of those items they have their own presentation defined in Presentation Details. So our Accordion Module view would just iterate through all of the children in the referenced datasource and call the ItemRendering() command (we are using Glass). This triggers each of the Items referenced views to render our markup.
Just curious if we are missing a key piece of functionality that your way provides?
Thanks
Hi Jerami, thanks for your comment.
Your approach is also a fine solution for creating an accordion, I’ve used it as well, and back in the webforms days a similar approach.
The main difference between reading the presentation details from another item and extending the placeholder as I show in this post is Experience Editor support.
By using an extended placeholder you will get all the well-known out-of-the-box functionality for personalization and testing on each of the elements in the accordion and not just on the rendering that iterates over its datasource child items and render these.
Makes sense? Perhaps the “bonus” examples that I show in the end of the post better illustrates the potential of the approach than an accordion.