allBlogsList

Dynamic Placeholders for Sitecore Components

Sitecore components and placeholders are one of the main tools that allow Content Editors to take charge of a page, and control what a site visitor might see. It is often necessary to design components with ease-of-use, as well as re-use in mind. One of the drawbacks of the standard Sitecore placeholder is that it cannot be used multiple times on the same page; Sitecore ends up populating all instances of that placeholder with the same content. This was a problem for me in a recent project, which prompted me to create a Dynamic Placeholders extension. For this post, I will be sharing the process I went through to get Dynamic Placeholders functional in a Sitecore MVC project.

There are four parts to making Dynamic Placeholders work in a Sitecore project:

1. MVC Razor Extension to be used in views
2. Configuring Rendering Parameters and Placeholder Settings
3. Sitecore Extension for enforcing placeholder settings in the Experience Editor
4. Sitecore Extension for rendering placeholders in the Experience Editor

MVC Razor Extension

public static HtmlString DynamicPlaceholder(this SitecoreHelper helper, string basePlaceholderName)
{
    var userDefinedPlaceholder = RenderingContext.Current.Rendering.Parameters[basePlaceholderName];
 
    if (!string.IsNullOrEmpty(userDefinedPlaceholder))
    {
        return helper.Placeholder(string.Format("{0}{1}{2}", basePlaceholderName, Placeholder.DynamicPlaceholderPattern, userDefinedPlaceholder));
    }
 
    return helper.Placeholder(basePlaceholderName);
}
The above code generates a razor extension that can be used to drop a Dynamic Placeholder on the page:

1 @Html.Sitecore().DynamicPlaceholder({BasePlaceholderNameHere})

When this extension is used, a placeholder is generated using the base name, a common delimiter, and a user defined placeholder name which is provided via the Rendering Parameters on a rendering. The delimiter ("_dph_") is used to assist with distinguishing between a normal placeholder and a dynamic placeholder when processing items in the Experience Editor.

Configuring Rendering Parameters and Placeholder Settings

Now that I have a mechanism for generating a Dynamic Placeholder, I need to create a rendering that will utilize this functionality. Within Sitecore, I have created a simple template that inherits from the Standard Rendering Parameters Template (sitecore/Templates/System/Layout/Rendering Parameters): Three Column Row. Within this template, I have defined 3 rendering parameters to represent the placeholder names for each of the three columns that this view will generate:

I have also created a simple View Rendering that utilizes this new template. Below is the HTML for this rendering:

?

<div class="row container">
    <div class="columns small-4">
        @Html.Sitecore().DynamicPlaceholder("left_column")
    <div>
    <div class="columns small-4">
        @Html.Sitecore().DynamicPlaceholder("middle_column")
    <div>
    <div class="columns small-4">
        @Html.Sitecore().DynamicPlaceholder("right_column")
    <div>
<div>

The important thing to notice about this HTML is that the name passed into the DynamicPlaceholder extension matches the name of the rendering parameter for this template; this value represents the base placeholder name, which will be used to configure the Placeholder Settings for this rendering.

Speaking of Placeholder Settings, I have created two additional renderings (Image Rendering and Rich Text Rendering, and have configured the Placeholder Settings for all three placeholders to allow either of these two renderings to be dropped in.

PlaceholderSettings

Sitecore Extension: GetAllowedRenderings Pipeline

When generated, the final name of the placeholder will not be the same as the value that is configured in the Placeholder Settings. This means that Sitecore will not enforce the Placeholder Settings configuration that has been put in place, and will instead allow any sort of rendering to be put into the slot. I need to add some custom logic to correct this.

class GetDynamicKeyAllowedRenderings : GetAllowedRenderings
{
    public new void Process(GetPlaceholderRenderingsArgs args)
    {
        Assert.IsNotNull((object)args, "args");
         
        string placeholderKey = args.PlaceholderKey; // {base}_dph_{userDefined}
        // extract the name of the base placeholder
        string basePlaceholder = placeholderKey;
 
        if (basePlaceholder.LastIndexOf("/") >= 0)
        {
            basePlaceholder = basePlaceholder.Substring(basePlaceholder.LastIndexOf("/") + 1);
        }
 
        if (basePlaceholder.Contains(DynamicPlaceholderPattern))
        {
            basePlaceholder = basePlaceholder.Substring(0, basePlaceholder.LastIndexOf(DynamicPlaceholderPattern));
        }
 
        // If this is not a dynamic placeholder, then we don't need to do anything special
        if (basePlaceholder == placeholderKey)
        {
            return;
        }
 
        #region Copied from Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRendering, Sitecore.Kernel
        Item placeholderItem = null;
 
        if (ID.IsNullOrEmpty(args.DeviceId))
        {
            placeholderItem = Client.Page.GetPlaceholderItem(basePlaceholder, args.ContentDatabase, args.LayoutDefinition);
        }
        else
        {
            using (new DeviceSwitcher(args.DeviceId, args.ContentDatabase))
            {
                placeholderItem = Client.Page.GetPlaceholderItem(basePlaceholder, args.ContentDatabase, args.LayoutDefinition);
            }
        }
 
        List list = null;
 
        if (placeholderItem != null)
        {
            args.HasPlaceholderSettings = true;
            bool allowedControlsSpecified;
            list = GetRenderings(placeholderItem, out allowedControlsSpecified);
 
            if (allowedControlsSpecified)
            {
                //args.CustomData["allowedControlsSpecified"] = true;
                args.Options.ShowTree = false;
            }
        }
 
        if (list != null)
        {
            if (args.PlaceholderRenderings == null)
            {
                args.PlaceholderRenderings = new List();
            }
            args.PlaceholderRenderings.AddRange(list);
        }
        #endregion
    }
}

Most of this logic is copied from the base GetAllowedRenderings process. The important things to note are at the beginning of the class, where I am extracting the base name of the placeholder. I then use that name to locate any Placeholder Settings that might be configured, add those items to a list, and move on to the next pipeline. This new pipeline gets patched in front of the base GetAllowedRenderings pipeline. This will allow both pipelines (for dynamic and standard placeholders) to execute, and give us full enforcement of the Placeholder Settings that have been configured.

<getplaceholderrenderings>
    processor type="SitecoreHelpers.SitecorePipelineExtensions.GetDynamicKeyAllowedRenderings, SitecoreHelpers" patch:before="processor[@type='Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel']"/>
getplaceholderrenderings>

Sitecore Extension: GetPlaceholderChromeData

The last thing I need to do, is to tell Sitecore how to render the Dynamic Placeholders in the Experience Editor. This is done with a simple extension:


public class GetDynamicKeyPlaceholderChromeData : GetPlaceholderChromeData
{
    public override void Process(GetChromeDataArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
        Assert.IsNotNull(args.ChromeData, "Chrome Data");
         
        if ("placeholder".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
        {
            string placeholderKey = args.CustomData["placeHolderKey"] as string;
 
            string basePlaceholder = Placeholder.ExtractBasePlaceholder(placeholderKey);
             
            args.ChromeData.DisplayName = basePlaceholder;
            args.ChromeData.ExpandedDisplayName = basePlaceholder + " Dynamic Placeholder";
        }
    }
}

<getchromedata>
    processor type="SitecoreHelpers.SitecorePipelineExtensions.GetDynamicKeyPlaceholderChromeData, SitecoreHelpers" patch:after="processor[@type='Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel']">processor>
getchromedata>

Now that all of this is done, I can add my Three Column Row to a page multiple times without worrying about content overlapping. Each time I add an instance of the Three Column Row rendering to the page, I am defining the name of the placeholders where the content will reside. It is important that these names remain unique, otherwise you will run into the same issues that come from using standard placeholders. My recommendation for keeping them unique, is to give them names based on where they reside on the page and inside the rendering. The below images show an example of the values I entered for placeholders, and then the resulting actual placeholder name that gets generated.

UniqueNamesactualplaceholder

And the final image shows all of the placeholders playing together; I have added two instances of the Three Column Row to the page, and loaded up content in each of the 6 placeholders that get generated.

 samerenderingonpage

In Conclusion...

Using Dynamic Placeholders can open up a lot of capabilities for developers and Content Editors. They are particularly useful when working within a standard CSS grid system (e.g. Bootstrap or Foundation). They offer up a lot of options for Content Editors to design and layout their pages however they wish, and can alleviate some development effort when creating components that are "similar", but have to be developed separately because some of their content is driven by extra renderings. 

Below is a final look at a page that was created using dynamic placeholders.

Final

The example used in this blog post has been implemented on the latest version of Sitecore 8.2.3, a MVC application running on .NET Framework 4.5.2. This implementation has been tested and supports multisite solution.