Inherited and non-inherited fields to Sitecore clone items

When an item is cloned in Sitecore, the clones inherits its values from the source item. This is represented by a null value in each field, meaning that it inherits its value from the clone source item. When a value is written to a field in a clone, that value is used instead, hence breaking the inheritance. This works great in most cases.

In some scenarios you might not want to inherit all the fields. You might want to exclude some of them, enforcing a local value in each field for such clones. By default a few fields are not inherited. Those are __Created, __Created by__Updated, __Updated by, __Revision, __Source, __Source item, __Workflow, __Workflow state and __Lock. It’s quite natural that those fields are not inherited to clones, since each item, the source and the clone, should keep their own values of those fields.

You can add your own fields to this list by modifying the ItemCloning.NonInheritedFields setting. It’s a string setting where you can provide a pipe (|) separated list list of field ID’s or field keys. The drawback of the setting being a pipe separated list, is that it’s hard to add additional fields through config patch files. I hope Sitecore will change this in the future.

Depending on your solution requirements, it might be a good idea to add the fields __Never publish, __Publish (date), __Unpublish (date), __Valid from, __Valid to and __Hide version to the ItemCloning.NonInheritedFields setting. Example: You have a source item that is cloned. If you add a versions to the source item and mark some of them as “Hide version”. Is the intention that all clones of that item, pointing the hidden versions, is also meant to be hidden too? By default, this would be the behavior, but I guess this behavior isn’t natural to most editors. But again, this depends on the solution and what purpose clones are serving. 

<!-- List of suggested built-in fields to add to the non-inherited fields as above -->
<setting name="ItemCloning.NonInheritedFields" value="{9135200A-5626-4DD8-AB9D-D665B8C11748}|{86FE4F77-4D9A-4EC3-9ED9-263D03BD1965}|{7EAD6FD6-6CF1-4ACA-AC6B-B200E7BAFE88}|{C8F93AFE-BFD4-4E8F-9C61-152559854661}|{4C346442-E859-4EFD-89B2-44AEDF467D21}|{B8F42732-9CB8-478D-AE95-07E25345FB0F}" />

Another good contender for this non-inherited field setting is the __Final Renderings field. When building large multi-market, multi-language solutions, clones are often used. Usually a “master” site is created, translated and then cloned into markets. Central changes to pages can therefore be stored i the shared __Rendering fields and it can be inherited to the clones. Links/Datasource rewrites are needed, but that’s a too big topic to be covered in this post. It makes sense to allow inheritance of the shared rendering fields, but not allowing inheritance of the final rendering field. Thereby changes to the shared rendering fields will apply “globally” and changes to the versioned final renderings field will apply “locally”. 

Now, this introduces a new problem. Let’s say you have a cloned item, where the shared renderings field is inherited (i.e. is null on the clone) and there are local changes to the final renderings field on both the source item and the item clone. If an author of the item clone uses the “Reset Layout” function and resets the final layout, the non-inherited final renderings field will get a null value and it will also inherit the source item value in its final rendering field. This was never the intention, right?

It turns out that the ItemCloning.NonInheritedFields setting is only used when the item is being cloned. Thereafter it’s never used anywhere. This means that if an editor resets any field on a clone, that field will be set to null and will inherited from its source. You can try this yourself by resetting one of the built-in non-inherited fields, such as the __Lock or __Workflow state fields. 

We can mostly solve this by adding a custom item:saving processor that’ll avoid resetting a non-inherited field to null, and instead force a default value typically an empty string. Example code below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Events;

namespace My.Namespace
{
    /// <summary>
    /// Fields specified in the "ItemCloning.NonInheritedFields" setting, and the
    /// built in fields, are not supposed to inherited from source items to its
    /// clones. However, resetting a field value on a clone, makes the clone
    /// inherit the source item value anyway. This seems to be a design flaw in
    /// Sitecore. This processor intends to correct this behavior.
    /// </summary>
    public class ItemCloningNonInheritedFieldsSavingHandler
    {
        private static HashSet<ID> _nonInheritedFieldIDs = null;
        private static HashSet<string> _nonInheritedFieldKeys = null;
        private static readonly object Lock = new object();

        private FieldInfo _fieldFieldInfo;
        private FieldInfo FieldFieldInfo => _fieldFieldInfo ?? (_fieldFieldInfo = typeof(FieldChange).GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance));

        private MethodInfo _getChangesMethodInfo;
        private MethodInfo GetChangesMethodInfo => _getChangesMethodInfo ?? (_getChangesMethodInfo = typeof(Item).GetMethod("GetChanges", BindingFlags.NonPublic | BindingFlags.Instance));

        public void OnItemSaving(object sender, EventArgs args)
        {
            Item savingItem = Event.ExtractParameter(args, 0) as Item;
            // Do this only for clones in the content tree on the master database
            if (savingItem == null || savingItem.Database.Name.ToLower() != "master" || 
                !savingItem.Paths.IsContentItem || !savingItem.IsItemClone)
            {
                return;
            }

            LoadConfig(); // Ensure config is loaded

            // Due to an error(?) in Sitecore, there is no good way to finding if a field has been reset.
            // Value is "", HasValue is true and ResetBlank is false. I'd expect the opposite...
            // So there's really no way to distinguish between null/reset and storing an empty string
            // without calling the internal private methods using reflection
            var changes = (ItemChanges)GetChangesMethodInfo.Invoke(savingItem, new object[] { true });
            var fieldChanges = changes.FieldChanges.Cast<FieldChange>().ToList();

            // Loop through all reset blank fields
            foreach (FieldChange fieldChange in fieldChanges.Where(fc => ((Field)FieldFieldInfo.GetValue(fc)).ResetBlank))
            {
                var field = savingItem.Fields[fieldChange.FieldID];
                if (_nonInheritedFieldIDs.Contains(fieldChange.FieldID) || _nonInheritedFieldKeys.Contains(field.Key))
                {
                    // Force storing the "blank" value. This is typically an empty string, but may be a
                    // standard value or similar. It should not be the clone source value.
                    var alternativeValue = field.GetValue(true, true, true, false) ?? string.Empty;
                    field.SetValue(alternativeValue, true);
                }
            }
        }

        /// <summary>
        /// Builds two static hash sets of field ID's and field Keys(names) that
        /// this pipeline processor applies to. The config is taken from Sitecore's
        /// clone config.
        /// Does nothing if the config is already loaded and parsed. 
        /// </summary>
        protected virtual void LoadConfig()
        {
            if (_nonInheritedFieldKeys != null &amp;&amp; _nonInheritedFieldIDs != null)
                return;

            lock (Lock)
            {
                if (_nonInheritedFieldKeys != null &amp;&amp; _nonInheritedFieldIDs != null)
                    return;

                // Load the list of built-in fields
                var nonInheritedFieldIDs = new HashSet<ID>(CloneItem.GetNonInheritedFieldIDs());
                // Add the list of additional fields (may be ID's or field names/keys)
                var nonInheritedFieldKeys = new HashSet<string>();
                foreach (var fieldKey in CloneItem.GetNonInheritedFieldKeys())
                {
                    if (ID.TryParse(fieldKey, out var fieldId))
                        nonInheritedFieldIDs.Add(fieldId);
                    else
                        nonInheritedFieldKeys.Add(fieldKey);
                }

                _nonInheritedFieldIDs = nonInheritedFieldIDs;
                _nonInheritedFieldKeys = nonInheritedFieldKeys;
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" 
               xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:env="http://www.sitecore.net/xmlconfig/env/">
  <sitecore>
    <settings>
      <!--
      ITEM CLONING NON INHERITED FIELDS
      Built-in:
        __NeverPublish = {9135200A-5626-4DD8-AB9D-D665B8C11748}
        __PublishDate = {86FE4F77-4D9A-4EC3-9ED9-263D03BD1965}
        __UnpublishDate = {7EAD6FD6-6CF1-4ACA-AC6B-B200E7BAFE88}
        __Final Renderings = {04BF00DB-F5FB-41F7-8AB7-22408372A981}
        __Hide version = {B8F42732-9CB8-478D-AE95-07E25345FB0F}
        __Valid from = {C8F93AFE-BFD4-4E8F-9C61-152559854661}
        __Valid to = {4C346442-E859-4EFD-89B2-44AEDF467D21}
      -->
      <setting name="ItemCloning.NonInheritedFields" set:value="{9135200A-5626-4DD8-AB9D-D665B8C11748}|{86FE4F77-4D9A-4EC3-9ED9-263D03BD1965}|{7EAD6FD6-6CF1-4ACA-AC6B-B200E7BAFE88}|{04BF00DB-F5FB-41F7-8AB7-22408372A981}|{B8F42732-9CB8-478D-AE95-07E25345FB0F}|{C8F93AFE-BFD4-4E8F-9C61-152559854661}|{4C346442-E859-4EFD-89B2-44AEDF467D21}" />
    </settings>

    <events role:require="ContentManagement or Standalone">
      <event name="item:saving">
        <handler type="My.Namespace.ItemCloningNonInheritedFieldsSavingHandler, My.Assembly" method="OnItemSaving" />
      </event>
    </events>
  </sitecore>
</configuration>

This solution have one limitation though. When a field is in the non-inherited field list, the built-in CloneItem operation copies any value from the source item to the clone by using the SetValue(value, force:true) method. This means that an initial value from the clone source cannot be restored. On the other hand, such value might not be expected to be copied in the first place for such fields. Consider overriding the clone operation as well in order to get a consistent behavior on this.

Leave a Reply