Unique item name constraint using index search in Sitecore

While working on a project we came across a requirement of adding a constraint for having unique item names. While you could just instruct the content authors to ensure distinct item names, it would be great to be able to enforce this through code with a pretty message to the content authors if they did create items with duplicate names, wouldn’t it?

2015-02-27_012756

Now this might me a business requirement, but it is also something that would need to be enforced if you use custom routes (another post about custom routes to follow shortly!), resolving the item by item name in the route.

We are basically going to add sitecore events to execute on item creation and item save – where we will do an index search to determine whether an item of the same name already exists.

So whatever be the reason, if you find the need to add a unique name constraint on items – be the scope a section in sitecore or the entire sitecore content tree, here’s how we go about it –

Code

using Sitecore.Data;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.SecurityModel;
using System;
using System.Collections.Generic;
using System.Linq;
using Event = Sitecore.Events.Event;

namespace MySite.Customizations.Customized_Sitecore
{
    public class UniqueItemNameValidator
    {
        /// <summary>
        /// Called when on item creation. Prevents creating items with duplicate names.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="args">The <see cref="EventArgs"/> instance containing the event data.</param>
        public void OnItemCreating(object sender, EventArgs args)
        {
            using (new SecurityDisabler())
            {
                var arg = Event.ExtractParameter(args, 0) as ItemCreatingEventArgs;
                if (arg == null || (Sitecore.Context.Site != null && Sitecore.Context.Site.Name != "shell")) return;

                try
                {
                    if (!IsUniqueItemNameInContext(arg.ItemName, arg.TemplateId))
                    {
                        ((SitecoreEventArgs)args).Result.Cancel = true;
                        Sitecore.Context.ClientPage.ClientResponse.Alert(string.Format("Name '{0}' is already in use. Please use another name for the item.", arg.ItemName));
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Error on creating item:" + arg.ItemId + Environment.NewLine + "StackTrace:" + ex.StackTrace, ex);
                }
            }
        }

        /// <summary>
        /// Called when item is saved. Prevents renaming items to have a duplicate name.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="args">The <see cref="EventArgs"/> instance containing the event data.</param>
        public void OnItemSaving(object sender, EventArgs args)
        {
            using (new SecurityDisabler())
            {
                var item = Event.ExtractParameter(args, 0) as Item;
                if (item == null || (Sitecore.Context.Site != null && Sitecore.Context.Site.Name != "shell")) return;
                if (item.Parent == null) return;

                try
                {
                    if (!IsUniqueItemNameInContext(item.Name, item.TemplateID, item.ID, true))
                    {
                        ((SitecoreEventArgs)args).Result.Cancel = true;
                        Sitecore.Context.ClientPage.ClientResponse.Alert(string.Format("Name '{0}' is already in use. Please use another name for the item.", item.Name));
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Error on saving item:" + item.ID.Guid + Environment.NewLine + "StackTrace:" + ex.StackTrace, ex);
                }
            }
        }

        private bool IsUniqueItemNameInContext(string itemName, ID templateId, ID itemId = null, bool excludeCurrentItem = false)
        {
            // Articles
            if (templateId == IArticleConstants.TemplateId)
            {
                return !CheckItemNameExists(itemName, itemId, templateId, "{<Guid of the Articles folder item>}",
                    Constants.Search.Index.SitecoreMaster, excludeCurrentItem);
            }

            // Projects
            if (templateId == IProjectConstants.TemplateId)
            {
                return !CheckItemNameExists(itemName, itemId, templateId, "{<Guid of the Projects folder>}",
                    Constants.Search.Index.SitecoreMaster, excludeCurrentItem);
            }

            #endregion

            return true;
        }

        private bool CheckItemNameExists
            (string itemName, ID itemId, ID templateId, ID locationId, 
            string indexName, bool excludeCurrentItem)
        {
            IEnumerable<Item> searchResults = SearchHelper.GetItemsOnCreateSave
                (indexName, Sitecore.Context.Language.ToString(), templateId, locationId, itemName);

            if (excludeCurrentItem && itemId != (ID)null && searchResults != null)
            {
                searchResults = searchResults.Where(x => x.ID != itemId);
            }

            return searchResults != null && searchResults.Any();
        }
    }
}

The item save event also needs to be written to handle scenarios when an item is renamed – to a duplicate name.
Something to note here is the “SearchHelper.GetItemsOnCreateSave” method! Here’s the code:

        public static List<Item> GetItemsOnCreateSave
            (string indexName, string language, ID templateId, ID locationId, string itemName)
        {
            using (var context = ContentSearchManager.GetIndex(indexName).CreateSearchContext())
            {
                var query = context.GetQueryable<SearchResultItem>().Where(x => x.Language == language);
                query = query.Where(x => x.Paths.Contains(locationId));
                query = query.Where(x => x.TemplateId == templateId);
                query = query.Where(x => x.Name == itemName);
                var temp = query.Select(x => x.GetItem()).ToList();
                return temp;
            }
        }

This method will do a search on the index passed in based on the template we are trying to check this for.
In my case we did have different templates being a part of different indexes in a couple of cases – if you have a single index, you might not need to pass the name around.

Also, additionally, in this case – I could have a project and an article of the same name – which would be a valid scenario, which actually meets the business requirements I had in hand. In case you want to have no duplicate item names in the entire content tree, you could skip the template check / location check and do a blind index search without a location / template filter.

Index search being as efficient as it is – the performance hit should be minimal.

Configuration
Of course, you would also need to add these events to your sitecore configuration:

  <sitecore>
    <events>
      <event name="item:creating">
        <handler type="MySite.Customizations.Customized_Sitecore.UniqueItemNameValidator, MySite" method="OnItemCreating" />
      </event>
      <event name="item:saving">
        <handler type="MySite.Customizations.Customized_Sitecore.UniqueItemNameValidator, MySite" method="OnItemSaving" />
      </event>
    </events>
  </sitecore>

Note: From experience, it would be a good idea to turn this configuration off while installing packages to your site – to avoid additional processing when you don’t need it – if that be the case.

Advertisements

, , , , , ,

  1. Using Custom Routes in Sitecore MVC | Tech Musingz

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: