If you’ve been reading my posts lately, you have probably noticed I’ve been having a ton of fun with Sitecore Item Buckets. I absolutely love this feature in Sitecore.
As a matter of, I love Item Buckets so much, I’m doing a presentation on them just next week at the Greater Cincinnati Sitecore Users Group. If you’re in the neighborhood, stop by — even if it’s only to say “Hello”.
Anyways, back to the post.
I noticed the following grey box on the Items Buckets page on the Sitecore Documentation site:
This got me thinking: why can’t we build something in Sitecore to prevent this from happening in the first place?
In other words, why can’t we just say “sorry, you can’t move an unbucketable Item into a bucket folder”?
So, that’s what I decided to do — build a solution that prevents this from happening. Let’s have a look at what I came up with.
I first created the following interface for classes whose instances will move a Sitecore item to a destination Item:
using Sitecore.Data.Items; namespace Sitecore.Sandbox.Utilities.Items.Movers { public interface IItemMover { bool DisableSecurity { get; set; } bool ShouldBeMoved(Item item, Item destination); void Move(Item item, Item destination); } }
I then defined the following class which implements the interface above:
using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.SecurityModel; namespace Sitecore.Sandbox.Utilities.Items.Movers { public class ItemMover : IItemMover { public bool DisableSecurity { get; set; } public virtual bool ShouldBeMoved(Item item, Item destination) { return item != null && destination != null; } public virtual void Move(Item item, Item destination) { if (!ShouldBeMoved(item, destination)) { return; } if(DisableSecurity) { MoveWithoutSecurity(item, destination); return; } MoveWithoutSecurity(item, destination); } protected virtual void MoveWithSecurity(Item item, Item destination) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(destination, "destination"); item.MoveTo(destination); } protected virtual void MoveWithoutSecurity(Item item, Item destination) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(destination, "destination"); using (new SecurityDisabler()) { item.MoveTo(destination); } } } }
Callers of the above code can move an Item from one location to another with/without Sitecore security in place.
The ShouldBeMoved() above is basically a stub that will allow subclasses to define their own rules on whether an Item should be moved, depending on whatever rules must be met.
I then defined the following subclass of the class above which has its own rules on whether an Item should be moved (i.e. move this unbucketable Item out of a bucket folder if makes its way there):
using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Sandbox.Buckets.Util.Methods; using Sitecore.Sandbox.Utilities.Items.Movers; namespace Sitecore.Sandbox.Buckets.Util.Items.Movers { public class UnbucketableItemMover : ItemMover { protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; } public override bool ShouldBeMoved(Item item, Item destination) { return base.ShouldBeMoved(item, destination) && !IsItemBucketable(item) && IsItemInBucket(item) && !IsItemBucketFolder(item) && IsItemBucketFolder(item.Parent) && IsItemBucket(destination); } protected virtual bool IsItemBucketable(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucketable(item); } protected virtual bool IsItemInBucket(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item); } protected virtual bool IsItemBucketFolder(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucketFolder(item); } protected virtual bool IsItemBucket(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucket(item); } protected virtual void EnsureItemBucketFeatureMethods() { Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!"); } } }
I’m injecting an instance of an IItemBucketsFeatureMethods class — this interface and its implementation are defined in my previous post; go have a look if you have not read that post so you can be familiar with the IItemBucketsFeatureMethods code — via the Sitecore Configuration Factory which contains common methods I am using in my Item Bucket code solutions (I will be using this in future posts).
The ShouldBeMoved() method basically says that an Item can only be moved when the Item and destination passed aren’t null — this is defined on the base class’ ShouldBeMoved() method; the Item isn’t bucketable; the Item is already in an Item Bucket; the Item isn’t a Bucket Folder; the Item’s parent Item is a Bucket Folder; and the destination is an Item Bucket.
Yes, the above sounds a bit confusing though there is a reason for it — I want to take an unbucketable Item out of a Bucket Folder and move it directly under the Item Bucket instead.
I then created the following class which contains methods that will serve as “item:moved” event handlers:
using System; using System.Collections.Generic; using Sitecore.Data; using Sitecore.Data.Events; using Sitecore.Data.Items; using Sitecore.Events; using Sitecore.Sandbox.Buckets.Util.Methods; using Sitecore.Sandbox.Utilities.Items.Movers; namespace Sitecore.Sandbox.Buckets.Events.Items.Move { public class RemoveFromBucketFolderIfNotBucketableHandler { protected static SynchronizedCollection<ID> ItemsBeingProcessed { get; set; } protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; } protected IItemMover UnbucketableItemMover { get; set; } protected void OnItemMoved(object sender, EventArgs args) { Item item = GetItem(args); RemoveFromBucketFolderIfNotBucketable(item); } static RemoveFromBucketFolderIfNotBucketableHandler() { ItemsBeingProcessed = new SynchronizedCollection<ID>(); } protected virtual Item GetItem(EventArgs args) { if (args == null) { return null; } return Event.ExtractParameter(args, 0) as Item; } protected void OnItemMovedRemote(object sender, EventArgs args) { Item item = GetItemRemote(args); RemoveFromBucketFolderIfNotBucketable(item); } protected virtual Item GetItemRemote(EventArgs args) { ItemMovedRemoteEventArgs remoteArgs = args as ItemMovedRemoteEventArgs; if (remoteArgs == null) { return null; } return remoteArgs.Item; } protected virtual void RemoveFromBucketFolderIfNotBucketable(Item item) { if(item == null) { return; } Item itemBucket = GetItemBucket(item); if (itemBucket == null) { return; } if(!ShouldBeMoved(item, itemBucket)) { return; } AddItemBeingProcessed(item); MoveUnderItemBucket(item, itemBucket); RemoveItemBeingProcessed(item); } protected virtual bool IsItemBeingProcessed(Item item) { if (item == null) { return false; } return ItemsBeingProcessed.Contains(item.ID); } protected virtual void AddItemBeingProcessed(Item item) { if (item == null) { return; } ItemsBeingProcessed.Add(item.ID); } protected virtual void RemoveItemBeingProcessed(Item item) { if (item == null) { return; } ItemsBeingProcessed.Remove(item.ID); } protected virtual Item GetItemBucket(Item item) { if(ItemBucketsFeatureMethods == null || item == null) { return null; } return ItemBucketsFeatureMethods.GetItemBucket(item); } protected virtual bool ShouldBeMoved(Item item, Item itemBucket) { if(UnbucketableItemMover == null) { return false; } return UnbucketableItemMover.ShouldBeMoved(item, itemBucket); } protected virtual void MoveUnderItemBucket(Item item, Item itemBucket) { if (UnbucketableItemMover == null) { return; } UnbucketableItemMover.Move(item, itemBucket); } } }
Both the OnItemMoved() and OnItemMovedRemote() methods extract the moved Item from their specific methods for getting the Item from the EventArgs instance. If that Item is null, the code exits.
Both methods pass their Item instance to the RemoveFromBucketFolderIfNotBucketable() method which ultimately attempts to grab an Item Bucket ancestor of the Item via the GetItemBucket() method. If no Item Bucket instance is returned, the code exits.
If an Item Bucket was found, the RemoveFromBucketFolderIfNotBucketable() method ascertains whether the Item should be moved — it makes a call to the ShouldBeMoved() method which just delegates to the IItemMover instance injected in via the Sitecore Configuration Factory (have a look at the patch configuration file below).
If the Item should not be moved, then the code exits.
If it should be moved, it is then passed to the MoveUnderItemBucket() method which delegates to the Move() method on the IItemMover instance.
You might be asking “Mike, what’s up with the ItemsBeingProcessed SynchronizedCollection of Item IDs?” I’m using this collection to maintain which Items are currently being moved so we don’t have racing conditions in code.
You might be thinking “Great, we’re done!”
We can’t just move an Item from one destination to another, especially when the user selected the first destination. We should let the user know that we will need to move the Item as it is unbucketable. Let’s not be evil.
I created the following class whose Process() method will serve as a custom processor for both the <uiDragItemTo> and <uiMoveItems> pipelines of the Sitecore Client:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Text; using Sitecore.Web.UI.Sheer; using Sitecore.Sandbox.Buckets.Util.Methods; namespace Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems { public class ConfirmMoveOfUnbucketableItem { protected string ItemIdsParameterName { get; set; } protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; } protected string ConfirmationMessageFormat { get; set; } public void Process(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); IEnumerable<string> itemIds = GetItemIds(args); if (itemIds == null || !itemIds.Any() || itemIds.Count() > 1) { return; } string targetId = GetTargetId(args); if (string.IsNullOrWhiteSpace(targetId)) { return; } Database database = GetDatabase(args); if (database == null) { return; } Item targetItem = GetItem(database, targetId); if (targetItem == null || !IsItemBucketOrIsItemInBucket(targetItem)) { return; } Item item = GetItem(database, itemIds.First()); if (item == null || IsItemBucketable(item)) { return; } Item itemBucket = GetItemBucket(targetItem); if (itemBucket == null) { return; } SetTokenValues(args, item, itemBucket); ConfirmMove(args); } protected virtual IEnumerable<string> GetItemIds(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); string itemIdsParameterName = GetItemIdsParameterName(args); Assert.IsNotNullOrEmpty(itemIdsParameterName, "GetItemIdParameterName() cannot return null or the empty string!"); return new ListString(itemIdsParameterName, '|'); } protected virtual string GetItemIdsParameterName(ClientPipelineArgs args) { Assert.IsNotNullOrEmpty(ItemIdsParameterName, "ItemIdParameterName must be set in configuration!"); Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); return args.Parameters[ItemIdsParameterName]; } protected virtual string GetTargetId(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); return args.Parameters["target"]; } protected virtual Database GetDatabase(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); return Factory.GetDatabase(args.Parameters["database"]); } protected virtual Item GetItem(Database database, string itemId) { Assert.ArgumentNotNull(database, "database"); Assert.ArgumentNotNullOrEmpty(itemId, "itemId"); try { return database.GetItem(itemId); } catch(Exception ex) { Log.Error(ToString(), ex, this); } return null; } protected virtual bool IsItemBucketOrIsItemInBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return IsItemBucket(item) || IsItemInBucket(item); } protected virtual bool IsItemBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucket(item); } protected virtual bool IsItemInBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item); } protected virtual bool IsItemBucketable(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucketable(item); } protected virtual Item GetItemBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); if(!ItemBucketsFeatureMethods.IsItemBucket(item)) { return ItemBucketsFeatureMethods.GetItemBucket(item); } return item; } protected virtual void SetTokenValues(ClientPipelineArgs args, Item item, Item itemBucket) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(itemBucket, "itemBucket"); args.Parameters["$itemName"] = item.Name; args.Parameters["$itemBucketName"] = itemBucket.Name; args.Parameters["$itemBucketFullPath"] = itemBucket.Paths.FullPath; } protected virtual void ConfirmMove(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); if(args.IsPostBack) { if (args.Result == "yes") { ClearResult(args); return; } if (args.Result == "no") { args.AbortPipeline(); return; } } else { SheerResponse.Confirm(GetConfirmationMessage(args)); args.WaitForPostBack(); } } protected virtual void ClearResult(ClientPipelineArgs args) { args.Result = string.Empty; args.IsPostBack = false; } protected virtual string GetConfirmationMessage(ClientPipelineArgs args) { Assert.IsNotNullOrEmpty(ConfirmationMessageFormat, "ConfirmationMessageFormat must be set in configuration!"); Assert.ArgumentNotNull(args, "args"); return ReplaceTokens(ConfirmationMessageFormat, args); } protected virtual string ReplaceTokens(string messageFormat, ClientPipelineArgs args) { Assert.ArgumentNotNullOrEmpty(messageFormat, "messageFormat"); Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); string message = messageFormat; message = message.Replace("$itemName", args.Parameters["$itemName"]); message = message.Replace("$itemBucketName", args.Parameters["$itemBucketName"]); message = message.Replace("$itemBucketFullPath", args.Parameters["$itemBucketFullPath"]); return message; } protected virtual void EnsureItemBucketsFeatureMethods() { Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!"); } } }
The Process() method above gets the Item ID for the Item that is being moved; the Item ID for the destination Item — this is referred to as the “target” in the code above; gets the Database instance of where we are moving this Item; the instances of both the Item and target Item; determines if the Target Item is a Bucket Folder or an Item Bucket; determines if the Item is unbucketable; and then the Item Bucket (this could be the target Item).
If any of of the instances above are null, the code exits.
If the Item is unbucketable but is being moved to a Bucket Folder or Item Bucket, we prompt the user with a confirmation dialog asking him/her whether he/she should like to continue given that the Item will be moved directly under the Item Bucket.
If the user clicks the ‘Ok’ button, the Item is moved. Otherwise, the pipeline is aborted and the Item will not be moved at all.
I then pieced all of the above together via the following patch configuration file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <buckets> <movers> <items> <unbucketableItemMover type="Sitecore.Sandbox.Buckets.Util.Items.Movers.UnbucketableItemMover, Sitecore.Sandbox" singleInstance="true"> <DisableSecurity>true</DisableSecurity> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> </unbucketableItemMover> </items> </movers> </buckets> <events> <event name="item:moved"> <handler type="Sitecore.Sandbox.Buckets.Events.Items.Move.RemoveFromBucketFolderIfNotBucketableHandler, Sitecore.Sandbox" method="OnItemMoved"> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <UnbucketableItemMover ref="buckets/movers/items/unbucketableItemMover" /> </handler> </event> <event name="item:moved:remote"> <handler type="Sitecore.Sandbox.Buckets.Events.Items.Move.RemoveFromBucketFolderIfNotBucketableHandler, Sitecore.Sandbox" method="OnItemMovedRemote"> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <UnbucketableItemMover ref="buckets/movers/items/unbucketableItemMover" /> </handler> </event> </events> <processors> <uiDragItemTo> <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemDrag, Sitecore.Buckets' and @method='Execute']" type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.ConfirmMoveOfUnbucketableItem, Sitecore.Sandbox" mode="on"> <ItemIdsParameterName>id</ItemIdsParameterName> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <ConfirmationMessageFormat>You are attempting to move the non-bucketable Item: $itemName to a bucket folder. If you continue, it will be moved directly under the Item Bucket: $itemBucketName ($itemBucketFullPath). Do you wish to continue?</ConfirmationMessageFormat> </processor> </uiDragItemTo> <uiMoveItems> <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemMove, Sitecore.Buckets' and @method='Execute']" type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.ConfirmMoveOfUnbucketableItem, Sitecore.Sandbox" mode="on"> <ItemIdsParameterName>items</ItemIdsParameterName> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <ConfirmationMessageFormat>You are attempting to move the non-bucketable Item: $itemName to a bucket folder. If you continue, it will be moved directly under the Item Bucket: $itemBucketName ($itemBucketFullPath). Do you wish to continue?</ConfirmationMessageFormat> </processor> </uiMoveItems> </processors> </sitecore> </configuration>
Let’s see how we did.
Let’s move this unbucketable Item to an Item Bucket:
Yes, I’m sure I’m sure:
I was then prompted with the confirmation dialog as expected:
As you can see, the Item was placed directly under the Item Bucket:
If you have any thoughts on this, please drop a comment.