Sitecore Clones explained in more detail: Part 2

In Sitecore Clones explained in more detail: Part 1 – we went over a few items things about clones that could be useful in deciding how and where to use them, and their basic function. In this post, I’ll go over a specific code-related solution we had to apply to get all our business cases implemented.

Auto accept of the notifications when parent item is changed (or a new item is added under a parent item that has been cloned)

One of things mentioned in the previous post was about how clones replicate themselves, and keep themselves in sync. It is by way of notifications that are created when the clone is changed or new items added under the parent item. A content editor then has to accept the change or reject it. If accepted, the resulting change from the parent is then copied over/implemented into the clone. This is great when editors want manual control over every piece of content. However, we had a business case scenario where the clones and the parent item were not controlled by the same department. The department that manages the tree section that has the clones are allowed to add in new items, but are not allowed to change the cloned items. So whenever a parent item is changed, those changes need to propagate automatically, meaning the notifications need to be auto accepted. To solve this, we added an event handler for item:saved event, which is triggered whenever an item is changed and saved. We need to create a class, such as ForceCloneAccept.cs, and add a include config to wire it up:


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="item:saved">
        <handler patch:after="handler[@type='Sitecore.Data.Fields.ItemEventHandler, Sitecore.Kernel']" type="HenrySchein.CMS.Business.EventHandlers.ForceCloneAccept, HenrySchein.CMS.Business" method="OnItemSaved" />
      </event>
    </events>
  </sitecore>
</configuration>

The code for ForceCloneAccept.cs is as below:


using System;
using System.Linq;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Events;
using Sitecore;
using Sitecore.SecurityModel;
using Sitecore.Data.Clones;
using Sitecore.Diagnostics;
using System.Collections.Generic;
using Sitecore.Links;
using Sitecore.Jobs;
using Sitecore.Data.Proxies;
using Sitecore.Workflows;
using Sitecore.Data;
using Sitecore.Configuration;
using System.Configuration;

namespace EventHandlers
{
    public class ForceCloneAccept
    {
        Database master = Factory.GetDatabase("master");

        public void OnItemSaved(object sender, EventArgs args)
        {
            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                var item = Event.ExtractParameter(args, 0) as Item;

                if (item != null && !item.HasClones)
                {
                    if (item.Parent == null || !item.Parent.HasClones) return;

                    var jobOptions = new JobOptions("AcceptChanges", string.Empty, Context.GetSiteName(), this, "AcceptChanges", new object[] { item.Parent });
                    var job = new Job(jobOptions);
                    jobOptions.InitialDelay = new TimeSpan(0, 0, 0, 1, 0);
                    JobManager.Start(job);
                }
                else
                {
                    var jobOptions = new JobOptions("AcceptChanges", string.Empty, Context.GetSiteName(), this, "AcceptChanges", new object[] { item });
                    var job = new Job(jobOptions);
                    jobOptions.InitialDelay = new TimeSpan(0, 0, 0, 1, 0);
                    JobManager.Start(job);
                }
            }
        }

        public void AcceptChanges(Item args)
        {
            using (new SecurityDisabler())
            {
                var item = args;
                var clones = item.GetClones(true);
                foreach (var clone in clones)
                {
                    var notifications = clone.Database.NotificationProvider.GetNotifications(clone);
                    foreach (var notification in notifications)
                    {
                        //if (notification.GetType() != typeof(FieldChangedNotification))
                        //{
                            clone.Database.NotificationProvider.RemoveNotification(notification.ID);
                            notification.Accept(clone);
                        //}

                    }

                    clone.Editing.BeginEdit();
                    try
                    {
                        clone.Fields["__Workflow"].Value = args.Fields["__Workflow"].Value;
                        clone.Fields["__Workflow state"].Value = args.Fields["__Workflow state"].Value;
                    }
                    finally
                    {
                        clone.Editing.EndEdit();

                    }

                }
            }
        }

        public static List GetItemClones(Item item, bool processChildren)
        {
            var realItem = ProxyManager.GetRealItem(item, false);

            var list = Globals.LinkDatabase.GetReferrers(realItem).Where(link => !(link.SourceFieldID != FieldIDs.Source)).Select(link => link.GetSourceItem()).Where(sourceItem => sourceItem != null).ToList();

            if (processChildren)
            {
                foreach (Item item4 in realItem.Children)
                {
                    list.AddRange(GetItemClones(item4, true));
                }
            }
            return list;
        }

    }
}

If you know how event handlers work, you already know that the way it is wired up, the method that will get triggered for this event is OnItemSaved; in this method we will find the context item (the item that got saved) and check for its clones, and get all the notifications each of the clone and accept them. Few things to note about this code:

    1. Notice that instead of applying the changes right in the OnItemSaved method, we are kicking off a job with a 1 second delay. Why? Because the notifications do not get added to the clones until after the item:saved event has been completed. So if you were to check the notification for that item at the time of this code running, there would be no notifications. There are other methods that you could also hook into, such as item:added, or item:created, but one of them has been deprecated, and the other only hits the first time. item:saved is something that gets called all the time, so this is more reliable. It will also get called when a new language version is added, or when new numbered version is added.

    2. There are several different types of notification that get created, based on the changes to the parent item. The notification types can be for a field change, for a child created, for an item that moved, or a version created. In AcceptChanges method, once you get all the notifications for the cloned item, you check and auto-accept only certain notifications.

    3. Clones by default do not inherit workflow. So, workflow fields are not copied over at all, even on the notification changes. The easiest (and somewhat crude) way to do this is to manually copy the fields over, so when a publish job runs, it will publish the cloned items also.

    4. Because we are only accepting notifications, and not really concerned with the changes are, we don’t have to write code to figure out the changes are. This simplifies things, and lets Sitecore handle applying the actual changes.

Similar to how we are force accepting the notifications, you can definitely write code to do anything your specific business scenario requires.

An edited version of this post also appears on 1000 lines of code

One thought on “Sitecore Clones explained in more detail: Part 2

Leave a comment