Dusty Eves

dusty.eves@techno-babble.net

Sitecore Architect with Paragon Consulting

Referenced Renderings – How They Work

So you've seen what Referenced Renderings can do, but how do we do it...

Sitecore Artifacts

In that most of this was covered in the how-to I'm not going to go too deeply into these, but feel free to to dig into the install package if you're wanting a closer look.  An item that I will call out for the sake of being interesting is a small extension to the WebControl template.  This is unique among Sitecore renderings in that it has no data source property.  A simple matter to add to the template, as well as an Inner Placeholder field and suddenly we're in business.

insertRenderings Pipeline

The insertRenderings pipeline is where Sitecore parses the Layout XML and assembles the presentation hierarchy and is the linchpin of what we're looking to do. The meat of this pipeline is the AddRenderings processor which on it's face is very simple.

  1. Retrieve the item to be rendered from the pipeline arguments
  2. Retrieve the rendering definition for the current context device.
  3. Create a list of RenderingReference objects for each rendering in the definition.
  4. Add that list to the pipeline arguments.

All we need to enable referenced rendering is to add a check to step 3 to the effect of:

  • Determine if rendering is a Referenced Rendering.
  • If yes, retrieve the item to which it is pointing.
  • Add any renderings on said item to the pipeline arguments.

Done like so:

 
    /// <summary>
    /// Defines the get item class.
    /// </summary>
    public class AddRenderings : InsertRenderingsProcessor
    {
        
        private const string REFERENCED_RENDERING_ID = "{46FD2322-D655-4547-A3EB-2E91A7E77B2D}";
        /// <summary>
        /// Processes the specified args.
        /// </summary>
        /// <param name="args">The arguments.</param>
        public override void Process(InsertRenderingsArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            if (args.HasRenderings || args.ContextItem == null)
                return;
            DeviceItem device = Context.Device;
            if (device == null)
                return;
            
            List<RenderingReference> outList = new List<RenderingReference>();
            addRenderings(args.ContextItem, device, outList);
           
            args.Renderings.AddRange(outList);
            args.HasRenderings = args.Renderings.Count > 0;
        }

        private void addRenderings(Item renderingItem, DeviceItem _device, List<RenderingReference> renderList)
        {
            IEnumerable<RenderingReference> renderings = renderingItem.Visualization.GetRenderings(_device, true);
            foreach (RenderingReference rendering in renderings)
            {
                renderList.Add(rendering);
                if (rendering.RenderingID.ToString() == REFERENCED_RENDERING_ID 
                    || rendering.RenderingItem.InnerItem["Tag"] == "ReferencedRendering"
                    )
                {
                    Item renderingReference = null;
                    
                    if (!string.IsNullOrEmpty(rendering.Settings.DataSource))
                        renderingReference = renderingItem.Database.GetItem(new ID(rendering.Settings.DataSource));
                    if (renderingReference == null && !string.IsNullOrEmpty(rendering.RenderingItem.InnerItem["Data source"]))
                        renderingReference = renderingItem.Database.GetItem(new ID(rendering.RenderingItem.InnerItem["Data source"]));

                    if (renderingReference == null)
                    {
                        Log.Debug(string.Format("Rendering item {0}.  Target Item for Reference Rendering {1} is null", Sitecore.Context.Item.Paths.Path, rendering.RenderingID));
                        continue;
                    }
                    addRenderings(renderingReference, _device, renderList);
                }
            }
        }
    }

Referenced Rendering WebControl

The next thing we need is a WebControl for our rendering. In that most of the heavy lifting is done in the pipeline the base control Sitecore.Web.UI.WebControls.Rendering already does almost everything that we need. The one additional thing we need is something that will instantiate our inner placeholders. This is a simple matter of extending the CreateChildControls method our base control.

 
    public class ReferencedRendering : Sitecore.Web.UI.WebControls.Rendering
    {
       
        protected override void CreateChildControls()
        {
            if (!string.IsNullOrEmpty(InnerPlaceholder))
            {
                Placeholder ph = new Placeholder();
                ph.Key = InnerPlaceholder;
                Controls.Add(ph);
            }
            base.CreateChildControls();
        }

        private string InnerPlaceholder
        {
            get
            {
                var parameters = WebUtil.ParseUrlParameters(Parameters);
                if (!parameters.AllKeys.Contains("Inner Placeholder"))
                    return string.Empty;
                else
                    return parameters["Inner Placeholder"];
            }
        }
        protected override void Render(System.Web.UI.HtmlTextWriter output)
        {
            #if DEBUG
                if (!string.IsNullOrEmpty(InnerPlaceholder))
                    output.Write(string.Format("<!-- <InnerPlaceholder id='{0}'> -->", InnerPlaceholder));
            #endif
            
            base.Render(output);

            #if DEBUG
                if (!string.IsNullOrEmpty(InnerPlaceholder))
                    output.Write("\r\n<!-- </InnerPlaceholder> -->");
            #endif
        }
    }

Note here that we also override the Render method. This is entirely unnecessary to get our desired effect but as is provides us with some HTML comments that do well for demonstration and debugging purposes.

Save Handler

You'll recall from our initial scaffolding that much the same as with other presentation artifacts we allowed that the data source and inner placeholder to be defined on the rendering definition itself as well as to be defined in the items presentation details. This works well from a design perspective but at runtime we want the inner placeholder to be available as a rendering parameter. The reason for this is retrieving it from the presentation definition and render time creates some unnecessary database calls and it more efficient to do as a save handler.

Though it looks complex at first glance the handler itself is pretty straight-forward.

  1. Check the saving item for presentation details.
  2. If found, loop through renderings for any that are ReferencedRenderings.
  3. For any ReferencedRenderings, copy the inner placeholder value from the rendering definition item, to the rendering parameters.
 
    public class SaveRenderingHandler
    {
        private Database masterDB { get { return Sitecore.Configuration.Factory.GetDatabase("master"); } }

        public void ProcessRenderingDataSource(object sender, EventArgs e)
        {
            Item _saveItem = Event.ExtractParameter(e, 0) as Item;

            if (_saveItem.Database.Name != "master" || _saveItem.Fields[Sitecore.FieldIDs.LayoutField] == null || string.IsNullOrEmpty(_saveItem.Fields[Sitecore.FieldIDs.LayoutField].Value))
                return;
            XmlDocument layoutXml = new XmlDocument();
            layoutXml.LoadXml(_saveItem[Sitecore.FieldIDs.LayoutField]);

            foreach (DeviceItem device in Devices(_saveItem))
                foreach (RenderingReference rendering in _saveItem.Visualization.GetRenderings(device, true))
                {
                    if (rendering.RenderingItem.InnerItem["Namespace"] != "TB.ReferencedRendering")
                        continue;
                    
                        copyToRenderParameters(rendering, device, layoutXml);

                }
            _saveItem.Fields[Sitecore.FieldIDs.LayoutField].Value = layoutXml.DocumentElement.OuterXml;
        }

        private void copyToRenderParameters(RenderingReference _rendering, DeviceItem _device, XmlDocument layoutXML)
        {
            XmlNamespaceManager nsMgr = new XmlNamespaceManager(layoutXML.NameTable);
            nsMgr.AddNamespace("s", "s");
            XmlNode rNode    = layoutXML.SelectSingleNode(string.Format("/r/d[@id='{0}']/r[@s:id='{1}']", _device.ID, _rendering.RenderingID.ToString()), nsMgr);
            
            if (!string.IsNullOrEmpty(_rendering.RenderingItem.InnerItem["Inner Placeholder"]) && rNode.Attributes["par"] == null)
            {
                XmlAttribute parameters = layoutXML.CreateAttribute("par");
                parameters.Value = string.Format("Inner Placeholder={0}", _rendering.RenderingItem.InnerItem["Inner Placeholder"]);
                rNode.Attributes.Append(parameters);
            }
        }
        /// <summary>
        /// Root item at /sitecore/layoouts/Devices
        /// </summary>
        private const string DEVICES_ROOT = "{E18F4BC6-46A2-4842-898B-B6613733F06F}";

        /// <summary>
        /// Cached Device IDs defined in the site
        /// </summary>
        private static string[] DeviceIDs;
        private static object DeviceSyncRoot = new object();

        /// <summary>
        /// Grabs add devices for which the item has presentation defined
        /// </summary>
        /// <param name="_item">Item for which to evaluate presentation</param>
        /// <returns></returns>
        private IEnumerable<DeviceItem> Devices(Item _item)
        {
            if (DeviceIDs == null)
                BuildDeviceList();

            string layoutXML = _item[Sitecore.FieldIDs.LayoutField];

            return DeviceIDs
                .Where(i => layoutXML.Contains(i))
                .Select(i => new DeviceItem(masterDB.GetItem(new ID(i))));
        }

        private void BuildDeviceList()
        {
            lock (DeviceSyncRoot)
                if (DeviceIDs == null)
                    DeviceIDs =
                        masterDB.GetItem(new ID(DEVICES_ROOT))
                        .Children.Select(i => i.ID.ToString()).ToArray();
        }

    }

The save handler in place our inner placeholders are now passed up the rendering chain and we're in business. No more copy-paste presentation component sets from here on out!

Like what you see? Something I missed? Have an even cooler way to do the same thing?!?! Let me know in the comments below.

Leave a Reply

Your email address will not be published. Required fields are marked *