“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 […]