Optimizing Sitecore Experience Editor - Extending Dynamic Placeholders

Posted 08/13/2018 by William Hubbell

In my last post, I walked through how to implement components in such a way that they could render multiple items that can be edited from the Experience Editor. In this post I’m going to explain how to use that functionality with a popular implementation of Dynamic Placeholders.

Dynamic Placeholders are crucial for page layouts that have multiple nested placeholders. For example, if you have a component with a placeholder “Y”, and you put multiple instances of that component in placeholder “X” on your main layout, any component you put in “Y” will show up in every instance of the parent component. That’s because the layout will be looking for placeholder “X/Y” and find multiple instances of it. Dynamic Placeholders solve this problem by adding text to the end of the placeholder name if there are multiple instances of it. Using Dynamic Placeholders, instead of multiple instances of “X/Y”, you’ll instead get “X/Y”, “X/Y_1”, “X/Y_2”. This way each placeholder stays unique, without having to create different renderings with different placeholder names.

While Sitecore 9 comes with Dynamic Placeholders out of the box, previous versions require downloading a module or creating your own instance. At TechGuilds, we recently implemented then extended a popular version of Dynamic Placeholders, and that’s what I’m going to show you today.

There are two ways to implement dynamic placeholders. The first is similar to the implementation I described above, simply adding text to the end of the placeholder at rendering time, as this module does. However, we ran into a problem with this approach: re-ordering the components on the page didn’t reorder the placeholders inside the components. So if you had components 1, 2, and 3 with placeholders Y_1, Y_2, and Y_3, reordering the components to 2, 3, 1 would still result with the placeholders in the order of Y_1, Y_2, and Y_3. We needed a more permanent solution.

The other way to implement dynamic placeholders is to add the GUID of the rendering in question to the end of the placeholder name. This is what the popular Fortis placeholder module does. We tested this implementation and it seemed to work well. However, we ran into problems when we tested it on an Accordion component we made for a client. The accordion, like the Carousel component I outlined in my last blog post, rendered multiple accordion panel items, each with their own dynamic placeholder. The problem we ran into was that because there was one rendering for the accordion, the same rendering GUID was being added to each placeholder, rendering them not dynamic at all.

The solution we found was to forego sitecore modules and implement our own modified version of dynamic placeholders. We extended code written by Matthew Dresser to include an overloaded method that would allow a developer to pass a GUID for a specific content item, which would be added to the end of the placeholder name. The placeholders for each accordion panel are now in the format of “[placeholder name]|[accordion panel item GUID]_[accordion rendering GUID]”, and this works.

Project Setup

In a previous blog post, I created a project called TGBlog.Foundation.SitecoreExtensions in order to add visible chrome around renderings and placeholders in the XPE. I added Matt Dresser’s dynamic placeholder code to a similar SitecoreExtensions project in order to get it working for our client. I added this code in a simple way, adding a SitecoreHelper.cs file in an “Extensions” folder in the project, and putting his classes inside that file. There are three classes: DynamicPlaceholder, GetDynamicKeyAllowedRenderings, and GetDynamicPlaceholderChromeData. DynamicPlaceholder is the most important one because it actually makes the placeholders dynamic. GetDynamicKeyAllowedRenderings allows placeholder settings to work with the dynamic placeholders. Finally, GetDynamicPlaceholderChromeData makes it so the GUIDs don’t show up in the placeholder names in the XPE.

Then I added his config code into a file called DynamicPlaceholders.config inside App_Config/Include/DynamicPlaceholders. [NOTE: this didn’t work at first and we eventually realized it was because the original dynamic placeholder module we installed was replacing a crucial line in the Sitecore config. Use the Sitecore Developer tool to check the config if this implementation isn’t working.] The config code with this setup was the following:

DynamicPlaceholders.config

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getPlaceholderRenderings>
        <processor type="TGBlog.Foundation.SitecoreExtensions.Extensions.GetDynamicKeyAllowedRenderings, TGBlog.Foundation.SitecoreExtensions"
          patch:before="processor[@type='Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel']"/>
      </getPlaceholderRenderings>
      <getChromeData>
        <processor
          type="TGBlog.Foundation.SitecoreExtensions.Extensions.GetDynamicPlaceholderChromeData, TGBlog.Foundation.SitecoreExtensions"
          patch:after="processor[@type='Cognifide.PowerShell.Integrations.Pipelines.PageEditorExperienceButtonScript, Sitecore.Kernel']"/>
      </getChromeData>
    </pipelines>
  </sitecore>
</configuration>

That was the initial setup I did before we realized we had to extend this code.

With that implementation, the only thing you need to do to use dynamic placeholders is to include “@using [YourSite].Foundation.SitecoreExtensions.Extensions” to your view after adding a reference to the SitecoreExtensions dll in the given project. Then you can add a dynamic placeholder with the following format:

@Html.Sitecore().DynamicPlaceholder("PlaceholderName")

Our Extension

We needed to extend this placeholder code to accept a parameter of an Item GUID. I decided to overload the DynamicPlaceholder method.

The original looks like this:

public static HtmlString DynamicPlaceholder(this Sitecore.Mvc.Helpers.SitecoreHelper helper, string dynamicKey)
{
     var currentRenderingId = RenderingContext.Current.Rendering.UniqueId;
     return helper.Placeholder(string.Format("{0}_{1}", dynamicKey, currentRenderingId));
}

The overloaded method looks like this:

//This overloaded method for edit frames within renderings that correspond to unique items
public static HtmlString DynamicPlaceholder(this Sitecore.Mvc.Helpers.SitecoreHelper helper, string dynamicKey, string Counter)
{
     string ItemGuidProper = Counter.Trim('{').Trim('}');
     var currentRenderingId = RenderingContext.Current.Rendering.UniqueId;
     return helper.Placeholder(string.Format("{0}|{2}_{1}", dynamicKey, currentRenderingId, ItemGuidProper));
}

As you can see, this method takes a string corresponding to a GUID. The way you use it is thus:

@Html.Sitecore().DynamicPlaceholder("PlaceholderName", SitecoreItem.ID.ToString())

This change alone would have solved our dynamic placeholder issue. However, to be completely XPE-friendly, we needed placeholder settings to work. That meant that we needed to edit GetDynamicKeyAllowedRenderings a bit.

The original code uses regex to check if there’s a GUID in the placeholder key, and returns if not, to make sure that this method is only running on dynamic placeholders. Because we’re adding a second GUID in some cases, we need to add some code to make sure that non-dynamic placeholders within dynamic placeholders aren’t being operated on.

Here’s the original code:

string placeholderKey = args.PlaceholderKey; 
Regex regex = new Regex(DYNAMIC_KEY_REGEX); 
Match match = regex.Match(placeholderKey); 
if (match.Success && match.Groups.Count > 0) 
{ 
     placeholderKey = match.Groups[1].Value; 
} 
else 
{ 
     return; 
}

And here’s the code with our modification:

string placeholderKey = args.PlaceholderKey;
Regex regex = new Regex(DYNAMIC_KEY_REGEX);

//only run this code if there's a GUID at the end matching the Dynamic key
if (placeholderKey.IndexOf("_") > 0 && placeholderKey.Length > 37)
{
    Match match = regex.Match(placeholderKey.Substring(placeholderKey.Length-37, 37)); 
    if (match.Success)
    {
        placeholderKey = regex.Replace(placeholderKey, "");

        if (placeholderKey.Contains("|"))
        {
             placeholderKey = placeholderKey.Substring(0, placeholderKey.IndexOf("|"));
        }
    }
    else
    {
        return;
    }
}
else
{
    return;
}

This way, we’re checking to make sure we’re working on a dynamic placeholder, then stripping one or both GUIDS from the placeholder key so we have the correct key with which to look up the renderings allowed in the placeholder settings.

Fixing Rendering Chrome

For the last step, we wanted to make sure the GUIDs weren’t showing up in the placeholder chrome that we’ve set up. To do this I revisited XPEPlaceholder.cs, our implementation of Kenneth McAndrew’s rendering chrome code.

The original code has the following lines:

if (Context.PageMode.IsExperienceEditorEditing)
{
   writer.Write($"<div class=\"component-wrapper scPlaceholder {placeholderName.Replace(" ", string.Empty)}\"><span class=\"wrapper-header\">{placeholderName} Placeholder</span><div class=\"component-content clearfix\">");
}

I kept these the same, but inserted some string manipulation before this so that “placeholderName” wouldn’t include the GUIDs. First I created a regex dynamic key, grabbed from Derk Hudepol’s revision of the dynamic placeholder code above.

This I put directly below the class declaration:

private const string DYNAMIC_KEY_REGEX = @"(_([A-Fa-f0-9]{8}(-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}))";

Then I added the string manipulation code at the beginning of the Render method:

Regex regex = new Regex(DYNAMIC_KEY_REGEX);
Match match = regex.Match(placeholderName);
if (match.Success)
{
    placeholderName = regex.Replace(placeholderName, "");
    if (placeholderName.Contains("|"))
    {
          placeholderName = placeholderName.Substring(0, placeholderName.IndexOf("|"));
    }
}

This code is more or less the same code I added to GetDynamicKeyAllowedRenderings, without the else statement that ended the process if there wasn’t a match.

After all this, our dynamic placeholders work as you would expect, and they look good too!

Let us know if you have any questions, concerns, or tips in the comments!

Add your comment