codeflood logo

Unique Item Name per Bucket

Last year I wrote a post on customising the URLs used to access items within an Item Bucket when using Sitecore 7+. The basic idea was that the structure of the bucket was completely ignored. So a URL for an item within a bucket would include the path to the bucket, then the item name. There was one important aspect of this approach that I deliberately ignored at the time of writing to keep the scope of the post contained. The approach explored in my previous post doesn't allow for multiple items within the same bucket to share the same name. So how would one ensure item names within a bucket (of the items of interest, not of the bucket folders) to be unique? As always with Sitecore, we have several options. The first option may be an event handler tied into the item:saving event. Although this may functionally work it can also have some usability issues. In particular, if using an event handler one would likely cancel the event if another item was found within the bucket with the same name. But it's not a good idea to stop an author from saving just to meet "validations". Always allow your authors to save their work and validate it later on. A more appropriate approach would be to use an item validator. Validators are more out-of-the-box flexible than an event handler. They can be tied into workflow so authors are free to save their work but not pass validation during the approval process of the workflow. They can also report a variety of different result levels from valid to error to critical error. Now there is actually already a "Duplicate Name" item validator that ships with Sitecore. However this validator only checks the names of the item's siblings. This won't work in a bucket where we need to check a tree of items. So what would this validator look like? Well it would grab the item being validated, find the bucket root, then use the Content Search API to search for other items under the bucket with the same name. Here's an example of such an item validator.

using System;
using System.Linq;
using System.Runtime.Serialization;
using Sitecore.Buckets.Extensions;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data.Validators;

namespace CustomBucketUrl
{
  [Serializable]
  public class UniqueNameBucketValidator : StandardValidator
  {
    public override string Name
    {
      get { return "Ensure unique item names within a bucket"; }
    }

    public UniqueNameBucketValidator()
    {
    }

    public UniqueNameBucketValidator(SerializationInfo info, StreamingContext context)
      : base(info, context)
    {
    }

    protected override ValidatorResult Evaluate()
    {
      var item = base.GetItem();

      // locate bucket root
      var bucketRoot = item.GetParentBucketItemOrParent();
      if (bucketRoot == null || !bucketRoot.IsABucket())
        return ValidatorResult.Valid;

      // search the bucket for an item with the same name
      using (var ctx = ContentSearchManager.GetIndex(
        (Sitecore.ContentSearch.SitecoreIndexableItem)item).CreateSearchContext())
      {
        var count = ctx.GetQueryable<SearchResultItem>().Where(
          x => string.Compare(x.Name, item.Name) == 0).Count();

        if (count > 1)
          return base.GetFailedResult(ValidatorResult.CriticalError);
      }

      return ValidatorResult.Valid;
    }

    protected override ValidatorResult GetMaxValidatorResult()
    {
      return base.GetFailedResult(ValidatorResult.CriticalError);
    }
  }
}

I've chosen in this example to return critical errors so the author gets an immediate notification about it. Even so, they can still continue saving even with a critical error. If you need to stop them from saving, you could change the critical errors to fatal errors instead, though as I mentioned above, I think this is a bad idea. As you can see in the above code, I'm using the Content Search API to search for items with the same name as the item being saved. From this query I grab the count of items matching and check to ensure there is only 1 item matching the query. With the class done we can now register the new validator in Sitecore and start using it. Item Validators are registered under /sitecore/system/Settings/Validation Rules/Item Rules. I'll create my rule under the Item subfolder in that location as the existing "Duplicate Name" validator is already. Create a new item in this location based on the /sitecore/templates/System/Validation/Validation Rule template and set the following fields:

Title: Ensure unique item name per bucket

Description: The item must have a unique name within the bucket.

Type: CustomBucketUrl.UniqueNameBucketValidator, CustomBucketUrl

Make sure you use the correct value in the type field based on the compiled validator class. With the validator registered we can now start using it. As a test I'll select the new validator in the Validation Bar Validation Rules field on the standard values of a template of an item I have in a bucket. Now when I create multiple items with the same name within the bucket and attempt to save the item, I get a notification that the item has critical errors. critical error from unique name validatorAs we're using Content Search to locate the items, if you're using Lucene as your search provider (the default) you'll need to consider if the default analyzer is adequate. The default analyzer (named the standard analyzer) removes common words from English phrases such as "and" and "or". You can read more about that at http://blog.horizontalintegration.com/2013/07/30/sitecore-contentsearch-fails-for-lucene-reserved-keywords-like-andor/. To work around this issue I'll update the index configuration to store the name field explicitly without using the standard analyzer. This works because Lucene allows a single document to include multiple fields of the same name and Sitecore will still be storing the name of the item using the standard analyzer in the _name field. This means the search logic above doesn't need to change.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <contentSearch>
            <indexConfigurations>
                <defaultLuceneIndexConfiguration>
                    <fieldMap>
                        <fieldNames>
                            <field fieldName="_name"
                                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>
                        </fieldNames>
                    </fieldMap>
                </defaultLuceneIndexConfiguration>
            </indexConfigurations>
        </contentSearch>
    </sitecore>
</configuration>

If you didn't want to add the additional _name field because Sitecore is already handling it then you could also add a differently named field and adjust the search logic above to use that field instead of _name.

Comments

Antonin

Nice solution. However, query is searching through the whole index which can be an issue if you have more bucket folders. I had to add a condition like 'x =>; x.Paths.Contains(bucketRoot.ID))' to ensure that only relevant bucket folder is searched.

Nick

This doesn't work because it only does validate on save. I can add the same named item 10 times but it only prevents me from saving when I edit it after the fact.

Leave a comment

All fields are required.