Experience Sitecore ! | Updating existing presentation details of base template's standard values after it has been set on derived templates

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

Updating existing presentation details of base template's standard values after it has been set on derived templates

Problem: let's imagine the situation when you may have some complex template inheritance. You would like to set presentation details for each of these templates' standard values, somehow like below:

A - define layout, and header/footer common holders that will be shared for all sites (we've got a multisite setup for now)

B - define footer and header on a site level so that they remain the same within each of implemented sites 

C - define base page template that will add the rest of presentation shared between the pages (but not homepage)

Once you set presentation for standard values of A, you can go to standard values of B and see these changes, so that you add only B-specific components; the same exact story will be when you will go next to std. values of C an so on. So far, so good.

The problem occurs when you want to modify presentation coming with a base template after a derived presentation is already set. In that case, you will not see any difference, that may be seen a weird for a while. In our example, imagine you've set presentation for std. values of all three templates - A, B and then C, and then decided to add one more component to a presentation on A template (or change datasource item of existing etc.). You do changes for std. values of A, save it and as you see - these changes come into a play for template A, however, once you open B or C  they won't be there...

Explanation: let's think what in fact Standard Values are - just the default values for each of field defined in that (or parent) template. In the second case if a field has standard values for both parent and inherited template - it simply overrides parent value with inherited child's value. But, wait for a second - presentation is also stored in fields of Standard Template that all pages inherit from, how that makes possible, does it simply override?

No, for such particular cases when presentation fields are involved - override behaviour would not work at all. Let's look at our example - template A defines layout and header/footer sublayout and all that goes within __Renderings field - it's where (shared, not versioned) presentation is stored in XML-serialised format. But then, it would be overridden by setting concrete footer with no layout. Since it is the same field - it will lose layout at template B level and entire behaviour does not make any sense. To address this issue Sitecore implements a feature called Layout Deltas - so that presentation fields are not stupidly overwritten. Instead, after we defined default presentation for template A, it goes as is, as A does not have any base template with presentation set. But when setting presentation for B - it will only save the difference between itself and presentation of base template (if exists). When page is being rendered, Sitecore is wise enough to construct resulting page presentation from all the base templates only adding deltas with each derived template. That is how Layout Deltas work.

One may create multilevel presentation inheritance of standard values, appending more and more presentation on each of derived levels. However, when we want to adjust the presentation of base template (A) of current template (B) - changes will be affected only for A, but not B or C if they already have layout deltas defined. That behaviour raises questions without a doubt.

Solution: what we need to do to in order to ensure changing presentation details for A will be affected for all derived templates' items is to recourse inheritance tree of A and re-calculate layout delta for each of them with recent updates from A. In order to get this done I have re-worked a solution suggested by ... The difference is that since that time we now got a feature called Versioned Layouts, so that we need to operate both fields - __Renderings and __Final Renderings correspondingly. Apart from that I have tested it for a while and fixed few of stability issues.

Implementation: when we change presentation - we change the field, so eventually the holding item is being updated. In order to intercept this we add a pipeline processor for item:saving event:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <events>
      <event name="item:saving">
        <handler type="Sitecore.Foundation.Presentation.Services.LayoutInheritance, Sitecore.Foundation.Presentation" method="OnItemSaving"/>
      </event>
    </events>
  </sitecore>
</configuration>

And the code. I am using XmlDeltas.GetDelta() and XmlDeltas.ApplyDelta() static classes of Sitecore.Data.Fields namespace to make this work.

using System;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Events;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.Globalization;
using Sitecore.SecurityModel;

namespace Sitecore.Foundation.Presentation.Services
{
    public class LayoutInheritance
    {
        public void OnItemSaving(object sender, EventArgs args)
        {
            Item item = Event.ExtractParameter(args, 0) as Item;
            PropagateLayoutChanges(item);
        }

        private void PropagateLayoutChanges(Item item)
        {
            if (StandardValuesManager.IsStandardValuesHolder(item))
            {
                Item oldItem = item.Database.GetItem(item.ID, item.Language, item.Version);

                PropagateLayoutChangesForField(item, oldItem, FieldIDs.LayoutField);
                PropagateLayoutChangesForField(item, oldItem, FieldIDs.FinalLayoutField);
            }
        }

        private void PropagateLayoutChangesForField(Item item, Item oldItem, ID layoutField)
        {
            string layout = item[layoutField];
            string oldLayout = oldItem[layoutField];

            if (layout != oldLayout)
            {
                string delta = XmlDeltas.GetDelta(layout, oldLayout);
                foreach (Template templ in TemplateManager.GetTemplate(item).GetDescendants())
                {
                    ApplyDeltaToStandardValues(templ, layoutField, delta, item.Language, item.Version, item.Database);
                }
            }
        }

        private void ApplyDeltaToStandardValues(Template template, ID layoutField, string delta, Language language, Sitecore.Data.Version version, Database database)
        {
            if (template?.StandardValueHolderId != (ID)null)
            {
                try
                {
                    Item item = ItemManager.GetItem(template.StandardValueHolderId, language, version, database, SecurityCheck.Disable);

                    if (item == null)
                    {
                        Log.Warn($"Foundation.Presentation: Item is null {template.StandardValueHolderId} in database {database.Name}", template);
                        return;
                    }

                    Field field = item.Fields[layoutField];

                    if (field == null)
                    {
                        Log.Warn($"Foundation.Presentation: Field is null in item {item.ID} in database database.Name", item);
                        return;
                    }

                    if (!field.ContainsStandardValue)
                    {
                        string newFieldValue = XmlDeltas.ApplyDelta(field.Value, delta);
                        if (newFieldValue != field.Value)
                        {
                            using (new EditContext(item))
                            {
                                LayoutField.SetFieldValue(field, newFieldValue);
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    Log.Info($"Foundation.Presentation: Exception {e.Message}", e);
                    throw;
                }
            }
        }
    }
}
As I am using Helix in my development, I created a foundation module Presentation and placed the above code and config into it correspondingly. 

Hope this helps someone!
Comments are closed