“Out of the box”, content authors are prompted via a content editor warning to take action on clones of a source item when a new subitem is added under the source item:
Content authors have a choice to either clone or not clone the new subitem, and such an action must be repeated on all clones of the source item — such might be a daunting task, especially when there are multiple clones for a given source item.
In a recent project, I had a requirement to automatically clone newly added subitems under clones of their parents, and remove any content editor warnings by programmatically accepting the Sitecore notifications driving these warnings.
Although I cannot show you that solution, I did wake up from a sound sleep early this morning with another solution to this problem — it was quite a dream — and this post captures that idea.
In a previous post, I built a utility object that gathers things, and decided to reuse this basic concept. Here, I define a contract for what constitutes a general gatherer:
using System.Collections.Generic;
namespace Sitecore.Sandbox.Utilities.Gatherers.Base
{
public interface IGatherer<T, U>
{
T Source { get; set; }
IEnumerable<U> Gather();
}
}
In this post, we will be gathering clones — these are Sitecore items:
using Sitecore.Data.Items;
namespace Sitecore.Sandbox.Utilities.Gatherers.Base
{
public interface IItemsGatherer : IGatherer<Item, Item>
{
}
}
The following gatherer grabs all clones for a given item as they are cataloged in an instance of the LinkDatabase — in this solution we are using Globals.LinkDatabase as the default instance:
using System.Collections.Generic;
using System.Linq;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Sandbox.Utilities.Gatherers.Base;
namespace Sitecore.Sandbox.Utilities.Gatherers
{
public class ItemClonesGatherer : IItemsGatherer
{
private LinkDatabase LinkDatabase { get; set; }
private Item _Source;
public Item Source
{
get
{
return _Source;
}
set
{
Assert.ArgumentNotNull(value, "Source");
_Source = value;
}
}
private ItemClonesGatherer()
: this(GetDefaultLinkDatabase())
{
}
private ItemClonesGatherer(LinkDatabase linkDatabase)
{
SetLinkDatabase(linkDatabase);
}
private ItemClonesGatherer(LinkDatabase linkDatabase, Item source)
{
SetLinkDatabase(linkDatabase);
SetSource(source);
}
private void SetLinkDatabase(LinkDatabase linkDatabase)
{
Assert.ArgumentNotNull(linkDatabase, "linkDatabase");
LinkDatabase = linkDatabase;
}
private void SetSource(Item source)
{
Source = source;
}
public IEnumerable<Item> Gather()
{
return (from itemLink in GetReferrers()
where IsClonedItem(itemLink)
select itemLink.GetSourceItem()).ToList();
}
private IEnumerable<ItemLink> GetReferrers()
{
Assert.ArgumentNotNull(Source, "Source");
return LinkDatabase.GetReferrers(Source);
}
private static bool IsClonedItem(ItemLink itemLink)
{
return IsSourceField(itemLink)
&& !IsSourceItemNull(itemLink);
}
private static bool IsSourceField(ItemLink itemLink)
{
Assert.ArgumentNotNull(itemLink, "itemLink");
return itemLink.SourceFieldID == FieldIDs.Source;
}
private static bool IsSourceItemNull(ItemLink itemLink)
{
Assert.ArgumentNotNull(itemLink, "itemLink");
return itemLink.GetSourceItem() == null;
}
private static LinkDatabase GetDefaultLinkDatabase()
{
return Globals.LinkDatabase;
}
public static IItemsGatherer CreateNewItemClonesGatherer()
{
return new ItemClonesGatherer();
}
public static IItemsGatherer CreateNewItemClonesGatherer(LinkDatabase linkDatabase)
{
return new ItemClonesGatherer(linkDatabase);
}
public static IItemsGatherer CreateNewItemClonesGatherer(LinkDatabase linkDatabase, Item source)
{
return new ItemClonesGatherer(linkDatabase, source);
}
}
}
Next, we need a way to remove the content editor warning I alluded to above. The way to do this is to accept the notification that is triggered on the source item’s clones.
I decided to use the decorator pattern to accomplish this:
using Sitecore.Data.Clones;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
namespace Sitecore.Sandbox.Data.Clones
{
public class AcceptAsIsNotification : Notification
{
private Notification InnerNotification { get; set; }
private AcceptAsIsNotification(Notification innerNotification)
{
SetInnerNotification(innerNotification);
SetProperties();
}
private void SetInnerNotification(Notification innerNotification)
{
Assert.ArgumentNotNull(innerNotification, "innerNotification");
InnerNotification = innerNotification;
}
private void SetProperties()
{
ID = InnerNotification.ID;
Processed = InnerNotification.Processed;
Uri = InnerNotification.Uri;
}
public override void Accept(Item item)
{
base.Accept(item);
}
public override Notification Clone()
{
return InnerNotification.Clone();
}
public static Notification CreateNewAcceptAsIsNotification(Notification innerNotification)
{
return new AcceptAsIsNotification(innerNotification);
}
}
}
An instance of the above AcceptAsIsNotification class would wrap an instance of a notification, and accept the wrapped notification without any other action.
Notifications we care about in this solution are instances of Sitecore.Data.Clones.ChildCreatedNotification — in Sitecore.Kernel — for a given clone parent, and this notification clones a subitem during acceptance, a behavior we do not want to leverage since we have already cloned the subitem.
I then added an item:added event handler to use an instance of our gatherer defined above to get all clones of the newly added subitem’s parent; clone the subitem under these clones; and accept all instances of Sitecore.Data.Clones.ChildCreatedNotification on the parent item’s clones:
using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Data;
using Sitecore.Data.Clones;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.Sandbox.Data.Clones;
using Sitecore.Sandbox.Utilities.Gatherers;
using Sitecore.Sandbox.Utilities.Gatherers.Base;
namespace Sitecore.Sandbox.Events.Handlers
{
public class AutomagicallyCloneChildItemEventHandler
{
private static readonly IItemsGatherer ClonesGatherer = ItemClonesGatherer.CreateNewItemClonesGatherer();
public void OnItemAdded(object sender, EventArgs args)
{
CloneItemIfApplicable(GetItem(args));
}
private static void CloneItemIfApplicable(Item item)
{
if (item == null)
{
return;
}
ChildCreatedNotification childCreatedNotification = CreateNewChildCreatedNotification();
childCreatedNotification.ChildId = item.ID;
IEnumerable<Item> clones = GetClones(item.Parent);
foreach (Item clone in clones)
{
Item clonedChild = item.CloneTo(clone);
RemoveChildCreatedNotifications(Context.ContentDatabase, clone);
}
}
private static void RemoveChildCreatedNotifications(Database database, Item clone)
{
Assert.ArgumentNotNull(database, "database");
Assert.ArgumentNotNull(clone, "clone");
foreach (Notification notification in GetChildCreatedNotifications(database, clone))
{
AcceptNotificationAsIs(notification, clone);
}
}
private static void AcceptNotificationAsIs(Notification notification, Item item)
{
Assert.ArgumentNotNull(notification, "notification");
Assert.ArgumentNotNull(item, "item");
Notification acceptAsIsNotification = AcceptAsIsNotification.CreateNewAcceptAsIsNotification(notification);
acceptAsIsNotification.Accept(item);
}
private static IEnumerable<Notification> GetChildCreatedNotifications(Database database, Item clone)
{
Assert.ArgumentNotNull(database, "database");
Assert.ArgumentNotNull(clone, "clone");
return GetNotifications(database, clone).Where(notification => notification.GetType() == typeof(ChildCreatedNotification)).ToList();
}
private static IEnumerable<Notification> GetNotifications(Database database, Item clone)
{
Assert.ArgumentNotNull(database, "database");
Assert.ArgumentNotNull(clone, "clone");
return database.NotificationProvider.GetNotifications(clone);
}
private static IEnumerable<Item> GetClones(Item item)
{
ClonesGatherer.Source = item;
return ClonesGatherer.Gather();
}
private static Item GetItem(EventArgs args)
{
return Event.ExtractParameter(args, 0) as Item;
}
private static ChildCreatedNotification CreateNewChildCreatedNotification()
{
return new ChildCreatedNotification();
}
}
}
I then plugged in the above in a patch include configuration file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<sitecore>
<events>
<event name="item:added">
<handler type="Sitecore.Sandbox.Events.Handlers.AutomagicallyCloneChildItemEventHandler, Sitecore.Sandbox" method="OnItemAdded"/>
</event>
</events>
</sitecore>
</configuration>
Let’s try this out, and see how we did.
I created a new child item under my source item:
As you can see, clones were added under all clones of the source item.
Plus, the notification about a new child item being added under the source item is not present:
This notification was automatically accepted in code via an instance of AcceptAsIsNotification within the item:added event handler defined above.
If you can think of any other interesting ways to leverage Sitecore’s item cloning capabilities, please leave a comment.







[…] Automagically Clone New Child Items to Clones of Parent Items In the Sitecore CMS […]