Thursday, June 2, 2016

Dynamic placeholders in sitecore

One problem with reusing Sitecore components that contain placeholders (such as a two column row) is that if you place more than one of the component on the page, the child elements will not render correctly - because there are two versions of the same placeholder on the page.

The solution to this issue is to use dynamic placeholder keys so that each version of a given component will have unique placeholders. This will then allow multiple components to work harmoniously together on the same page.

The solution below is for projects which are using web forms, there are plenty of solutions available online for those who are using MVC.

Defining the dynamic placeholder

The code below is a custom implementation of a Sitecore placeholder which will take the placeholder key and append the unique id of the rendering to ensure each key is unqiue.

using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using Sitecore.Common;
using Sitecore.Layouts;
using Sitecore.Web.UI;
using Sitecore.Web.UI.WebControls;

namespace MyProject.Helper
{
    public class DynamicKeyPlaceholder : WebControl, IExpandable
    {
        protected string _key = Placeholder.DefaultPlaceholderKey;
        protected string _dynamicKey = null;
        protected Placeholder _placeholder;

        public string Key
        {
            get
            {
                return _key;
            }
            set
            {
                _key = value.ToLower();
            }
        }

        protected string DynamicKey
        {
            get
            {
                if (_dynamicKey != null)
                {
                    return _dynamicKey;
                }
                _dynamicKey = _key;
                //find the last placeholder processed, will help us find our parent
                Stack<Placeholder> stack = Switcher<Placeholder, PlaceholderSwitcher>.GetStack(false);
                Placeholder current = stack.Peek();
                //find the rendering reference we are contained in
                var renderings = Sitecore.Context.Page.Renderings.Where(rendering => (rendering.Placeholder == current.ContextKey || rendering.Placeholder == current.Key) && rendering.AddedToPage);
                if (renderings.Count() > 0)
                {
                    //last one added to page defines our parent
                    var rendering = renderings.Last();
                    //use rendering reference unique ID to uniquely and permanently identify the placeholder
                    _dynamicKey = _key + rendering.UniqueId;
                }
                return _dynamicKey;
            }
        }

        protected override void CreateChildControls()
        {
            Sitecore.Diagnostics.Tracer.Debug("DynamicKeyPlaceholder: Adding dynamic placeholder with Key " + DynamicKey);
            _placeholder = new Placeholder();
            _placeholder.Key = this.DynamicKey;
            this.Controls.Add(_placeholder);
            _placeholder.Expand();
        }

        protected override void DoRender(HtmlTextWriter output)
        {
            base.RenderChildren(output);
        }

        #region IExpandable Members

        public void Expand()
        {
            this.EnsureChildControls();
        }

        #endregion
    }
}

Using a dynamic placeholder

Firstly to use the dynamic placeholder, a custom tag needs needs to be defined in the web.config under the controls node (<configuration><system.web><pages><controls>).
lt;add tagPrefix="dkp" namespace="WCC.Internet.UI.Helper" assembly="WCC.Internet.UI" />
Now a dynamic placeholder can be referenced on a sublayout/rendering:
<dkp:DynamicKeyPlaceholder ID="plSingle" runat="server" Key="singlecolumn" />
Don't forget to create placeholder settings for the placeholder key (singlecolumn in this case). Once rendered on the page it will appear unique with an appended GUID.

Allowing for placeholder settings

Now to allow placeholder settings to work with these dynamic placeholders.

public class GetDynamicKeyAllowedRenderings : GetAllowedRenderings
{
 //text that ends in a GUID
 private const string DYNAMIC_KEY_REGEX = @"(.+){[\d\w]{8}\-([\d\w]{4}\-){3}[\d\w]{12}}";

 public new void Process(GetPlaceholderRenderingsArgs args)
 {
  Assert.IsNotNull(args, "args");

  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;
  }
  // Same as Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings but with fake placeholderKey
  Item placeholderItem = null;
  if (ID.IsNullOrEmpty(args.DeviceId))
  {
   placeholderItem = Client.Page.GetPlaceholderItem(placeholderKey, args.ContentDatabase,
                args.LayoutDefinition);
  }
  else
  {
   using (new DeviceSwitcher(args.DeviceId, args.ContentDatabase))
   {
    placeholderItem = Client.Page.GetPlaceholderItem(placeholderKey, args.ContentDatabase,
                 args.LayoutDefinition);
   }
  }
  List<Item> collection = null;
  if (placeholderItem != null)
  {
   bool flag;
   args.HasPlaceholderSettings = true;
   collection = this.GetRenderings(placeholderItem, out flag);
   if (flag)
   {
    args.CustomData["allowedControlsSpecified"] = true;
    args.Options.ShowTree = false;
   }
  }
  if (collection != null)
  {
   if (args.PlaceholderRenderings == null)
   {
    args.PlaceholderRenderings = new List<Item>();
   }
   args.PlaceholderRenderings.AddRange(collection);
  }
 }
}
And to have the chrome appear correctly in the page editor.
public class GetDynamicPlaceholderChromeData : GetChromeDataProcessor
{
 //text that ends in a GUID
 private const string DYNAMIC_KEY_REGEX = @"(.+){[\d\w]{8}\-([\d\w]{4}\-){3}[\d\w]{12}}";

 public override void Process(GetChromeDataArgs args)
 {
  Assert.ArgumentNotNull(args, "args");
  Assert.IsNotNull(args.ChromeData, "Chrome Data");
  if ("placeholder".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
  {
   string argument = args.CustomData["placeHolderKey"] as string;

   string placeholderKey = argument;
   Regex regex = new Regex(DYNAMIC_KEY_REGEX);
   Match match = regex.Match(placeholderKey);
   if (match.Success && match.Groups.Count > 0)
   {
    // Is a Dynamic Placeholder
    placeholderKey = match.Groups[1].Value;
   }
   else
   {
    return;
   }

   // Handles replacing the displayname of the placeholder area to the master reference
   Item item = null;
   if (args.Item != null)
   {
    string layout = ChromeContext.GetLayout(args.Item);
    item = Client.Page.GetPlaceholderItem(placeholderKey, args.Item.Database, layout);
    if (item != null)
    {
     args.ChromeData.DisplayName = item.DisplayName;
    }
    if ((item != null) && !string.IsNullOrEmpty(item.Appearance.ShortDescription))
    {
     args.ChromeData.ExpandedDisplayName = item.Appearance.ShortDescription;
    }
   }
  }
 }
}
Along with a patch file to register these two custom pipelines.
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getPlaceholderRenderings>
        <processor
          type="MyProject.DynamicPlaceholders.GetDynamicKeyAllowedRenderings, MyProject"
          patch:before="processor[@type='Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel']"/>
      </getPlaceholderRenderings>

      <getChromeData>
        <processor
          type="MyProject.DynamicPlaceholders.GetDynamicPlaceholderChromeData, MyProject"
          patch:after="processor[@type='Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel']"/>
      </getChromeData>
    </pipelines>
  </sitecore>
</configuration>

You should now have dynamic placeholders working correctly. The main dynamic placeholder was taken from Nick Wesselman at Techphoria414. The placeholder settings/chrome was taken from user dunston on Stack Overflow.

No comments:

Post a Comment