True Dynamic Placeholders in MVC

40 min read


A common topic I hear that comes up while working with Sitecore layouts and MVC (although the problem also occurs in Web Forms) is Placeholders not allowing the content editor to select a specific placeholder to add a rendering when that rendering already exists on the page. An Example being, lets say you have modeled your layouts similar to bootstrap. You’d like to have the ability to set a grid to a page, such as an 8×4 within the current row. Then you’d like to have maybe a 6×6 followed then by another 8×4. The problem with the Sitecore layout rendering engine, is that when you set an element to the placeholder “8×4.Left” for example, it doesn’t know which 8×4 to set that element to, so what would happen in this scenerio, is that you’d have the same element rendering for both 8×4’s, even though you only wanted it to show up in the first 8×4.

So for example your Device Editor (PLD) might look like this:

Layout: General

8×4 Grid – Placeholder: Page –>Html Block – Placeholder: Grid.8×4.Left 6×6 Grid – Placeholder: Page 8×4 Grid – Placeholder: Page –>Html Block – Placeholder: Grid.8×4.Right

What you would like is for the Html Block rendering to only show up in the area’s you’ve specified above. However the way it works out of the box with Sitecore’s Layout engine, is that the first Html Block in the example above, would be exactly where you would expect it to be. However the second Html Block, would actually be up in the first 8×4, even though based on that order, it’s obviously you’d like to have it placed in the second 8×4 (right).

There are currently solutions out there that attempt to resolve this issue, including some of the articles below: However these article solutions, are technically dynamic, but they require the content editor to specify a number to the placeholder name (within the Content Editor). The problem with this solution is that it requires the content editor to keep track of the numbering. That doesn’t seem like a horrible problem if your layout is pretty simple, but can be tedious and time consuming keeping track of the numbers, especially when you start moving the renderings around, adding renderings or removing a rendering. Below is an example of how this would look in the PLD:

Layout: General

8×4 Grid – Placeholder: Page –>Html Block – Placeholder: Grid.8×4.Left[0] 6×6 Grid – Placeholder: Page 8×4 Grid – Placeholder: Page –>Html Block – Placeholder: Grid.8×4.Right[1]

By using the numbering system, you are specifically telling the rendering engine that you want to for example, set the Html Block to the second ([1]) 8×4 Grid in the PLD.

But ideally, wouldn’t it be great if you could set a rendering and only need to specify it’s placeholder name, without the numbering and based on it’s order in the PLD it could logically figure out which 8×4 for example, you wanted to set the Html Block for? Below is a solution I’ve created that will solve this issues. It’s actually a pretty simple solution. It works with MVC and I’ve tested it against 8.0 and 8.1 within Sitecore.

First you’ll need to create a new Extension Method (Helper) for Html.Sitecore(). This extension will take the name (string) you pass it and append the placeholder name with the unique Id of the current rendering.

1using Sitecore.Mvc.Helpers; 2using Sitecore.Mvc.Presentation; 3using System; 4using System.Collections.Generic; 5using System.Linq; 6using System.Web; 7 8namespace Site.Web.Helpers 9{ 10 public static class SitecoreHelperExtension 11 { 12 public static HtmlString DynamicPlaceholder(this SitecoreHelper sitecoreHelper, string placeholderName) 13 { 14 Rendering currentRendering = RenderingContext.Current.Rendering; 15 return sitecoreHelper.Placeholder(string.Format("{0}|{1}", placeholderName, currentRendering.UniqueId.ToString())); 16 } 17 } 18}

Second you'll need to overwrite the Pipeline responsible for Rendering items for each placeholder on the page. Use the code below to overwrite PerformRendering.cs:

1using Sitecore; 2using Sitecore.Diagnostics; 3using Sitecore.Mvc.Common; 4using Sitecore.Mvc.Extensions; 5using Sitecore.Mvc.Pipelines; 6using Sitecore.Mvc.Pipelines.Response.RenderPlaceholder; 7using Sitecore.Mvc.Pipelines.Response.RenderRendering; 8using Sitecore.Mvc.Presentation; 9using System; 10using System.Collections.Generic; 11using System.IO; 12using System.Linq; 13using System.Text; 14using System.Text.RegularExpressions; 15using System.Threading.Tasks; 16 17namespace Site.Infrastructure.Pipelines.MVC 18{ 19 public class PerformRendering : RenderPlaceholderProcessor 20 { 21 private const string DYNAMIC_KEY = "{0}"; 22 private const string DYNAMIC_KEY_REGEX = @"(.+)_[\d\w]{8}\-([\d\w]{4}\-){3}[\d\w]{12}"; 23 24 public override void Process(RenderPlaceholderArgs args) 25 { 26 Assert.ArgumentNotNull((object)args, "args"); 27 this.Render(args.PlaceholderName, args.Writer, args); 28 } 29 30 protected virtual IEnumerable<Rendering> GetRenderings(string placeholderName, RenderPlaceholderArgs args) 31 { 32 Guid deviceId = this.GetPageDeviceId(args); 33 var childRenderings = new List<Rendering>(); 34 35 if (placeholderName.Contains("|")) 36 { 37 string[] placeholderParts = placeholderName.Split(new[] {'|'}, StringSplitOptions.RemoveEmptyEntries); // 0 = Placeholder Name, 1 = UniqueId (For the Current Rendering) 38 39 List<Rendering> lstRenderings = args.PageContext.PageDefinition.Renderings; 40 41 if (placeholderParts.Count() > 1) 42 { 43 string renderingId = string.Empty; 44 bool foundCurrentRendering = false; 45 foreach (Rendering rendering in lstRenderings) 46 { 47 if (rendering.DeviceId != deviceId) 48 continue; 49 50 if (rendering.UniqueId.ToString().Equals(placeholderParts[1]) || foundCurrentRendering) 51 { 52 // Store RenderingItemPath which is common across all renderings of this type 53 if (!foundCurrentRendering) 54 renderingId = rendering.RenderingItemPath; 55 56 // Exit Foreach When Next Rendering of Same Type as the current one is found 57 if (foundCurrentRendering && rendering.RenderingItemPath.Equals(renderingId)) 58 break; 59 60 // Only Add Renderings that match Placeholdername 61 if (rendering.Placeholder.Equals(placeholderParts[0])) 62 { 63 childRenderings.Add(rendering); 64 } 65 66 foundCurrentRendering = true; 67 } 68 } 69 } 70 } 71 else 72 { 73 string placeholderPath = StringExtensions.OrEmpty(ObjectExtensions.ValueOrDefault<PlaceholderContext, string>(PlaceholderContext.Current, (Func<PlaceholderContext, string>)(context => context.PlaceholderPath))); 74 childRenderings = Enumerable.Where<Rendering>((IEnumerable<Rendering>)args.PageContext.PageDefinition.Renderings, (Func<Rendering, bool>)(r => 75 { 76 if (!(r.DeviceId == deviceId)) 77 return false; 78 if (!StringExtensions.EqualsText(r.Placeholder, placeholderName)) 79 return StringExtensions.EqualsText(r.Placeholder, placeholderPath); 80 return true; 81 })).ToList(); 82 } 83 84 return childRenderings; 85 } 86 87 protected virtual Guid GetPageDeviceId(RenderPlaceholderArgs args) 88 { 89 Guid guid1 = ObjectExtensions.ValueOrDefault<Rendering, Guid>(args.OwnerRendering, (Func<Rendering, Guid>)(rendering => rendering.DeviceId)); 90 if (guid1 != Guid.Empty) 91 return guid1; 92 Guid guid2 = ObjectExtensions.ValueOrDefault<Rendering, Guid>(ObjectExtensions.ValueOrDefault<RenderingView, Rendering>(PageContext.Current.PageView as RenderingView, (Func<RenderingView, Rendering>)(view => view.Rendering)), (Func<Rendering, Guid>)(rendering => rendering.DeviceId)); 93 if (guid2 != Guid.Empty) 94 return guid2; 95 return Context.Device.ID.ToGuid(); 96 } 97 98 protected virtual void Render(string placeholderName, TextWriter writer, RenderPlaceholderArgs args) 99 { 100 IEnumerable<Rendering> renderings = this.GetRenderings(placeholderName, args); 101 if (renderings != null) { 102 foreach (Rendering rendering in renderings) 103 PipelineService.Get().RunPipeline<RenderRenderingArgs>("mvc.renderRendering", new RenderRenderingArgs(rendering, writer)); 104 } 105 106 } 107 } 108}

Most of the magic happens within the GetRenderings control. It will take the current placeholder name that’s being passed in. Keep in mind this Pipeline runs when you call Sitecore().Placeholder, so the placeholder name that is passed in, will include the placeholder name, plus the unique id of the current rendering. Then the logic will get all the renderings for the current page (args.PageContext.PageDefinition.Renderings) and will start traversing the enumerable List of renderings. It will not add items to be returned until it finds the rendering that matches the Unique ID of the placeholder name that you are passing in. Once it matches the unique ID, it will get the current rendering’s RenderingItemPath, which is an ID that is common across all renderings of that same type. It will then only add to childRenderings, the items that match the placeholder name (minus the unique id that was passed in). It will run this logic until it finds another rendering that has the same RenderingItemPath that matches the original rendering or it runs out of renderings (whichever happens first).

Lastly, you need to make sure you patch the Sitecore.config (8.1) or Web.config (8.0 and below). You should be able to follow the example below to do this:

1<?xml version="1.0" ?> 2<configuration xmlns:patch=""> 3 <sitecore> 4 <pipelines> 5 <mvc.renderPlaceholder> 6 <processor patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderPlaceholder.PerformRendering, Sitecore.Mvc']" type="Site.Infrastructure.Pipelines.Mvc.PerformRendering, Site.Infrastructure" /> 7 </mvc.renderPlaceholder> 8 </pipelines> 9 </sitecore> 10</configuration>

That should be it. You can now specify placeholder names in your code, by calling the extension method we created in the first code example. So in your General Layout, you could create one by called @Html.Sitecore().DynamicPlaceholder(“Page”). And then in your Sitecore PLD, all you need to do is ensure you have the correct Placeholder name specified and that you have the renderings in the correct order.

Also currently there is one limitation with the current implementation, it uses a string split on the pipe character (‘|’), so if you use pipes in your placeholder names, you could experience problems. I plan to fix this limitation in future iterations of this feature.

Happy Coding, feel free to comment if you have any questions.



Stay up to date

© 2024 All rights reserved.