In my previous post, I gave a solution that removes names of Bucket Folder Items from URLs in Sitecore, and also resolves those same URLs when they are called up in a browser.
However, that solution wasn’t complete — code is needed to ensure Bucketed Item names are unique given the solution assumes Bucketed Item names are unique (code in that solution uses the Sitecore.ContentSearch API to find an Item by name within an Item Bucket, and if there are two or more Items with the same name, only one will be returned — this will prevent the resolution of URLs for those other Bucketed page Items).
I decided to take up the challenge on continuing that solution over this previous weekend, and share what I built.
One thing to note: the solution that follows is not a complete solution for preventing duplicate Bucketed Item names as such a post could go on for ages — actually, I probably would still be writing the code for it. I leave the rest for you guys to do as a homework assignment. 😉
I first defined the following interface to centralize common methods I have been using in my Sitecore Item Buckets code (I will be reusing this same interface and its implementation in future blog posts):
using Sitecore.Data.Items; namespace Sitecore.Sandbox.Buckets.Util.Methods { public interface IItemBucketsFeatureMethods { bool IsItemBucketFolder(Item item); bool IsItemContainedWithinBucket(Item item); bool IsItemBucketable(Item item); Item GetItemBucket(Item item); bool IsItemBucket(Item item); bool HasBucketedItemWithName(Item itemBucket, string itemName); } }
The following class implements the interface above:
using Sitecore.Buckets.Extensions; using Sitecore.Buckets.Managers; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Sandbox.Buckets.Providers.Items; namespace Sitecore.Sandbox.Buckets.Util.Methods { public class ItemBucketsFeatureMethods : IItemBucketsFeatureMethods { private IFindBucketedItemProvider FindBucketedItemProvider { get; set; } public virtual bool IsItemBucketFolder(Item item) { Assert.ArgumentNotNull(item, "item"); return item.IsABucketFolder(); } public virtual bool IsItemContainedWithinBucket(Item item) { Assert.ArgumentNotNull(item, "item"); return BucketManager.IsItemContainedWithinBucket(item); } public virtual bool IsItemBucketable(Item item) { Assert.ArgumentNotNull(item, "item"); return item.IsItemBucketable(); } public virtual Item GetItemBucket(Item item) { Assert.ArgumentNotNull(item, "item"); Item ancestor = item.GetParentBucketItemOrParent(); if (!IsItemBucket(ancestor)) { return null; } return ancestor; } public virtual bool IsItemBucket(Item item) { Assert.ArgumentNotNull(item, "item"); return item.IsABucket(); } public virtual bool HasBucketedItemWithName(Item itemBucket, string itemName) { EnsureFindBucketedItemProvider(); Assert.ArgumentNotNull(itemBucket, "itemBucket"); Assert.ArgumentNotNullOrEmpty(itemName, "itemName"); Item item = FindBucketedItemProvider.FindBucketedItemByName(itemBucket, itemName); if(item == null) { return false; } return true; } protected virtual void EnsureFindBucketedItemProvider() { Assert.IsNotNull(FindBucketedItemProvider, "FindBucketedItemProvider must be set in configuration!"); } } }
I’m not going to go into details of the code above as it is self-explanatory.
However, I do want to call out that I am reusing the IFindBucketedItemProvider code from my previous post. I advise having a look at that code before moving forward.
I then defined the following class whose Process() method will serve as a processor of 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 HandleDuplicateBucketedItemName { protected string ItemIdsParameterName { get; set; } protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; } protected string RenameItemMessage { 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) { return; } Item item = GetItem(database, itemIds.First()); if (item == null) { return; } Item itemBucket = GetItemBucket(targetItem); if (itemBucket == null || !HasBucketedItemWithName(itemBucket, item.Name)) { return; } PromptRenameItem(args, item); } 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 Item GetItemBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); if(!ItemBucketsFeatureMethods.IsItemBucket(item)) { return ItemBucketsFeatureMethods.GetItemBucket(item); } return item; } protected virtual bool HasBucketedItemWithName(Item itemBucket, string bucketedItemName) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(itemBucket, "itemBucket"); Assert.ArgumentNotNullOrEmpty(bucketedItemName, "bucketedItemName"); return ItemBucketsFeatureMethods.HasBucketedItemWithName(itemBucket, bucketedItemName); } protected virtual void PromptRenameItem(ClientPipelineArgs args, Item item) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(item, "item"); if (args.IsPostBack) { if(!args.HasResult) { args.AbortPipeline(); return; } GetProposedValidItemName(args.Result); RenameItem(item, GetProposedValidItemName(args.Result)); ClearResult(args); } else { SheerResponse.Input(GetRenameItemMessage(), string.Empty); args.WaitForPostBack(); } } protected virtual void ClearResult(ClientPipelineArgs args) { args.Result = string.Empty; args.IsPostBack = false; } protected virtual string GetProposedValidItemName(string itemName) { Assert.ArgumentNotNull(itemName, "itemName"); return ItemUtil.ProposeValidItemName(itemName); } protected virtual void RenameItem(Item item, string name) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNullOrEmpty(name, "name"); using (new EditContext(item, false, true)) { item.Name = name; } } protected virtual string GetRenameItemMessage() { Assert.ArgumentNotNull(RenameItemMessage, "RenameItemMessage must be set in configuration!"); return RenameItemMessage; } protected virtual void EnsureItemBucketsFeatureMethods() { Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!"); } } }
The Process() method above basically gets the Item IDs of the Items that are moving from the Parameters collection on the arguments object passed to it — these are stored under different keys in the Parameters collection by the <uiDragItemTo> and <uiMoveItems> pipelines, so I’m letting the Sitecore Configuration Factory pass in the name of that parameter for each (see the patch configuration file below).
If more than one Item ID is returned, the code exits.
The Process() method then gets the target Item’s ID; the Database instance; the target Item; and the instance of the Item we are moving. If any of these are null, it exits.
The code then attempts to get the Item Bucket for the target Item — this is done via the GetItemBucket() method which just delegates to the GetItemBucket() method on the IItemBucketsFeatureMethods instance, or returns the passed Item if it is an Item Bucket. If the returned Item is null, the code exits.
The Process() method then calls the HasBucketedItemWithName() — this method just makes a call to a method with the same name on the ItemBucketsFeatureMethods instance — to see if there is another Bucketed Item within the Item Bucket with the same name. If one is not found, we prompt the user for a new Item name, and rename the Item if one is supplied. If no Item name is supplied, we abort the pipeline completely to prevent the user from moving forward.
I do want to highlight one more thing. the RenameItem() method uses a Sitecore.Data.Items.EditContext instance when changing the Item name. I decided to use an instance of this class to have this change be silent and not log any update statistics as this was causing the Item to become un-bucketable after it was moved (no clue as to why this would happen).
I then plugged-in all of the code above via the following patch configuration file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <buckets> <methods> <itemBucketsFeatureMethods type="Sitecore.Sandbox.Buckets.Util.Methods.ItemBucketsFeatureMethods, Sitecore.Sandbox"> <FindBucketedItemProvider ref="buckets/providers/items/findBucketedItemProvider" /> </itemBucketsFeatureMethods> </methods> </buckets> <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.HandleDuplicateBucketedItemName, Sitecore.Sandbox" mode="on"> <ItemIdsParameterName>id</ItemIdsParameterName> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <RenameItemMessage>Duplicate bucketed Item names are not allowed. Please enter in a new name for the item:</RenameItemMessage> </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.HandleDuplicateBucketedItemName, Sitecore.Sandbox" mode="on"> <ItemIdsParameterName>items</ItemIdsParameterName> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <RenameItemMessage>Duplicate bucketed Item names are not allowed. Please enter in a new name for the item:</RenameItemMessage> </processor> </uiMoveItems> </processors> </sitecore> </configuration>
Let’s take this for a spin.
Let’s test this out by dragging an Item to an Item Bucket that has a bucketed Item with the same name:
Of course, I do:
I gave it a unique name:
As you can see, the Item was renamed and then moved:
One thing to note: the Item will not be moved if the user clicks the ‘Cancel’ button in the dialog that asks for a new name.
Moreover, the above code only works when moving Items in the Sitecore Client. These pipeline processors will not run when moving Items via Sitecore API code (you’ll have to tap into one of the “move” related events, instead).
I’m going to omit sharing my testing of the “Move To” piece of the code above as it is using the same code. Trust me, it works. 😉
If you have any thoughts on this, please drop a comment.
[…] Prevent Duplicate Names of Bucketed Sitecore Items […]