Home » 2016 » June

Monthly Archives: June 2016

Display How Many Bucketed Items Live Within a Sitecore Item Bucket Using a Custom DataView

Over the past few weeks — if you haven’t noticed — I’ve been having a blast experimenting with Sitecore Item Buckets. It seems new ideas on what to build for it keep flooding my thoughts everyday. ๐Ÿ˜€

However, the other day an old idea that I wanted to solve a while back bubbled its way up into the forefront of my consciousness: displaying the count of Bucketed Items which live within each Item Bucket in the Content Tree.

I’m sure someone has built something to do this before though I didn’t really do any research on it as I was up for the challenge.

darth-vader-didnt-read

In all honesty, I enjoy spending my nights after work and on weekends building things in Sitecore — even if someone has built something like it before — as it’s a great way to not only discover new treasures hidden within the Sitecore assemblies, but also improve my programming skills — the saying “you lose it if you don’t use it” applies here.

nerds

You might be asking “Mike, we don’t store that many Sitecore Items in our Item Buckets; I can just go count them all by hand”.

Well, if that’s the case for you then you might want to reconsider why you are using the Item Buckets feature.

However, in theory, thousands if not millions of Items can live within an Item Bucket in Sitecore. If counting by hand is your thing — or even writing some sort of “script” (I’m not referring to PowerShell scripts that you would write using Sitecore PowerShell Extensions (SPE) — I definitely recommend harnessing all of the power this module has to offer — but instead to standalone ASP.NET Web Forms which some people erroneously call “scripts”) to generate some kind of report, then by all means go for it.

counting

That’s just not how I roll.

aint-no-time-for-that

So how are we going to display these counts to the user? We are ultimately going to create a custom Sitecore DataView.

If you aren’t familiar with DataViews in Sitecore, they basically allow you to change how Items are displayed in the Sitecore Content Tree.

I’m not going to go too much into details of how these work. I recommend having a read of the following posts by two fellow Sitecore MVPs for more information and to see other examples:

I do want to warn you: there is a lot of code in this post.

arghhhhh

You might want to go get a snack for this as it might take a while to get through all the code that I am showing here. Don’t worry, I’ll wait for you to get back.

eat-popcorn

Anyways, let’s jump right into it.

partay-meow

For this feature, I want to add a checkbox toggle in the Sitecore Ribbon to give users the ability turn this feature on and off.

bucketed-items-count-view-ribbon

In order to save the state of this checkbox, I defined the following interface:

namespace Sitecore.Sandbox.Web.UI.HtmlControls.Registries
{
    public interface IRegistry
    {
        bool GetBool(string key);

        bool GetBool(string key, bool defaultvalue);

        int GetInt(string key);

        int GetInt(string key, int defaultvalue);

        string GetString(string key);

        string GetString(string key, string defaultvalue);

        string GetValue(string key);

        void SetBool(string key, bool val);

        void SetInt(string key, int val);

        void SetString(string key, string value);

        void SetValue(string key, string value);
    }
}

Classes of the above interface will keep track of settings which need to be stored somewhere.

The following class implements the interface above:

namespace Sitecore.Sandbox.Web.UI.HtmlControls.Registries
{
    public class Registry : IRegistry
    {
        public virtual bool GetBool(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetBool(key);
        }

        public virtual bool GetBool(string key, bool defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetBool(key, defaultvalue);
        }

        public virtual int GetInt(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetInt(key);
        }

        public virtual int GetInt(string key, int defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetInt(key, defaultvalue);
        }

        public virtual string GetString(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetString(key);
        }

        public virtual string GetString(string key, string defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetString(key, defaultvalue);
        }

        public virtual string GetValue(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetValue(key);
        }

        public virtual void SetBool(string key, bool val)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetBool(key, val);
        }

        public virtual void SetInt(string key, int val)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetInt(key, val);
        }

        public virtual void SetString(string key, string value)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetString(key, value);
        }

        public virtual void SetValue(string key, string value)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetValue(key, value);
        }
    }
}

I’m basically wrapping calls to methods on the static Sitecore.Web.UI.HtmlControls.Registry class which is used for saving state on the checkboxes in the Sitecore ribbon — it might be used for keeping track of other things in the Sitecore Content Editor though that is beyond the scope of this post. Nothing magical going on here.

I then defined the following interface for keeping track of Content Editor settings for things related to Item Buckets:

namespace Sitecore.Sandbox.Buckets.Settings
{
    public interface IBucketsContentEditorSettings
    {
        bool ShowBucketedItemsCount { get; set; }

        bool AreItemBucketsEnabled { get; }
    }
}

The ShowBucketedItemsCount boolean property lets the caller know if we are to show the Bucketed Items count, and the AreItemBucketsEnabled boolean property lets the caller know if the Item Buckets feature is enabled in Sitecore.

The following class implements the interface above:

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Determiners.Features;
using Sitecore.Sandbox.Web.UI.HtmlControls.Registries;

namespace Sitecore.Sandbox.Buckets.Settings
{
    public class BucketsContentEditorSettings : IBucketsContentEditorSettings
    {
        protected IFeatureDeterminer ItemBucketsFeatureDeterminer { get; set; }
        
        protected IRegistry Registry { get; set; }

        protected string ShowBucketedItemsCountRegistryKey { get; set; }

        public bool ShowBucketedItemsCount
        {
            get
            {
                return ShouldShowBucketedItemsCount();
            }
            set
            {
                ToggleShowBucketedItemsCount(value);
            }
        }

        public bool AreItemBucketsEnabled
        {
            get
            {
                return GetAreItemBucketsEnabled();
            }
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            if (!AreItemBucketsEnabled)
            {
                return false;
            }

            EnsureRegistryDependencies();
            return Registry.GetBool(ShowBucketedItemsCountRegistryKey, false);
        }

        protected virtual void ToggleShowBucketedItemsCount(bool turnOn)
        {
            
            if (!AreItemBucketsEnabled)
            {
                return;
            }

            EnsureRegistryDependencies();
            Registry.SetBool(ShowBucketedItemsCountRegistryKey, turnOn);
        }

        protected virtual void EnsureRegistryDependencies()
        {
            Assert.IsNotNull(Registry, "Registry must be defined in configuration!");
            Assert.IsNotNullOrEmpty(ShowBucketedItemsCountRegistryKey, "ShowBucketedItemsCountRegistryKey must be defined in configuration!");
        }

        protected virtual bool GetAreItemBucketsEnabled()
        {
            Assert.IsNotNull(ItemBucketsFeatureDeterminer, "ItemBucketsFeatureDeterminer must be defined in configuration!");
            return ItemBucketsFeatureDeterminer.IsEnabled();
        }
    }
}

I’m injecting an IFeatureDeterminer instance into the instance of the class above via the Sitecore Configuration Factory — have a look at the patch configuration file further down in this post — specifically the ItemBucketsFeatureDeterminer which is defined in a previous blog post. The IFeatureDeterminer instance determines whether the Item Buckets feature is turned on/off (I’m not going to repost that code here so if you haven’t seen this code, please go have a look now so you have an understanding of what it’s doing).

Its instance is used in the GetAreItemBucketsEnabled() method which just delegates to its IsEnabled() method and returns the value from that call. The GetAreItemBucketsEnabled() method is used in the get accessor of the AreItemBucketsEnabled property.

I’m also injecting an IRegistry instance into the instance of the class above — this is also defined in the patch configuration file further down — which is used for storing/retrieving the value of the ShowBucketedItemsCount property.

It is leveraged in the ShouldShowBucketedItemsCount() and ToggleShowBucketedItemsCount() methods where a boolean value is saved or retrieved, respectively, in the Sitecore Registry under a certain key — this key is also injected into the ShowBucketedItemsCountRegistryKey property via the Sitecore Configuration Factory.

So, we now have a way to keep track of whether we should display the Bucketed Items count. We just need a way to let the user turn this on/off. To do that, I need to create a custom Sitecore.Shell.Framework.Commands.Command.

Since Sitecore Commands are instantiated by the CreateObject() method on the MainUtil class (this lives in the Sitecore namespace in Sitecore.Kernel.dll and isn’t as advanced as the Sitecore.Configuration.Factory class as it won’t instantiate nested objects defined in configuration as does the Sitecore Configuration Factory), I built the following Command which will decorate Commands defined in Sitecore configuration:

using System.Xml;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class ExtendedConfigCommand : Command
    {
        private Command command;
        protected Command Command
        {
            get
            {
                if(command == null)
                {
                    command = GetCommand();
                    EnsureCommand();
                }

                return command;
            }
        }
        
        protected virtual Command GetCommand()
        {
            XmlNode currentCommandNode = Factory.GetConfigNode(string.Format("commands/command[@name='{0}']", Name));
            string configPath = XmlUtil.GetAttribute("extendedCommandPath", currentCommandNode);
            Assert.IsNotNullOrEmpty(configPath, string.Format("The extendedCommandPath attribute must be set {0}!", currentCommandNode));
            Command command = Factory.CreateObject(configPath, false) as Command;
            Assert.IsNotNull(command, string.Format("The command defined at '{0}' was either not properly set or is not an instance of Sitecore.Shell.Framework.Commands.Command. Double-check it!", configPath));
            return command;
        }

        protected virtual void EnsureCommand()
        {
            Assert.IsNotNull(Command, "GetCommand() cannot return a null Sitecore.Shell.Framework.Commands.Command instance!");
        }

        public override void Execute(CommandContext context)
        {
            Command.Execute(context);
        }

        public override string GetClick(CommandContext context, string click)
        {
            return Command.GetClick(context, click);
        }

        public override string GetHeader(CommandContext context, string header)
        {
            return Command.GetHeader(context, header);
        }

        public override string GetIcon(CommandContext context, string icon)
        {
            return Command.GetIcon(context, icon);
        }

        public override Control[] GetSubmenuItems(CommandContext context)
        {
            return Command.GetSubmenuItems(context);
        }

        public override string GetToolTip(CommandContext context, string tooltip)
        {
            return Command.GetToolTip(context, tooltip);
        }

        public override string GetValue(CommandContext context, string value)
        {
            return Command.GetValue(context, value);
        }

        public override CommandState QueryState(CommandContext context)
        {
            return Command.QueryState(context);
        }
    }
}

The GetCommand() method reads the XmlNode for the current command, and gets the value set on its extendedCommandPath attribute. This value must to be a config path defined under the <sitecore> element in Sitecore configuration.

If the attribute doesn’t exist or is empty, or a Command instance isn’t properly created, an exception is thrown.

Otherwise, it is set on the Command property on the class.

All methods here delegate to the same methods on the Command stored in the Command property.

I then defined the following Command which will be used by the checkbox we are adding to the Sitecore Ribbon:

using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Buckets.Settings;

namespace Sitecore.Sandbox.Buckets.Shell.Framework.Commands
{
    public class ToggleBucketedItemsCountCommand : Command
    {
        protected IBucketsContentEditorSettings BucketsContentEditorSettings { get; set; }

        public override void Execute(CommandContext context)
        {
            if (!AreItemBucketsEnabled())
            {
                return;
            }
            
            ToggleShowBucketedItemsCount();
            Reload();
        }

        protected virtual void ToggleShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            BucketsContentEditorSettings.ShowBucketedItemsCount = !BucketsContentEditorSettings.ShowBucketedItemsCount;
        }

        protected virtual void Reload()
        {
            SheerResponse.SetLocation(string.Empty);
        }

        public override CommandState QueryState(CommandContext context)
        {
            if(!AreItemBucketsEnabled())
            {
                return CommandState.Hidden;
            }

            if(!ShouldShowBucketedItemsCount())
            {
                return CommandState.Enabled;
            }

            return CommandState.Down;
        }

        protected virtual bool AreItemBucketsEnabled()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.AreItemBucketsEnabled;
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.ShowBucketedItemsCount;
        }
    }
}

The QueryState() method determines whether we should display the checkbox — it will only be displayed if the Item Buckets feature is on — and what the state of the checkbox should be — if we are currently showing Bucketed Items count, the checkbox will be checked (this is represented by CommandState.Down). Otherwise, it will be unchecked (this is represented by CommandState.Enabled).

The Execute() method encapsulates the logic of what we are to do when the user checks/unchecks the checkbox. It’s basically delegating to the ToggleShowBucketedItemsCount() method to toggle the value of whether we are to display the Bucketed Items count, and then reloads the Content Editor to refresh the display in the Content Tree.

I then had to define this checkbox in the Core database:

bucketed-items-count-checkbox-core

I’m not going to go into details of how the above works as I’ve written over a gazillion posts on the subject. I recommend having a read of one of these older posts.

After going back to my Master database, I saw the new checkbox in the Sitecore Ribbon:

buckted-items-count-new-checkbox

Since we could be dealing with thousands — if not millions — of Bucketed Items for each Item Bucket, we need a performant way to grab the count of these Items. In this solution, I am leveraging the Sitecore.ContentSearch API to get these counts though needed to add some custom
Computed Index Field classes:

using Sitecore.Buckets.Managers;
using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields
{
    public class IsBucketed : AbstractComputedIndexField
    {
        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; private set; }

        public IsBucketed()
        {
            ItemBucketsFeatureMethods = GetItemBucketsFeatureMethods();
            Assert.IsNotNull(ItemBucketsFeatureMethods, "GetItemBucketsFeatureMethods() cannot return null!");
        }

        protected virtual IItemBucketsFeatureMethods GetItemBucketsFeatureMethods()
        {
            IItemBucketsFeatureMethods methods = Factory.CreateObject("buckets/methods/itemBucketsFeatureMethods", false) as IItemBucketsFeatureMethods;
            Assert.IsNotNull(methods, "the IItemBucketsFeatureMethods instance was not defined properly in /sitecore/buckets/methods/itemBucketsFeatureMethods!");
            return methods;
        }

        public override object ComputeFieldValue(IIndexable indexable)
        {
            Item item = indexable as SitecoreIndexableItem;
            if (item == null)
            {
                return null;
            }

            
            return IsBucketable(item) && IsItemContainedWithinBucket(item);
        }

        protected virtual bool IsBucketable(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return BucketManager.IsBucketable(item);
        }

        protected virtual bool IsItemContainedWithinBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(IsItemBucket(item))
            {
                return false;
            }

            return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item);
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (!ItemBucketsFeatureMethods.IsItemBucket(item))
            {
                return false;
            }

            return true;
        }
    }
}

An instance of the class above ultimately determines if an Item is bucketed within an Item Bucket, and passes a boolean value to its caller denoting this via its ComputeFieldValue() method.

What determines whether an Item is bucketed? The code above says it’s bucketed only when the Item is bucketable and is contained within an Item Bucket.

The IsBucketable() method above ascertains whether the Item is bucketable by delegating to the IsBucketable() method on the BucketManager class in Sitecore.Buckets.dll.

The IsItemContainedWithinBucket() method determines if the Item is contained within an Item Bucket — you might be laughing as the name on the method is self-documenting — by delegating to the IsItemContainedWithinBucket() method on the IItemBucketsFeatureMethods instance — I’ve defined the code for this in this post so go have a look.

Moreover, the code does not consider Item Buckets to be Bucketed as that just doesn’t make much sense. ๐Ÿ˜‰ This would also give us an inaccurate count.

The following Computed Index Field’s ComputeFieldValue() method returns the string representation of the ancestor Item Bucket’s Sitecore.Data.ID for the Item — if it is contained within an Item Bucket:

using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields
{
    public class ItemBucketAncestorId : AbstractComputedIndexField
    {
        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; private set; }

        public ItemBucketAncestorId()
        {
            ItemBucketsFeatureMethods = GetItemBucketsFeatureMethods();
            Assert.IsNotNull(ItemBucketsFeatureMethods, "GetItemBucketsFeatureMethods() cannot return null!");
        }

        protected virtual IItemBucketsFeatureMethods GetItemBucketsFeatureMethods()
        {
            IItemBucketsFeatureMethods methods = Factory.CreateObject("buckets/methods/itemBucketsFeatureMethods", false) as IItemBucketsFeatureMethods;
            Assert.IsNotNull(methods, "the IItemBucketsFeatureMethods instance was not defined properly in /sitecore/buckets/methods/itemBucketsFeatureMethods!");
            return methods;
        }

        public override object ComputeFieldValue(IIndexable indexable)
        {
            Item item = indexable as SitecoreIndexableItem;
            if (item == null)
            {
                return null;
            }

            Item itemBucketAncestor = GetItemBucketAncestor(item);
            if(itemBucketAncestor == null)
            {

                return null;
            }

            return NormalizeGuid(itemBucketAncestor.ID);
        }

        protected virtual Item GetItemBucketAncestor(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(IsItemBucket(item))
            {
                return null;
            }

            Item itemBucket = ItemBucketsFeatureMethods.GetItemBucket(item);
            if(!IsItemBucket(itemBucket))
            {
                return null;
            }

            return itemBucket;
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (!ItemBucketsFeatureMethods.IsItemBucket(item))
            {
                return false;
            }

            return true;
        }

        protected virtual string NormalizeGuid(ID id)
        {
            return IdHelper.NormalizeGuid(id);
        }
    }
}

Not to go too much into details of the class above, it will only return an Item Bucket’s Sitecore.Data.ID as a string if the Item lives within an Item Bucket and is not itself an Item Bucket.

If the Item is not within an Item Bucket or is an Item Bucket, null is returned to the caller via the ComputeFieldValue() method.

I then created the following subclass of Sitecore.ContentSearch.SearchTypes.SearchResultItem — this lives in Sitecore.ContentSearch.dll — in order to use the values in the index that the previous Computed Field Index classes returned for their storage in the search index:

using System.ComponentModel;

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Converters;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;

namespace Sitecore.Sandbox.Buckets.ContentSearch.SearchTypes
{
    public class BucketedSearchResultItem : SearchResultItem
    {
        [IndexField("item_bucket_ancestor_id")]
        [TypeConverter(typeof(IndexFieldIDValueConverter))]
        public ID ItemBucketAncestorId { get; set; }

        [IndexField("is_bucketed")]
        public bool IsBucketed { get; set; }
    }
}

Now, we need a class to get the Bucketed Item count for an Item Bucket. I defined the following interface for class implementations that do just that:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public interface IBucketedItemsCountProvider
    {
        int GetBucketedItemsCount(Item itemBucket);
    }
}

I then created the following class that implements the interface above:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Xml;

using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.Linq.Utilities;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Xml;

using Sitecore.Sandbox.Buckets.ContentSearch.SearchTypes;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public class BucketedItemsCountProvider : IBucketedItemsCountProvider
    {
        protected IDictionary<string, ISearchIndex> SearchIndexMap { get; private set; }

        public BucketedItemsCountProvider()
        {
            SearchIndexMap = CreateNewSearchIndexMap();
        }

        protected virtual IDictionary<string, ISearchIndex> CreateNewSearchIndexMap()
        {
            return new Dictionary<string, ISearchIndex>();
        }

        protected virtual void AddSearchIndexMap(XmlNode configNode)
        {
            if(configNode == null)
            {
                return;
            }

            string databaseName = XmlUtil.GetAttribute("database", configNode, null);
            Assert.IsNotNullOrEmpty(databaseName, "The database attribute on the searchIndexMap configuration element cannot be null or the empty string!");
            Assert.ArgumentCondition(!SearchIndexMap.ContainsKey(databaseName), "database", "The searchIndexMap configuration element's database attribute values must be unique!");

            Database database = Factory.GetDatabase(databaseName);
            Assert.IsNotNull(database, string.Format("No database exists with the name of '{0}'! Make sure the database attribute on your searchIndexMap configuration element is set correctly!", databaseName));
            
            string searchIndexName = XmlUtil.GetAttribute("searchIndex", configNode, null);
            Assert.IsNotNullOrEmpty(searchIndexName, "The searchIndex attribute on the searchIndexMap configuration element cannot be null or the empty string!");

            ISearchIndex searchIndex = GetSearchIndex(searchIndexName);
            Assert.IsNotNull(searchIndex, string.Format("No search index exists with the name of '{0}'! Make sure the searchIndex attribute on your searchIndexMap configuration element is set correctly", searchIndexName));

            SearchIndexMap.Add(databaseName, searchIndex);
        }

        public virtual int GetBucketedItemsCount(Item bucketItem)
        {
            Assert.ArgumentNotNull(bucketItem, "bucketItem");

            ISearchIndex searchIndex = GetSearchIndex();
            using (IProviderSearchContext searchContext = searchIndex.CreateSearchContext())
            {
                var predicate = GetSearchPredicate<BucketedSearchResultItem>(bucketItem.ID);
                IQueryable<SearchResultItem> query = searchContext.GetQueryable<BucketedSearchResultItem>().Filter(predicate);
                SearchResults<SearchResultItem> results = query.GetResults();
                return results.Count();
            }
        }

        protected virtual ISearchIndex GetSearchIndex()
        {
            string databaseName = GetContentDatabaseName();
            Assert.IsNotNullOrEmpty(databaseName, "The GetContentDatabaseName() method cannot return null or the empty string!");
            Assert.ArgumentCondition(SearchIndexMap.ContainsKey(databaseName), "databaseName", string.Format("There is no ISearchIndex instance mapped to the database: '{0}'!", databaseName));
            return SearchIndexMap[databaseName];
        }

        protected virtual string GetContentDatabaseName()
        {
            Database database = Context.ContentDatabase ?? Context.Database;
            Assert.IsNotNull(database, "Argggggh! There's no content database! Houston, we have a problem!");
            return database.Name;
        }

        protected virtual ISearchIndex GetSearchIndex(string searchIndexName)
        {
            Assert.ArgumentNotNullOrEmpty(searchIndexName, "searchIndexName");
            return ContentSearchManager.GetIndex(searchIndexName);
        }

        protected virtual Expression<Func<TSearchResultItem, bool>> GetSearchPredicate<TSearchResultItem>(ID itemBucketId) where TSearchResultItem : BucketedSearchResultItem
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemBucketId), "itemBucketId", "itemBucketId cannot be null or empty!");
            var predicate = PredicateBuilder.True<TSearchResultItem>();
            predicate = predicate.And(item => item.ItemBucketAncestorId == itemBucketId);
            predicate = predicate.And(item => item.IsBucketed);
            return predicate;
        }
    }
}

Ok, so what’s going on in the class above? The AddSearchIndexMap() method is called by the Sitecore Configuration Factory to add database-to-search-index mappings — have a look at the patch configuration file further below. The code is looking up the appropriate search index for the content/context database.

The GetBucketedItemsCount() method gets the “predicate” from the GetSearchPredicate() method which basically says “Hey, I want an Item that has an ancestor Item Bucket Sitecore.Data.ID which is the same as the Sitecore.Data.ID passed to the method, and also this Item should be bucketed”.

The GetBucketedItemsCount() method then employs the Sitecore.ContentSearch API to get the result-set of the Items for the query, and returns the count of those Items.

Just as Commands, DataViews in Sitecore are instantiated by the CreateObject() method on MainUtil. I want to utilize the Sitecore Configuration Factory instead so that my nested configuration elements are instantiated and injected into my custom DataView. I built the following interface to make that possible:

using System.Collections;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public interface IDataViewBaseExtender
    {
        void FilterItems(ref ArrayList children, string filter);

        void GetChildItems(ItemCollection items, Item item);

        Database GetDatabase();

        Item GetItemFromID(string id, Language language, Version version);

        Item GetParentItem(Item item);

        bool HasChildren(Item item, string filter);

        void Initialize(string parameters);

        bool IsAncestorOf(Item ancestor, Item item);

        void SortItems(ArrayList children, string sortBy, bool sortAscending);
    }
}

All of the methods in the above interface correspond to virtual methods defined on the Sitecore.Web.UI.HtmlControl.DataViewBase class in Sitecore.Kernel.dll.

I then built the following abstract class which inherits from any DataView class that inherits from Sitecore.Web.UI.HtmlControl.DataViewBase:

using System.Collections;

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public abstract class ExtendedDataView<TDataView> : DataViewBase where TDataView : DataViewBase
    {
        protected IDataViewBaseExtender DataViewBaseExtender { get; private set; }

        protected ExtendedDataView()
        {
            DataViewBaseExtender = GetDataViewBaseExtender();
            EnsureDataViewBaseExtender();
        }

        protected virtual IDataViewBaseExtender GetDataViewBaseExtender()
        {
            string configPath = GetDataViewBaseExtenderConfigPath();
            Assert.IsNotNullOrEmpty(configPath, "GetDataViewBaseExtenderConfigPath() cannot return null or the empty string!");
            IDataViewBaseExtender dataViewBaseExtender = Factory.CreateObject(configPath, false) as IDataViewBaseExtender;
            Assert.IsNotNull(dataViewBaseExtender, string.Format("the IDataViewBaseExtender instance was not defined properly in '{0}'!", configPath));
            return dataViewBaseExtender;
        }

        protected abstract string GetDataViewBaseExtenderConfigPath();

        protected virtual void EnsureDataViewBaseExtender()
        {
            Assert.IsNotNull(DataViewBaseExtender, "GetDataViewBaseExtender() cannot return a null IDataViewBaseExtender instance!");
        }

        protected override void FilterItems(ref ArrayList children, string filter)
        {
            DataViewBaseExtender.FilterItems(ref children, filter);
        }

        protected override void GetChildItems(ItemCollection items, Item item)
        {
            DataViewBaseExtender.GetChildItems(items, item);
        }

        public override Database GetDatabase()
        {
            return DataViewBaseExtender.GetDatabase();
        }

        protected override Item GetItemFromID(string id, Language language, Version version)
        {
            return DataViewBaseExtender.GetItemFromID(id, language, version);
        }

        protected override Item GetParentItem(Item item)
        {
            return DataViewBaseExtender.GetParentItem(item);
        }

        public override bool HasChildren(Item item, string filter)
        {
            return DataViewBaseExtender.HasChildren(item, filter);
        }

        public override void Initialize(string parameters)
        {
            DataViewBaseExtender.Initialize(parameters);
        }

        public override bool IsAncestorOf(Item ancestor, Item item)
        {
            return DataViewBaseExtender.IsAncestorOf(ancestor, item);
        }

        protected override void SortItems(ArrayList children, string sortBy, bool sortAscending)
        {
            DataViewBaseExtender.SortItems(children, sortBy, sortAscending);
        }
    }
}

The GetDataViewBaseExtender() method gets the config path for the configuration-defined IDataViewBaseExtender — these IDataViewBaseExtender configuration definitions may or may not have nested configuration elements which will also be instantiated by the Sitecore Configuration Factory — from the abstract GetDataViewBaseExtenderConfigPath() method (subclasses must define this method).

The GetDataViewBaseExtender() then employs the Sitecore Configuration Factory to create this IDataViewBaseExtender instance, and return it to the caller (it’s being called in the class’ constructor).

If the instance is null, an exception is thrown.

All other methods in the above class delegate to methods with the same name and parameters on the IDataViewBaseExtender instance.

I then built the following subclass of the abstract class above:

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public class ExtendedMasterDataView : ExtendedDataView<MasterDataView>
    {
        protected override string GetDataViewBaseExtenderConfigPath()
        {
            return "extendedDataViews/extendedMasterDataView";
        }
    }
}

The above class is used for extending the MasterDataView in Sitecore.

It’s now time for the “real deal” DataView that does what we want: show the Bucketed Item counts for Item Buckets. The instance of the following class does just that:

using System.Collections;

using Sitecore.Buckets.Forms;
using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;

using Sitecore.Sandbox.Buckets.Providers.Items;
using Sitecore.Sandbox.Buckets.Settings;
using Sitecore.Sandbox.Buckets.Util.Methods;
using Sitecore.Sandbox.Web.UI.HtmlControls.DataViews;

namespace Sitecore.Sandbox.Buckets.Forms
{
    public class BucketedItemsCountDataView : BucketDataView, IDataViewBaseExtender
    {
        protected IBucketsContentEditorSettings BucketsContentEditorSettings { get; set; }

        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        protected IBucketedItemsCountProvider BucketedItemsCountProvider { get; set; }

        protected string SingularBucketedItemsDisplayNameFormat { get; set; }

        protected string PluralBucketedItemsDisplayNameFormat { get; set; }

        void IDataViewBaseExtender.FilterItems(ref ArrayList children, string filter)
        {
            FilterItems(ref children, filter);
        }

        void IDataViewBaseExtender.GetChildItems(ItemCollection children, Item parent)
        {
            GetChildItems(children, parent);
        }

        protected override void GetChildItems(ItemCollection children, Item parent)
        {
            
            base.GetChildItems(children, parent);
            if(!ShouldShowBucketedItemsCount())
            {
                return;
            }

            for (int i = children.Count - 1; i >= 0; i--)
            {
                Item child = children[i];
                if (IsItemBucket(child))
                {
                    int count = GetBucketedItemsCount(child);
                    Item alteredItem = GetCountDisplayNameItem(child, count);
                    children.RemoveAt(i);
                    children.Insert(i, alteredItem);
                }
            }
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.ShowBucketedItemsCount;
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!");
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucket(item);
        }

        protected virtual int GetBucketedItemsCount(Item itemBucket)
        {
            Assert.IsNotNull(BucketedItemsCountProvider, "BucketedItemsCountProvider must be set in configuration!");
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            return BucketedItemsCountProvider.GetBucketedItemsCount(itemBucket);
        }

        protected virtual Item GetCountDisplayNameItem(Item item, int count)
        {
            FieldList fields = new FieldList();
            item.Fields.ReadAll();
            
            foreach (Field field in item.Fields)
            {
                fields.Add(field.ID, field.Value);
            }

            int bucketedCount = GetBucketedItemsCount(item);
            string displayName = GetItemNameWithBucketedCount(item, bucketedCount);
            ItemDefinition itemDefinition = new ItemDefinition(item.ID, displayName, item.TemplateID, ID.Null);
            return new Item(item.ID, new ItemData(itemDefinition, item.Language, item.Version, fields), item.Database) { RuntimeSettings = { Temporary = true } };
        }

        protected virtual string GetItemNameWithBucketedCount(Item item, int bucketedCount)
        {
            Assert.IsNotNull(SingularBucketedItemsDisplayNameFormat, "SingularBucketedItemsDisplayNameFormat must be set in configuration!");
            Assert.IsNotNull(PluralBucketedItemsDisplayNameFormat, "PluralBucketedItemsDisplayNameFormat must be set in configuration!");

            if (bucketedCount == 1)
            {
                return ReplaceTokens(SingularBucketedItemsDisplayNameFormat, item, bucketedCount);
            }

            return ReplaceTokens(PluralBucketedItemsDisplayNameFormat, item, bucketedCount);
        }

        protected virtual string ReplaceTokens(string format, Item item, int bucketedCount)
        {
            Assert.ArgumentNotNullOrEmpty(format, "format");
            Assert.ArgumentNotNull(item, "item");
            string replaced = format;
            replaced = replaced.Replace("$displayName", item.DisplayName);
            replaced = replaced.Replace("$bucketedCount", bucketedCount.ToString());
            return replaced;
        }

        Database IDataViewBaseExtender.GetDatabase()
        {
            return GetDatabase();
        }

        Item IDataViewBaseExtender.GetItemFromID(string id, Language language, Version version)
        {
            return GetItemFromID(id, language, version);
        }

        Item IDataViewBaseExtender.GetParentItem(Item item)
        {
            return GetParentItem(item);
        }

        bool IDataViewBaseExtender.HasChildren(Item item, string filter)
        {
            return HasChildren(item, filter);
        }

        void IDataViewBaseExtender.Initialize(string parameters)
        {
            Initialize(parameters);
        }

        bool IDataViewBaseExtender.IsAncestorOf(Item ancestor, Item item)
        {
            return IsAncestorOf(ancestor, item);
        }

        void IDataViewBaseExtender.SortItems(ArrayList children, string sortBy, bool sortAscending)
        {
            SortItems(children, sortBy, sortAscending);
        }
    }
}

You might be saying to yourself “Mike, what in the world is going on here?” ๐Ÿ˜‰ Let me explain by starting with the GetChildItems() method.

The GetChildItems() method is used to build up the collection of child Items that display in the Content Tree when you expand a parent node. It does this by populating the ItemCollection instance passed to it.

The particular implementation above is delegating to the base class’ implementation to get the list of child Items for display in the Content Tree.

If we should not show the Bucketed Items count — this is determined by the ShouldShowBucketedItemsCount() method which just returns the boolean value set on the ShowBucketedItemsCount property of the injected IBucketsContentEditorSettings instance — the code just exits.

If we are to show the Bucketed Items count, we iterate over the ItemCollection collection and see if any of these child Items are Item Buckets — this is determined by the IsItemBucket() method.

If we find an Item Bucket, we get its count of Bucketed Items via the GetBucketedItemsCount() method which delegates to the GetBucketedItemsCount() method on the injected IBucketedItemsCountProvider instance.

Once we have the count, we call the GetCountDisplayNameItem() method which populates a FieldList collection with all of the fields defined on the Item Bucket; call the GetItemNameWithBucketedCount() method to get the new display name to show in the Content Tree — this method determines which display name format to use depending on whether we should use singular or pluralized messaging, and expands value on tokens via the ReplaceTokens() method — these tokens are defined in the patch configuration file below; creates an ItemDefinition instance so we can set the new display name; and returns a new Sitecore.Data.Items.Item instance to the caller.

No, don’t worry, we aren’t adding a new Item in the content tree but creating a fake “wrapper” of the real one, and replacing this in the ItemCollection.

We also have to fully implement the IDataViewBaseExtender interface. For most methods, I just delegate to the corresponding methods defined on the base class except for the IDataViewBaseExtender.GetChildItems() method which uses the GetChildItems() method defined above.

I then bridged everything above together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <buckets>
      <extendedCommands>
        <toggleBucketedItemsCountCommand type="Sitecore.Sandbox.Buckets.Shell.Framework.Commands.ToggleBucketedItemsCountCommand, Sitecore.Sandbox" singleInstance="on">
          <BucketsContentEditorSettings ref="buckets/settings/bucketsContentEditorSettings" />
        </toggleBucketedItemsCountCommand>
      </extendedCommands>
      <providers>
        <items>
          <bucketedItemsCountProvider type="Sitecore.Sandbox.Buckets.Providers.Items.BucketedItemsCountProvider, Sitecore.Sandbox" singleInstance="true">
            <searchIndexMaps hint="raw:AddSearchIndexMap">
              <searchIndexMap database="master" searchIndex="sitecore_master_index" />
              <searchIndexMap database="web" searchIndex="sitecore_web_index" />
            </searchIndexMaps>
          </bucketedItemsCountProvider>
        </items>
      </providers>
      <settings>
        <bucketsContentEditorSettings type="Sitecore.Sandbox.Buckets.Settings.BucketsContentEditorSettings, Sitecore.Sandbox" singleInstance="true">
          <ItemBucketsFeatureDeterminer ref="determiners/features/itemBucketsFeatureDeterminer"/>
          <Registry ref="registries/registry" />
          <ShowBucketedItemsCountRegistryKey>/Current_User/UserOptions.View.ShowBucketedItemsCount</ShowBucketedItemsCountRegistryKey>
        </bucketsContentEditorSettings>
      </settings>
    </buckets>
    <commands>
      <command name="contenteditor:togglebucketeditemscount" type="Sitecore.Sandbox.Shell.Framework.Commands.ExtendedConfigCommand, Sitecore.Sandbox" extendedCommandPath="buckets/extendedCommands/toggleBucketedItemsCountCommand" />
    </commands>
    <contentSearch>
      <indexConfigurations>
        <defaultLuceneIndexConfiguration>
          <fieldMap>
            <fieldNames>
              <field fieldName="item_bucket_ancestor_id" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
                <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
              <field fieldName="is_bucketed" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
            </fieldNames>
          </fieldMap>
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">
              <field fieldName="item_bucket_ancestor_id">Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields.ItemBucketAncestorId, Sitecore.Sandbox</field>
              <field fieldName="is_bucketed">Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields.IsBucketed, Sitecore.Sandbox</field>
            </fields>
          </documentOptions>
        </defaultLuceneIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
    <dataviews>
      <dataview name="Master">
        <patch:attribute name="assembly">Sitecore.Sandbox</patch:attribute>
        <patch:attribute name="type">Sitecore.Sandbox.Web.UI.HtmlControls.DataViews.ExtendedMasterDataView</patch:attribute>
      </dataview>
    </dataviews>
    <extendedDataViews>
      <extendedMasterDataView type="Sitecore.Sandbox.Buckets.Forms.BucketedItemsCountDataView, Sitecore.Sandbox" singleInstance="true">
        <BucketsContentEditorSettings ref="buckets/settings/bucketsContentEditorSettings" />
        <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
        <BucketedItemsCountProvider ref="buckets/providers/items/bucketedItemsCountProvider" />
        <SingularBucketedItemsDisplayNameFormat>$displayName &lt;span style="font-style: italic; color: blue;"&gt;($bucketedCount bucketed item)&lt;span&gt;</SingularBucketedItemsDisplayNameFormat>
        <PluralBucketedItemsDisplayNameFormat>$displayName &lt;span style="font-style: italic; color: blue;"&gt;($bucketedCount bucketed items)&lt;span&gt;</PluralBucketedItemsDisplayNameFormat>
      </extendedMasterDataView>
    </extendedDataViews>
    <registries>
      <registry type="Sitecore.Sandbox.Web.UI.HtmlControls.Registries.Registry, Sitecore.Sandbox" singleInstance="true" />
    </registries>
  </sitecore>
</configuration>

bridge-collapse

Let’s see this in action:

bucketed-items-count-testing

As you can see, it is working as intended.

partay-hard

Magical, right?

magic

Well, not really — it just appears that way. ๐Ÿ˜‰

magic-not-really

If you have any thoughts on this, please drop a comment.

Prevent Unbucketable Sitecore Items from Being Moved to Bucket Folders

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:

item-buckets-unbucketable-import

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”?

nope

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!”

no

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.

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.

jenga-topple

Let’s move this unbucketable Item to an Item Bucket:

move-unbucketable-1

Yes, I’m sure I’m sure:

move-unbucketable-2

I was then prompted with the confirmation dialog as expected:

move-unbucketable-3

As you can see, the Item was placed directly under the Item Bucket:

move-unbucketable-4

If you have any thoughts on this, please drop a comment.

thats-all-folks

Prevent Duplicate Names of Bucketed Sitecore Items

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.

challenge

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. ๐Ÿ˜‰

Cat-ate-my-homework-GIF

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>

plugged-in

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:

unique-name-move-1

Of course, I do:

unique-name-move-2

I gave it a unique name:

unique-name-move-3

As you can see, the Item was renamed and then moved:

unique-name-move-4

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.