Wednesday 16 January 2019

Restricting the number of times a rendering can be added to a placeholder

This is another one of those tasks that shouldn't really be necessary to do. Every time a client says "We don't want two of these components added to this placeholder" my answer is always "So don't add two." It's a people problem that companies want solved with technology. This solution is geared more toward the use of the Experience Editor and it is going to stop a rendering appearing in the "Select a rendering" dialog if it already exists in that placeholder the maximum number of times.

We're going to achieve this by modifying the getPlaceholderRenderings pipeline. This is the pipeline that loads the relevant components from the placeholder settings for display in a dialog box. We will inject a step at the end that is going to go through the renderings that Sitecore has selected to display, and for each rendering count the number of times that rendering already appears in the placeholder, and if it has reached its maximum configured limit it is removed from the list of valid renderings so that it doesn't appear in the dialog.

Before we write any code we're going to have to find a way to allow us to specify how many times we want a rendering to be used. I'm going to break the rules here slightly and modify a standard template which is common across the rendering types, and that template is "Rendering Options" which can be found here;

/sitecore/templates/System/Layout/Sections/Rendering Options

I'm going to add a shared Integer field called "Maximum Instances" that will allow us to specify the maximum number of times it can be added to a placeholder.


I can now configure my rendering to have a maximum instance of 2




Some more standard set-up is my placeholder that can contain both my restricted and unrestricted renderings.



That's the set up taken care of, the first bit of code is going to be a basic helper class called RestrictedRendering

namespace MyNamespace
{
    using Sitecore.Data.Items;

    public class RestrictedRendering
    {
        public Item RenderingItem { get; set; }

        public int Maximum { get; set; }
    }
}

Now we will create the pipline step that we'll be injecting, which is called RestrictRenderings. This will happen after Sitecore has done all of its work so when this step is executed the args are going to be populated with all of the renderings that are going to be shown in the Select a Rendering dialog.

namespace MyNamesapce
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Sitecore.Data;
    using Sitecore.Data.Items;
    using Sitecore.Diagnostics;
    using Sitecore.Layouts;
    using Sitecore.Pipelines.GetPlaceholderRenderings;

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

            if (args.PlaceholderRenderings == null || args.PlaceholderRenderings.Count == 0)
            {
                return;
            }

            try
            {
                // Get the name of the placeholder from the placeholder key
                // args.PlaceholderKey contains the key of the placeholder to add the renderings to
                string placeholder = GetPlaceholderName(args.PlaceholderKey);

                // get the layout for the device being edited.
                // The layout contains a list of all renderings on the device
                DeviceDefinition layoutDevice = GetDeviceDefinition(args);

                if (layoutDevice == null)
                {
                    return;
                }

                // GetRestrictedRenderings is defined below, it gets the list of all renderings
                // on the page that have a Maximum Instances value set
                foreach (var rendering in GetRestrictedRenderings(args))
                {
                    // Get the number of times the rendering already appears in the placeholder
                    int matchingRenderings = layoutDevice.Renderings.Cast<RenderingDefinition>()
                        .Count(x =>
                            new ID(x.ItemID) == rendering.RenderingItem.ID
                            && GetPlaceholderName(x.Placeholder).Equals(placeholder, StringComparison.InvariantCultureIgnoreCase)
                            );

                    // if the number of instances is at the maximum
                    // remove it from the list of renderings
                    if (matchingRenderings >= rendering.Maximum)
                    {
                        args.PlaceholderRenderings.Remove(rendering.RenderingItem);
                    }
                }
            }
            catch(Exception exp)
            {
                Log.Error($"RestrictRenderings.Process: {exp.Message}", exp, this);
            }
        }

        private static string GetPlaceholderName(string key)
        {
            return key.Split(new [] { '/' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
        }

        private static DeviceDefinition GetDeviceDefinition(GetPlaceholderRenderingsArgs args)
        {
            LayoutDefinition layout = LayoutDefinition.Parse(args.LayoutDefinition);

            if (layout.Devices.Count == 0)
            {
                return null;
            }

            // If there is no device ID in the args just use the first defined device
            // otherwise get the defined device from the list of devices
            return ID.IsNullOrEmpty(args.DeviceId) ? (DeviceDefinition) layout.Devices[0]
                : layout.Devices.Cast<DeviceDefinition>().FirstOrDefault(d => new ID(d.ID) == args.DeviceId);
        }

        private static List<RestrictedRendering> GetRestrictedRenderings(GetPlaceholderRenderingsArgs args)
        {
            var renderings = new List<RestrictedRendering>();

            foreach (Item renderingItem in args.PlaceholderRenderings)
            {
                int max = 0;
                int.TryParse(renderingItem["Maximum Instances"], out max);

                if (max > 0)
                {
                    renderings.Add(new RestrictedRendering { RenderingItem = renderingItem, Maximum = max });
                }
            }

            return renderings;
        }
    }
}

Now we need to use a config patch file to inject our new pipeline step

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
   <sitecore>     
      <pipelines>
        <getPlaceholderRenderings>
          <processor type="MyNamespace.RestrictRenderings, MyAssembly"
            patch:after="*[@type='Sitecore.Pipelines.GetPlaceholderRenderings.RemoveNonEditableRenderings, Sitecore.Kernel']" />
        </getPlaceholderRenderings>
      </pipelines>
   </sitecore>
</configuration>

Now let's see it working. I have my placeholder on a page and when I add a rendering I get a dialog that allows me to select either of my test renderings.



I'll add two of each rendering and try to add another rendering and now it only allows me to add the unrestricted rendering;


Issues

There is one issue with this implementation and that is that it affects the functionality of the move feature where if there are the maximum number of renderings in the given placeholder then those renderings can't be moved as they are not considered legal renderings for that placeholder.

Things to improve

There are some possible improvements to this solution such as finding a different way to store the Maximum Instances value that would allow you to have a different number of allowed renderings in different placeholders, so you might only want one instance of the rendering in a header, but up to three in a footer.

If you stick with having the Maximum Instances value on the rendering itself it would be better to use template inheritance rather than amending the built-in Rendering Options template.

No comments:

Post a Comment