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 The Runtime Report.

Advertisements

Sitecore Clones explained in more detail: Part 1

Sitecore clones were introduced first in version 6.3, as a way to centralize content items. It is a very useful way to place your items throughout the content tree, and then have the content controlled from one central location in the content tree. This is, at least the basic usage. There are other granular way to fine-tune this usage, where each of the cloned items have more control over themselves.

I’m not going to go very far into how to use clones, what they are and how they function – it is pretty well documented in the Sitecore documentation, and various Sitecore expert blogs, such as John West’s Blog. What I will detail here are some of the complications that arise when using clones, and how I tackled them.

1. Basic Usage of clones

As stated before, the basic usage of clones is to ‘clone‘ or create a copy of an item in the content tree to another different location within the same content tree. One parent item can have multiple cloned items. What this allows you to do is control the content once, in the parent item, and all the child cloned items will also get the changes (there is more to this, in the following points).

To simply create a clone, first choose the item you want to clone (News Item for Cloning), and then go to configure tab in the ribbon, and click ‘clone

Add a clone of an item by going to Configure->Clone

Add a clone of an item by going to Configure->Clone

When you click ‘clone‘, and a popup will show with the content tree – you then have to choose where the cloned item will be created. Once you hit ‘clone‘ button in the popup, the cloned item will be created. (I chose /home/news):

Cloned item created (shown in grey)

Cloned item created (shown in grey)

Notice that the cloned item is shown in grey. This is one way to tell whether an item is a clone.

Another way to tell if an item is a clone is to see the ‘Quick Info‘ tab – A cloned item will have a value for ‘Created From‘, which points to the original parent item.

From within code, you can check the values of item.IsClone and/or item.SourceUri

Item 'Quick Info' section

Item ‘Quick Info’ section

2. Changes to the cloned item, the parent item, and new items created under the parent.

Once a clone is created, you can still make changes to the clone. Since now there is a link between the parent item and the cloned item, it is somewhat assumed that if I change any fields in the parent item, the cloned item must change also. This is partially correct, but there are a few things that also happens:

    a. If the original value of the field and the value of the field in the cloned item is the same, it applies the changes immediately. When you click on the cloned item, you can see the change immediately.

    You can always click on a cloned item and see which values are the original value (from the parent) and which values have been changed in the cloned item itself – it will state [original value] to designate all fields that are exactly the same as the parent item.

    Cloned item field values

    Cloned item field values

    b. If the value in the field has been changed in the cloned item, and it does not match the parent item, and the value is changed in the parent item, it is not automatically applied (as of version 6.6). In this scenario when anything is changed in the parent item, and you want to see that change in the cloned item, there is an approval process. Sitecore creates a notification, which the user must accept in order to apply the change to the cloned item. For example, changing the title in the News Item For Cloning item creates a notification in the cloned item, which shows up once you click on the cloned item:

    Cloned item field change notification

    Cloned item field change notification

    At this point, you can review the original item to see the changes, accept the changes (which will overwrite the values that has been changed in the cloned item) or reject the change, which effectively keep the values that the cloned item has. If the changes are rejected, the field will not say [original value] anymore for all the fields that do not match the value in the field of the parent item. Subsequent changes to the same fields in the parent item will recreate these notifications.

    This also applies to new sub-items created under the parent item – in this case a different notification appears:

    Cloned item new item created nofication

    Cloned item new item created nofication

    As same with the other notification, the user has the ability to accept or reject the change. In this case if the change is rejected, the sub-item never appears under the cloned item. The entire tree must be re-cloned in order for the new sub-item to get cloned.

For an effective complete control of all cloned items, we have to write code to handle item:saved events for any items that have been cloned, and automatically accept the changes to all cloned items. I’m hoping in the future versions of Sitecore will give us a way to configure (when cloning) to designate whether a change must be forced or not, so there can much more granular content over each piece of item.

For examples of how to write event handlers for this, see here: http://adeneys.wordpress.com/2010/11/02/sharing-content-with-item-clones/

Note: I’ve also written code to do this: if you need some code samples, I will explain this in more detail in Part 2 very shortly. If you need the samples right away, leave a comment and I will get back to you..

3. Managing cloned items from workflow and publishing perspective

Cloned items are nothing but regular Sitecore items, with a link between the cloned item and the parent item. Once published, though, cloned item becomes real items, and the relationship between the items no longer exist. This is not an issue, because on the web database, this relationship is not needed. But remember that all changes to cloned items must be individually published, and just publishing the parent item does not publish the cloned items.

In respect to this, the cloned items can have their own workflow, even if field value changes are forced (via an event handler). So in fact, a parent item can be changed, approved and published, while the cloned items are still in a draft mode. To solve this scenario, cloned items can be made to follow the same approval process by copying the workflow properties during the event handler process which auto-accepts the notifications (more in Part 2).

4. Appearance of cloned items

As of version 6.6, the cloned items are always in grey. There is no way to configure a different style for this, other than writing custom code for it. You can add your processor to the uiCloneItems pipeline processor, and change style of the item as such:

clonedItem.Fields[Sitecore.FieldIDs.Style].Value = "color:#666666; font-style: italic";

More details about how to write the event handlers for auto-accept and their usages coming in Part 2.

An edited version of this post also appears on The Runtime Report.