Dynamic Placeholders on Sitecore 7.2

Posted 20 January 2015 12:00 AM by Alan Yip, Senior Sitecore Consultant@ClearPeople

There comes a time when a client requires something slightly richer in editorial experience and as an advocate of this, we always try and find a way to allow them to do this.

It just so happens that a client wanted exactly this:

The requirement

The client wanted the ability to design the layout of a page at creation time. Now this is slightly different to actually dropping in controls on a page to a set number of placeholders. This is in fact allowing the editor to drop in these placeholders dynamically so they could essentially create one, two, three or more columns for instance on a page.

The requirement also wanted the ability to choose the width of each column and its background.

Sitecore out of the box does not allow for this unless we define set column pages at the start.

The solution

So we needed to have a dynamic way of adding placeholders onto a page. The solution was found in a blog created by John Newcombe which was based on a technique introduced by Nick Wesselman.

The basic idea is to allow multiple placeholders with the same key to exist on the same layer by making each one unique.

The code

To get started, you will need to create the following files:
  • GetDynamicKeyAllowedRenderings.cs 
  • DynamicKeyPlaceholder.cs
  • GetDynamicKeyPlaceholderChromeData.cs
  • ItemEventHandler.cs
  • DynamicPlaceHolderConfig.config

The class files should be created and compiled as an assembly somewhere in your web folder. The config file should be included under the App_Config/Includes folder.

The below files are an adaptation of John Newcombe’s solution.

GetDynamicKeyAllowedRenderings.cs

Copy the code below into GetDynamicKeyAllowedRenderings.cs. This class handles identifying the dynamic placeholders.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetPlaceholderRenderings;
using Sitecore.Text;

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

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

            //this taken from
            //http://stackoverflow.com/questions/15134720/sitecore-dynamic-placeholders-with-mvc


            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> renderings = null;
            if (placeholderItem != null)
            {
                bool allowedControlsSpecified;
                args.HasPlaceholderSettings = true;
                renderings = this.GetRenderings(placeholderItem, out allowedControlsSpecified);
                if (allowedControlsSpecified)
                {
                    args.CustomData["allowedControlsSpecified"] = true;
                    args.Options.ShowTree = false; //Remove this line if using Sitecore 6.5 (see text)
                }
            }

            if (renderings != null)
            {
                if (args.PlaceholderRenderings == null)
                {
                    args.PlaceholderRenderings = new List<Item>();
                }
                
                args.PlaceholderRenderings.AddRange(renderings);
                
                if (!args.Options.ShowTree)
                {
                    args.Options.ShowTree = false;
                }
            }
        }
    }
}

DynamicKeyPlaceholder.cs

Copy the code below into DynamicKeyPlaceholder.cs. This class handles adding the unique GUID to the end of the placeholder key. This makes each placeholder key unique.

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

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

        public Placeholder InnerPlaceholder
        {
            get { return _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);
                if (stack.Count == 0)
                {
                    //not used within a placeholder apparently. dynamic key is actually not necessary in this case.
                    return _dynamicKey;
                }
                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
    }
}

GetDynamicKeyPlaceholderChromeData.cs

Copy the code below into GetDynamicKeyPlaceholderChromeData.cs. This class handles the removal of the GUID from the key for presentation purposes.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetChromeData;

namespace Your.Namespace
{
    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;
                Regex regex = new Regex(GetDynamicKeyAllowedRenderings.DYNAMIC_KEY_REGEX);
                Match match = regex.Match(placeholderKey);
                if (match.Success && match.Groups.Count > 0)
                {
                    string newPlaceholderKey = match.Groups[1].Value;
                    args.CustomData["placeHolderKey"] = newPlaceholderKey;
                    base.Process(args);
                    args.CustomData["placeHolderKey"] = placeholderKey;
                }
                else
                {
                    base.Process(args);
                }
            }
        }
    }
}

ItemEventHandler.cs

Copy the code below into ItemEventHandler.cs. This class handles the removal of orphaned presentation controls where their parent placeholders have been deleted.

The event is called OnItemSaved event in Sitecore.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Events;

namespace Your.Namespace
{
    class ItemEventHandler
    {
        
        public const string DYNAMIC_KEY_REGEX = @"(.+){[\d\w]{8}\-([\d\w]{4}\-){3}[\d\w]{12}}";   

        public void OnItemSaved(object sender, EventArgs args)
        {
            var item = Event.ExtractParameter(args, 0) as Item;

            if (item != null)
            {
                //if the rendering reference points to a dynamic placeholder then ensure that that placeholder exists
                //if not then remove the reference. This takes care of the scenario where a scaffolding
                //component has been removed without first removing the Sub-Layouts that may by bound to it.

                var device = Context.Device;
                if (device != null)
                {
                    var renderingReferences = item.Visualization.GetRenderings(device, false);

                    foreach (var renderingReference in renderingReferences)
                    {
                        var key = renderingReference.Placeholder;
                        var regex = new Regex(DYNAMIC_KEY_REGEX);
                        var match = regex.Match(renderingReference.Placeholder);

                        if (match.Success && match.Groups.Count > 0)
                        {

                            //get the rendering reference unique id that we are contained in
                            //added by ANY - getting GUID_LENGTH from the 
                            //"<setting name="DefaultBaseTemplate" value="{1930BBEB-7805-471A-A3BE-4858AC7CF696}" />" setting

                            int GUID_LENGTH = Sitecore.Configuration.Settings.DefaultBaseTemplate.Length;
                            //added by ANY
                            var parentRenderingId = key.Substring(key.Length - GUID_LENGTH, GUID_LENGTH).ToUpper();

                            //if this parent renderingReference is not in the current list of rendering references 
                            //then the current rendering reference should be removed as it means that the parent
                            //rendering reference has been removed by the user without first removing  the children

                            if (renderingReferences.All(r => r.UniqueId.ToUpper() != parentRenderingId))
                            {
                                //use an extension method to remove the orphaned rendering reference
                                //from the item's layout definition

                                RemoveRenderingReference(item, renderingReference.UniqueId);
                            }
                        }
                    }
                }
            }
        }//

        public static void RemoveRenderingReference(Item item, string renderingReferenceUid)
        {
            var doc = new XmlDocument();
            doc.LoadXml(item[FieldIDs.LayoutField]);

            //remove the orphaned rendering reference from the layout definition
            var node = doc.SelectSingleNode(string.Format("//r[@uid='{0}']", renderingReferenceUid));

            if (node != null && node.ParentNode != null)
            {
                node.ParentNode.RemoveChild(node);

                //save layout definition back to the item
                using (new EditContext(item))
                {
                    item[FieldIDs.LayoutField] = doc.OuterXml;
                }
            }
        }
        
    }//class ItemEventHandler
}

DynamicPlaceHolderConfig.config

Copy the code below into DynamicPlaceHolderConfig.config and place this file into the App_Config/Include folder. This config file ties all the above files together to enable the ability to add dynamic placeholders in your Sitecore solution.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="item:saved">
        <handler type="Your.Namespace.ItemEventHandler, Your.Assembly" method="OnItemSaved"
                     patch:before="handler[@type='Sitecore.Links.ItemEventHandler, Sitecore.Kernel' and @method='OnItemSaved']"/>
      </
event>
    </
events>
    <
pipelines>
      <
getPlaceholderRenderings>
        <
processor type=" Your.Namespace.GetDynamicKeyAllowedRenderings, Your.Assembly "
                   patch:before="processor[@type='Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel']"/>
      </getPlaceholderRenderings>
      <getChromeData>
        <processor type="Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel">
          <patch:attribute name="type">Your.Namespace.GetDynamicKeyPlaceholderChromeData, Your.Assembly</patch:attribute>
        </processor>
      </getChromeData>
    </pipelines>
  </sitecore>
</configuration>

Web.config addition

An additional step has also been added to the web.config so that the DynamicKeyPlaceholder can be used in your code.

You will need to add the following to system.web/pages/controls:

<add tagPrefix="uc" namespace="Your.Namespace" assembly="Your.Assembly"/>

Customisation

With many requirements we receive from clients, it is not normally as simple as 1, 2, 3. Our example also gives the editor the ability to customise the width and background of these placeholders.

So the easiest solution to this was to add Sitecore’s Rendering Parameters to these special sublayouts.

The fields we wanted are two DropLinks containing a list of class names which will eventually be used to style the sublayouts.

An editor will then simply choose the type of column they want by selecting these dropdown lists as shown below:

customisation editor 

The above example shows the selected 960 grid layout and the background gradient selection.

test page 

As you can see above, we can simply drop in two sublayouts containing the same placeholder key but utilising the dynamic placeholder implementation to achieve a much better user experience in designing page layouts.

Example usage

In order to achieve the above layout with rendering parameters, the following sublayout markup is used:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="GridContainer.ascx.cs" Inherits="Your,Namespace.GridContainer" %>
<div class="<%= GridClass %> <%= GradientBgClass %>">
    <uc:DynamicKeyPlaceHolder ID="scGridContainer" runat="server" Key="GridContainer"></uc:DynamicKeyPlaceHolder>
</div>

GridClass and GradientBgClass are the values taken from the rendering parameters explained above. These are class names contained in the CSS.

Share:

Add your comment

 
 

 

Archive

Tagcloud

Microsoft Teams Office 365 Yammer cms content management system agile GDPR Microsoft Graph collaboration Microsoft sharepoint 2016 upgrade migration SharePoint Online 2016 Tech Trends Digital Disruption Context marketing marketing SharePoint 2010 SharePoint 2013 TFS Git security kentico Analytics intranet jquery QA Quality Assurance testing digital workspace content management websites Sitecore sitecore marketplace sitecore module cloud Microsoft Cloud Storage digital strategy technical consulting sitecore modules Experience database Sitecore 7 Sitecore 8 support account management customer experience Data Storage windows azure cms integration front end front end development prototype Cloud Storage StorSimple Front-end Development Layout SharePoint 2013 colour palette UI design website design log viewer sitecore cms website Azure big data business-critical sharepoint accessibility android apple chrome clear people clearpeople debug emulator ios mobile testing opera resize adobe desktop flash ie10 internet explorer 10 metro windows 8 bcsp SharePoint Advanced System Reporter reporting framework ControlMode form control master page placeholder publishing console SharePoint 2007 SharePoint error search search results search values software testing testing scenario audit content information architecture retention schedules PowerShell QuickLaunch scripts SharePoint server 2010 business solutions metalogix replication replicator storagepoint stena technet UK Technet picture library slideshow web part RTM released to manufacturing caml caml query MOSS 2007 query infopath