Blog

Ramblings about development and woodworking

 

Dynamic Sitecore MVC Placeholders

Posted on in Development by Kyle Heon

No Comments
Praesent dictum diam

So about a month ago I ran into a well known dynamic placeholder issue with Sitecore. If you aren't aware of this issue I encourage you to check out previous posts from Matthew Kenney, John Newcombe, Nick Wesselman and David Leigh. My current project however is using Sitecore MVC and these solutions cover webforms implementations.

When I solicted feedback via Twitter Nick Wesselman was kind enough to point out this Stackoverflow post which appeared to be exactly what was needed, so we (co-worker Joe and myself) set out to get this integrated. There were a few wrinkles with the code/configuration as listed so I figured it was a good idea to document what was needed to get this working correctly.

As the first issue we ran into was with configuration I'm starting off with the proper overrides configuration. This is what our custom Pipelines.config file looks like:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getPlaceholderRenderings>
        <processor patch:before="*[@type='Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel']"
                   type="MyProject.Custom.Pipelines.GetPlaceholderRenderings.GetDynamicKeyAllowedRenderings, MyProject.Custom"
                   />
      </getPlaceholderRenderings>

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

The following extension method adds a new DynamicPlacholder method to the SitecoreHelper class which creates the dynamic placeholder key by appending the unique id from the rendering to the key passed in. The code for this is shown below:

public static class DynamicPlaceholderExtension
{
    public static HtmlString DynamicPlaceholder(this SitecoreHelper helper, string dynamicKey)
    {
        string DynamicKey = GetDynamicKey(dynamicKey);
        if (!String.IsNullOrWhiteSpace(DynamicKey)) dynamicKey = DynamicKey;

        Guid currentRenderingId = RenderingContext.Current.Rendering.UniqueId;
        return helper.Placeholder(String.Format("{0}_{1}", dynamicKey, currentRenderingId));
    }

    private static string GetDynamicKey(string placeHolderName)
    {
        bool NeedIncrement = false;
        int IncrementStep = 0;
        IEnumerable<PlaceholderContext> myPlaceholders = ContextService.Get().GetInstances<PlaceholderContext>();

        foreach (PlaceholderContext context in myPlaceholders)
        {
            if (context.PlaceholderName == placeHolderName ||
                context.PlaceholderName.StartsWith(placeHolderName + "_"))
            {
                NeedIncrement = true;
                IncrementStep++;
            }
        }

        if (NeedIncrement)
        {
            placeHolderName += "_" + IncrementStep.ToString(CultureInfo.InvariantCulture);
        }

        return placeHolderName;
    }
}

The following pipeline code is used to extract existing placeholder settings assigned to the non-dynamic placeholder name. This section of code had to be modified from what was provided on the Stackoverflow post, here is where we netted out:

/// <summary>
///     Handles changing context to the references dynamic "master" renderings settings for inserting the allowed controls for the placeholder and making it editable
/// </summary>
public class GetDynamicKeyAllowedRenderings : GetAllowedRenderings
{
    //text that ends in a GUID
    private const string DynamicKeyRegex = @"(.+)_[\d\w]{8}\-([\d\w]{4}\-){3}[\d\w]{12}";

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

        string placeholderKey = args.PlaceholderKey;
        var regex = new Regex(DynamicKeyRegex);
        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 = GetRenderings(placeholderItem, out flag);
            if (flag)
            {
                args.Options.ShowTree = false;
            }
        }
        if (collection != null)
        {
            if (args.PlaceholderRenderings == null)
            {
                args.PlaceholderRenderings = new List<Item>();
            }
            args.PlaceholderRenderings.AddRange(collection);
        }
    }
}

In case you are interested in what we changed, we removed the following line:

args.CustomData["allowedControlsSpecified"] = true;

This line was just above:

args.Options.ShowTree = false;

Finally, in an effort to keep the pretty placeholder names the following pipeline code is necessary:

/// <summary>
///     Replaces the Displayname of the Placeholder rendering with the dynamic "parent"
/// </summary>
public class GetDynamicPlaceholderChromeData : GetChromeDataProcessor
{
    //text that ends in a GUID
    private const string DynamicKeyRegex = @"(.+)_[\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))
        {
            var argument = args.CustomData["placeHolderKey"] as string;

            string placeholderKey = argument;
            var regex = new Regex(DynamicKeyRegex);
            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
            if (args.Item != null)
            {
                string layout = ChromeContext.GetLayout(args.Item);
                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;
                }
            }
        }
    }
}

I can't take credit for really any of this code as I pulled it all from the Stackoverflow post and only made some tweaks to clean it up and get it working. I unleashed ReSharper on the code as well. I only hope that this summary of how to get dynamic placeholders working with Sitecore MVC saves someone time in the future. As always, if you tackled this in a different way let me know in the comments below.

comments powered by Disqus

Categories

About

Dust Collected was founded in 2014 by Kyle Heon and is a play on a series of words that have meaning in both software engineering and woodworking.