Home » DataView

Category Archives: DataView

Alter the Appearance of the Sitecore Content Tree using a Pipeline-backed MasterDataView

About 4 years ago, I blogged about creating a custom MasterDataView class to change the appearance of Items in the content tree.

One thing that bugged me back then was how this particular piece of Sitecore functionality wasn’t backed by a pipeline of some sort which gave me an idea on creating a custom MasterDataView which would delegate to a custom pipeline to alter how Items display in the content tree but I never got to it; as a matter of fact, I forgot all about it once I moved to Australia at the time — it’s easy to forget things when you are trying to dodge viscious magpies and venomous spiders 😉

However, I recently rethought of this idea due to a project I had worked on where I had to notify content authors that they had almost or too many child items in Media Folders in the Media Library. In this post, I am going to share this solution with you, and it heavily uses code from my previous post — I suggest having a read of that before proceeding.

I also want to call out that I’ve omitted code which registers most services used on this post in the Sitecore IoC container for brevity. If you want to learn about Dependency Injection on Sitecore, I highly recommend watching this presentation on YouTube.

Like any Sitecore pipeline, we need a PipelineArgs class of some sort, and the following is just that:

using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.Pipelines;

namespace Foundation.Kernel.Pipelines.DataViewChildItems
{
	public class DataViewChildItemsArgs : PipelineArgs
	{
		public ItemCollection Children { get; set; }

		public Item Parent { get; set; }

		public DataViewChildItemsArgs()
		{
		}

		public DataViewChildItemsArgs(ItemCollection children, Item parent)
		{
			Children = children;
			Parent = parent;
		}
	}
}

The GetChildItems() method on Sitecore.Web.UI.HtmlControls.MasterDataView, or those which inherit from it, take in an ItemCollection of children and their parent Item in the content tree. This is why I have these two properties on the DataViewChildItemsArgs class above — we are going to pass these to all processors of our custom pipeline.

On my recent project, I wanted to give the ability to have things be turned on and off via a feature toggle in Sitecore configuration, both at a global and invidual piece functionality level (i.e. have a global settings object with an Enabled property but also another Enabled property on each pipeline processor so we can turn the entire thing off, or just a piece of it).

The follow interface is to go with each bit of functionality that can be turned on/off:

namespace Foundation.Kernel.Services.FeatureToggle
{
	public interface IFeatureToggleable
	{
		bool Enabled { get; set; }
	}
}

We also need a service to read these Enabled properties, and determine if something should be turned on/off. The following is the interface for this service:

namespace Foundation.Kernel.Services.FeatureToggle
{
	public interface IFeatureToggleService
	{
		bool IsEnabled(params IFeatureToggleable[] toggleables);
	}
}

Here’s the implementation of the interface above:

using System.Collections.Generic;
using System.Linq;

namespace Foundation.Kernel.Services.FeatureToggle
{
	public class FeatureToggleService : IFeatureToggleService
	{
		public bool IsEnabled(params IFeatureToggleable[] toggleables) => !ShouldTurnOff(toggleables);

		protected virtual bool ShouldTurnOff(IEnumerable<IFeatureToggleable> toggleables)
		{
			if (toggleables == null || !toggleables.Any())
			{
				return true;
			}

			return toggleables.Any(toggleable => !toggleable.Enabled);
		}
	}
}

It takes in a collection of IFeatureToggleable, and returns false if any of the IFeatureToggleable has an Enabled property value of “false”.

I then define a base class for pipeline processors of the custom pipeline:

using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Kernel.Pipelines.DataViewChildItems
{
	public abstract class DataViewChildItemsProcessor : IFeatureToggleable
	{
		private readonly IFeatureToggleService _featureToggleService;

		public bool Enabled { get; set; }

		protected bool AbortPipelineAfterExecution { get; set; }

		protected DataViewChildItemsProcessor(IFeatureToggleService featureToggleService)
		{
			_featureToggleService = featureToggleService;
		}

		public void Process(DataViewChildItemsArgs args)
		{
			if (!IsEnabled() || !ShouldExecute(args))
			{
				return;
			}

			GetChildItems(args);
			if (!ShouldAbortPipeline(args))
			{
				return;
			}

			args.AbortPipeline();
		}

		protected virtual bool IsEnabled() => _featureToggleService.IsEnabled(this);

		protected abstract bool ShouldExecute(DataViewChildItemsArgs args);

		protected abstract void GetChildItems(DataViewChildItemsArgs args);

		protected virtual bool ShouldAbortPipeline(DataViewChildItemsArgs args) => AbortPipelineAfterExecution;
	}
}

Any pipeline processor which implements the base class above must consume the IFeatureToggleService instance above — ideally from the Sitecore IoC container which I have done further down below but you could also use Poor Man’s DI for this which I do not recommend as we have a native IoC container in Sitecore 😉

Now, let’s dive into setting up the custom MasterDataView. The following Config Object are settings for this class, and it contains values for both the custom pipeline’s domain and the pipeline’s name (I decided to group this by pipeline domain as I can see entry points on other overridable methods on the MasterDataView class which could be driven by other custom pipelines in this domain):

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;

namespace Foundation.Kernel.Models.DataViews
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/pipelineDrivenDataViewSettings", Lifetime = Lifetime.Singleton)]
	public class PipelineDrivenDataViewSettings
	{
		public string PipelineDomain { get; set; }

		public string PipelineName { get; set;  }
	}
}

I then created the following MasterDataView which inherits from the OOTB Sitecore.Buckets.Forms.BucketDataView in Sitecore.Buckets.dll as the OOTB MasterDataView which ships with Sitecore is this one — I could have gone ahead and created yet another pipeline processor for Buckets but I decided against this for now:

using Sitecore.Abstractions;
using Sitecore.Buckets.Forms;
using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.DependencyInjection;

using Foundation.Kernel.Pipelines.DataViewChildItems;
using Foundation.Kernel.Models.DataViews;

namespace Foundation.Kernel.DataViews
{
	public class PipelineDrivenDataView : BucketDataView
	{
		protected override void GetChildItems(ItemCollection children, Item parent)
		{
			base.GetChildItems(children, parent);
			DataViewChildItemsArgs args = CreateDataViewChildItemsArgs(children, parent);
			if(args == null)
			{
				return;
			}

			ICorePipeline corePipeline = GetService<ICorePipeline>();
			PipelineDrivenDataViewSettings settings = GetSettings();

			if (corePipeline == null || string.IsNullOrWhiteSpace(settings?.PipelineDomain) || string.IsNullOrWhiteSpace(settings?.PipelineName))
			{
				return;
			}

			corePipeline.Run(settings.PipelineName, args, settings.PipelineDomain);
		}

		protected virtual ICorePipeline GetCorePipeline() => GetService<ICorePipeline>();

		protected virtual PipelineDrivenDataViewSettings GetSettings() => GetService<PipelineDrivenDataViewSettings>();

		protected virtual TService GetService<TService>() where TService : class => ServiceLocator.ServiceProvider.GetService(typeof(TService)) as TService;

		protected virtual DataViewChildItemsArgs CreateDataViewChildItemsArgs(ItemCollection children, Item parent) => new DataViewChildItemsArgs(children, parent);
	}
}

The MasterDataView above creates a new DataViewChildItemsArgs instance by passing the Children ItemCollection and parent; the custom pipeline will ultimately modify just the Children collection but I built this being mindful that other pipeline processors might need to change the Parent Item for their purposes; I just don’t have this as an example on this post.

This DataViewChildItemsArgs instance is then passed to a call to the custom pipeline within the defined domain.

I then defined the new pipeline along with the custom MasterDataView, both defined above, in the Sitecore configuration patch file below:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
	<sitecore>
		<dataviews>
			<dataview name="Master">
				<patch:attribute name="assembly">Foundation.Kernel</patch:attribute>
				<patch:attribute name="type">Foundation.Kernel.DataViews.PipelineDrivenDataView</patch:attribute>
			</dataview>
		</dataviews>
		<moduleSettings>
			<foundation>
				<kernel>
					<pipelineDrivenDataViewSettings type="Foundation.Kernel.Models.DataViews.PipelineDrivenDataViewSettings, Foundation.Kernel" singleInstance="true">
						<PipelineDomain>masterDataView</PipelineDomain>
						<PipelineName>dataviewChildItems</PipelineName >
					</pipelineDrivenDataViewSettings>
				</kernel>
			</foundation>
		</moduleSettings>
		<pipelines>
			<group groupName="masterDataView" name="masterDataView">
				<pipelines>
					<dataviewChildItems>
					</dataviewChildItems>
				</pipelines>
			</group>
		</pipelines>
	</sitecore>
</configuration>

I wanted to abstract out the logic which determines if an Item has almost or too many child items as the project I was working on had more than one customization of the content editor to let content authors know it’s time to create a new folder for their items — dont worry I’ll blog about some of these in the future 😉 — so I defined the following interface for turning these individual bits of functionality on and off:

using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsFeatureToggleService
	{
		bool IsEnabled(IFeatureToggleable toggleable);
	}
}

Here’s the implementation of the IFeatureToggleable interface above — this is a service Config Object which will be the global settings object to turn on and off the entire feature set of these Content Editor customizations — and will be injected into the implementation of the ITooManySubItemsFeatureToggleService further below:

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;

using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Validation.Models.TooManySubItems
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/validation/tooManySubItemsSettings ", Lifetime = Lifetime.Singleton)]
	public class TooManySubItemsSettings : IFeatureToggleable
	{
		public bool Enabled { get; set; }

		public int NumberOfItemsToStartWarningUser{ get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }
	}
}

The Config Object above also has values on the number of child items on when to start warning the content authors they have almost too many child items, and also a value on when there are too many child items.

I then defined the TooManySubItemsSettings Config Object in the Sitecore patch configuration file below:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
	<sitecore>
		<moduleSettings>
			<foundation>
				<validation>
					<tooManySubItemsSettings type="Foundation.Validation.Models.TooManySubItems.TooManySubItemsSettings, Foundation.Validation" singleInstance="true">
						<Enabled>true</Enabled>
						<!-- <MaximumNumberOfItemsInFolder> must be set and greater than zero in order for the feature to work.  In other words, default Sitecore functionality will execute 
							when this isn't set (it's essentially like setting <Enabled>false</Enabled> above; you can't warn users they are approaching a maximum number of items in a folder
							when there is no maximum value set.
						-->
						<NumberOfItemsToStartWarningUser>2</NumberOfItemsToStartWarningUser>
						<MaximumNumberOfItemsInFolder>4</MaximumNumberOfItemsInFolder>
					</tooManySubItemsSettings>
				</validation>
			</foundation>
		</moduleSettings>
	</sitecore>
</configuration>

For testing, I set the values on when to warning content authors to a low number for testing but ideally, MaximumNumberOfItemsInFolder should be 100 child Items, and I chose 95 child Items for NumberOfItemsToStartWarningUser in the production version of this.

Now, we need an implementation of the ITooManySubItemsFeatureToggleService interface above. The following is just that:

using Foundation.Kernel.Services.FeatureToggle;

using Foundation.Validation.Models.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems;

namespace Feature.Validation.Services.TooManySubItems
{
	public class TooManySubItemsFeatureToggleService : ITooManySubItemsFeatureToggleService
	{
		private readonly TooManySubItemsSettings _settings;
		private readonly IFeatureToggleService _featureToggleService;

		public TooManySubItemsFeatureToggleService(TooManySubItemsSettings settings, IFeatureToggleService featureToggleService)
		{
			_settings = settings;
			_featureToggleService = featureToggleService;
		}

		public bool IsEnabled(IFeatureToggleable toggleable) => _featureToggleService.IsEnabled(_settings, toggleable);
	}
}

The class above will delegate to the IFeatureToggleService instance to determine if the entire feature set of visual cues to the content authors should be turned on/off, or if an invidual piece of functionality should be turned on/off; each IFeatureToggleable will also pass itself to this service so that it can determine whether it should be turned on/off.

Most of the features I had built for this will warn content authors for when they are approaching too many child Items and also convey there are too many child items but there were a few where I only had to let content authors know when they had reached the maximum number of child Items under an item. Due to this, I broke all of these values out into the following interfaces:

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsMaxItemsInFolderFeature
	{
		int MaximumNumberOfItemsInFolder { get; set; }
	}
}

The above interface is only for features which will let the content authors know when they are reached too many child Items.

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsWarnUserFeature
	{
		int NumberOfItemsToStartWarningUser { get; set; }
	}
}

The ITooManySubItemsWarnUserFeature is only for features that will warn users that they approaching too many child Items.

using System.Collections.Generic;

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsFeature : ITooManySubItemsMaxItemsInFolderFeature, ITooManySubItemsWarnUserFeature
	{
		List<string> TemplateIdsToIgnore { get; set; }
	}
}

The above interface is an amalgam of the two interfaces above, and also includes a collection of Template IDs to ignore during the almost or has too many child Items check — I had included this in case there are reasons for ignoring certain child Items with certain templates but am not using this in this blog post.

Due to the need of passing a bunch of stuff to the service which determines that an Item has almost or too many child Items, I created the following parameters object class:

using System.Collections.Generic;

using Sitecore.Data.Items;

namespace Foundation.Validation.Models.TooManySubItems
{
	public class TooManySubItemsServiceParameters
	{
		public Item Item { get; set; }

		public int NumberOfItemsToStartWarningUser { get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }

		public List<string> TemplateIdsToIgnore { get; set; } = new List<string>();
	}
}

I then created a factory object to create TooManySubItemsServiceParameters instances above. The following is an interface for this service:

using System.Collections.Generic;

using Sitecore.Data.Items;

using Foundation.Validation.Models.TooManySubItems;

namespace Foundation.Validation.Services.TooManySubItems.Factories
{
	public interface ITooManySubItemsServiceParametersFactory
	{
		TooManySubItemsServiceParameters CreateParameters(TooManySubItemsCommandServiceParameters parameters); // not used in this post; I will discuss in a future post
		
		TooManySubItemsServiceParameters CreateParameters(Item item, ITooManySubItemsFeature feature);

		TooManySubItemsServiceParameters CreateParameters(Item item, int numberOfItemsToStartWarningUser, int maximumNumberOfItemsInFolder, List<string> templateIdsToIgnore);
	}
}

Here’s the implementation of the interface above:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;

using Foundation.Validation.Models.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems.Factories;

namespace Feature.Validation.Services.TooManySubItems.Factories
{
	public class TooManySubItemsServiceParametersFactory : ITooManySubItemsServiceParametersFactory
	{
		// not used in this post; I will discuss in a future post
		public TooManySubItemsServiceParameters CreateParameters(TooManySubItemsCommandServiceParameters parameters) => CreateParameters(GetItem(parameters), 0, parameters.Feature.MaximumNumberOfItemsInFolder, parameters.TemplateIdsToIgnore);

		protected virtual Item GetItem(TooManySubItemsCommandServiceParameters parameters) => parameters?.Context.Items.FirstOrDefault();

		public TooManySubItemsServiceParameters CreateParameters(Item item, ITooManySubItemsFeature feature) => CreateParameters(item, feature.NumberOfItemsToStartWarningUser, feature.MaximumNumberOfItemsInFolder, feature.TemplateIdsToIgnore);

		public TooManySubItemsServiceParameters CreateParameters(Item item, int numberOfItemsToStartWarningUser, int maximumNumberOfItemsInFolder, List<string> templateIdsToIgnore)
		{ 
			return new TooManySubItemsServiceParameters
			{
				Item = item,
				NumberOfItemsToStartWarningUser = numberOfItemsToStartWarningUser,
				MaximumNumberOfItemsInFolder = maximumNumberOfItemsInFolder,
				TemplateIdsToIgnore = templateIdsToIgnore
			};
		}
	}
}

It’s basically just creating TooManySubItemsServiceParameters instances based on values passed.

We are ready to define the service which determines if an Item has almost or too many child Items. The following interface is for that service:

using Foundation.Validation.Models.TooManySubItems;

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsService
	{
		bool HasAlmostTooManySubItems(TooManySubItemsServiceParameters parameters);

		bool HasTooManySubItems(TooManySubItemsServiceParameters parameters);
	}
}

The following is the implementation of the interface defined above:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;

using Foundation.Validation.Models.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems;

namespace Feature.Validation.Services.TooManySubItems
{
	public class TooManySubItemsService : ITooManySubItemsService
	{
		private readonly TooManySubItemsSettings _settings; 

		public TooManySubItemsService(TooManySubItemsSettings settings)
		{
			_settings = settings;
		}

		public bool HasAlmostTooManySubItems(TooManySubItemsServiceParameters parameters)
		{
			int numberOfItemsToStartWarningUser = GetNumberOfItemsToStartWarningUser(parameters);
			int maximumNumberOfItemsInFolder = GetMaximumNumberOfItemsInFolder(parameters);
			if (parameters?.Item == null || numberOfItemsToStartWarningUser < 1 || maximumNumberOfItemsInFolder < 1)
			{
				return false;
			}

			IEnumerable<Item> items = GetItemsWithoutTemplateIds(parameters?.Item.Children, parameters?.TemplateIdsToIgnore);
			return items.Count() >= numberOfItemsToStartWarningUser && items.Count() < maximumNumberOfItemsInFolder;
		}

		public bool HasTooManySubItems(TooManySubItemsServiceParameters parameters)
		{
			int maximumNumberOfItemsInFolder = GetMaximumNumberOfItemsInFolder(parameters);
			if (parameters?.Item == null || maximumNumberOfItemsInFolder < 1)
			{
				return false;
			}

			IEnumerable<Item> items = GetItemsWithoutTemplateIds(parameters?.Item.Children, parameters?.TemplateIdsToIgnore);
			return items.Count() >= maximumNumberOfItemsInFolder;
		}

		protected virtual int GetNumberOfItemsToStartWarningUser(TooManySubItemsServiceParameters parameters)
		{
			if (parameters == null || _settings == null)
			{
				return 0;
			}

			if (parameters.NumberOfItemsToStartWarningUser > 0)
			{
				return parameters.NumberOfItemsToStartWarningUser;
			}

			return _settings.NumberOfItemsToStartWarningUser;
		}

		protected virtual int GetMaximumNumberOfItemsInFolder(TooManySubItemsServiceParameters parameters)
		{
			if (parameters == null || _settings == null)
			{
				return 0;
			}

			if (parameters.MaximumNumberOfItemsInFolder > 0)
			{
				return parameters.MaximumNumberOfItemsInFolder;
			}

			return _settings.MaximumNumberOfItemsInFolder;
		}

		protected virtual IEnumerable<Item> GetItemsWithoutTemplateIds(IEnumerable<Item> items, IEnumerable<string> templateIds)
		{
			if (items == null)
			{
				return Enumerable.Empty<Item>();
			}

			if(templateIds == null || !templateIds.Any())
			{
				return items;
			}

			return items.Where(item => templateIds.All(templateId => templateId != item.TemplateID.ToString())).ToList();
		}
	}
}

The HasAlmostTooManySubItems() method above determines if the passed Item has almost too many child items but not greater than or equal to the maximum number of child items as that check is governed by the HasTooManySubItems() method — it determines if the number of child items equals or exceeds the maximum number of child Items passed via the Parameters object; both of these methods only look at Items who do not have Template IDs which are in the TemplateIdsToIgnore list though we are not using this on this blog post.

Since we are modifying the appear of Item names in the Content Tree, I wanted to put markup for this in a nice place. I decided to use Handlebars.Net for this solution — in an earlier iteration of this project, I had this markup in a Sitecore configuration file all HTML encoded but it seemed like a cumbersome solution especially if the markup had to be changed so I went with this alternative approach instead as I had a good experience using Handlebars.Net over 2 years ago — but to my dismay, the current version of Handlebars.Net did not come with its OOTB ViewEngineFileSystem any longer as older versions had, so I had to create one myself. This required me to create the following service class for dealing with files but I’m ultimately just delegating to methods on Sitecore.IO.FileUtil (this lives in Sitecore.Kernel.dll) through this service:

namespace Foundation.Kernel.Services.IO
{
	public interface IFileUtilService
	{
		bool Exists(string path);

		string MakePath(string part1, string part2);

		string ReadFromFile(string path);
	}
}

The following implements the interface above but just delegates to methods on Sitecore.IO.FileUtil:

using Sitecore.IO;

namespace Foundation.Kernel.Services.IO
{
	public class FileUtilService : IFileUtilService
	{
		public bool Exists(string path) => FileUtil.Exists(path);

		public string MakePath(string part1, string part2) => FileUtil.MakePath(part1, part2);

		public string ReadFromFile(string path) => FileUtil.ReadFromFile(path);
	}
}

I then created a subclass of HandlebarsDotNet.ViewEngineFileSystem; this adapter class talks to the file system for HandlebarsDotNet:

using HandlebarsDotNet;

using Foundation.Kernel.Services.IO;

namespace Feature.Token.Services
{
	public class HandlebarsViewEngineFileSystem : ViewEngineFileSystem
	{
		private readonly IFileUtilService _fileUtilService;

		public HandlebarsViewEngineFileSystem(IFileUtilService fileUtilService)
		{
			_fileUtilService = fileUtilService;
		}

		public override bool FileExists(string filePath) => _fileUtilService.Exists(filePath);

		public override string GetFileContent(string filename) => _fileUtilService.ReadFromFile(filename);

		protected override string CombinePath(string dir, string otherFileName) => _fileUtilService.MakePath(dir, otherFileName);
	}
}

I then wrapped HandlebarsDotNet’s main static class in a service class so I can inject things together using DI. The following interface is for this service class:

using System;
using System.IO;

using HandlebarsDotNet;

namespace Feature.Token.Services
{
	public interface IHandlebarsService
	{
		Action<TextWriter, object> Compile(TextReader template);

		Func<object, string> Compile(string template);

		Func<object, string> CompileView(string templatePath);

		void RegisterHelper(string helperName, HandlebarsHelper helperFunction);

		void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction);

		void RegisterTemplate(string templateName, Action<TextWriter, object> template);

		void RegisterTemplate(string templateName, string template);
	}
}

We now need to implement the interface above:

using System;
using System.IO;

using HandlebarsDotNet;

namespace Feature.Token.Services
{
	public class HandlebarsService : IHandlebarsService
	{
		private ViewEngineFileSystem _viewEngineFileSystem;

		public HandlebarsService(ViewEngineFileSystem viewEngineFileSystem)
		{
			_viewEngineFileSystem = viewEngineFileSystem;
		}

		private readonly object _locker = new object();
		private IHandlebars _instance;
		private IHandlebars Instance
		{
			get
			{
				if(_instance  == null)
				{
					lock(_locker)
					{
						_instance = Create();
					}
				}

				return _instance;
			}
		}

		private IHandlebars Create(HandlebarsConfiguration configuration = null)
		{
			IHandlebars handlebars = Handlebars.Create(configuration);
			handlebars.Configuration.FileSystem = _viewEngineFileSystem;
			return handlebars;
		}

		public Action<TextWriter, object> Compile(TextReader template) => Instance.Compile(template);

		public Func<object, string> Compile(string template) => Instance.Compile(template);

		public Func<object, string> CompileView(string templatePath) => Instance.CompileView(templatePath);

		public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) => Instance.RegisterHelper(helperName, helperFunction);

		public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) => Instance.RegisterHelper(helperName, helperFunction);

		public void RegisterTemplate(string templateName, Action<TextWriter, object> template) => Instance.RegisterTemplate(templateName, template);

		public void RegisterTemplate(string templateName, string template) => Instance.RegisterTemplate(templateName, template);
	}
}

I’m injecting the ViewEngineFileSystem service — this is an instance of the HandlebarsViewEngineFileSystem above but I stick it into the IoC contain with a service type of ViewEngineFileSystem — into this class so we can create a new IHandlebars instance using Handlebars’s Create() method, and then it gets stored in an internal Singleton in the class.

Next, we need a service to replace tokens (no, not Sitecore tokens such as $name, $id, etc) but tokens in handlebars template strings or files:

using System.Collections.Generic;

namespace Foundation.Token.Services
{
	public interface ITemplatedTokenReplacer
	{
		string ReplaceTokens(string input, IDictionary<string, string> tokens);

		string ReplaceTokens(string templateSource, object tokens);

		string ReplaceTokensFromView(string viewPath, IDictionary<string, string> tokens);

		string ReplaceTokensFromView(string viewPath, object tokens);

		void ClearAllCaches();

		void ClearTemplateCache();

		void ClearViewsCache();
	}
}

The implementation of the interface above will need to “cache” compiled Handlebars.Net templates as the documentation calls out that compiling their templates is an expensive operation and recommend caching these compilations so I am using the following interface to create services which cache things:

namespace Foundation.Kernel.Services.Cache
{
    public interface ICacher<TKey, TObject>
    {
        void Add(TKey key, TObject obj);

        bool ContainsKey(TKey key);

        TObject Get(TKey key);

        void Clear();
    }
}

Classes which implement this base class below will be caching their things in a ConcurrentDictionary:

using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Foundation.Kernel.Services.Cache
{
    public abstract class Cacher<TKey, TObject>
    {
        private IDictionary<TKey, TObject> _cache;

        protected Cacher()
        {
            _cache = CreateCache();
        }

        protected virtual IDictionary<TKey, TObject> CreateCache() => new ConcurrentDictionary<TKey, TObject>();

        public void Add(TKey key, TObject obj) => _cache[key] = obj;

        public TObject Get(TKey key) => ContainsKey(key) ? _cache[key] : default(TObject);

        public bool ContainsKey(TKey key) => _cache.ContainsKey(key);

        public void Clear() => _cache.Clear();
    }
}

We will be caching compilations of Handlebars.Net template strings:

using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public interface ICompiledTemplateCache : ICacher<string, Func<object, string>>
	{
	}
}
using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public class CompiledTemplateCache : Cacher<string, Func<object, string>>, ICompiledTemplateCache
	{
	}
}

We will also be caching compilations of Handlebars.Net template strings sourced from files:

using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public interface ICompiledViewTemplateCache : ICacher<string, Func<object, string>>
	{
	}
}
using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public class CompiledViewTemplateCache : Cacher<string, Func<object, string>>, ICompiledViewTemplateCache
	{

	}
}

Finally, we can talk about the implemenation of ITemplatedTokenReplacer 😉 :

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;

using HandlebarsDotNet;

using Foundation.Token.Services;

using Feature.Token.Services.Cachers;

namespace Feature.Token.Services
{
	public class HandlebarsTokenReplacer : ITemplatedTokenReplacer
	{
		private readonly IHandlebarsService _handlebarsService;

		private readonly ICompiledTemplateCache _compiledTemplateCache;
		private readonly ICompiledViewTemplateCache _compiledViewTemplateCache;

		public HandlebarsTokenReplacer(IHandlebarsService handlebarsService, ICompiledTemplateCache compiledTemplateCache, ICompiledViewTemplateCache compiledViewTemplateCache)
		{
			_handlebarsService = handlebarsService;
			_compiledTemplateCache = compiledTemplateCache;
			_compiledViewTemplateCache = compiledViewTemplateCache;
		}

		public string ReplaceTokens(string input, IDictionary<string, string> tokens) => ReplaceTokens(input, (object)tokens);

		public string ReplaceTokens(string templateSource, object tokens)
		{
			var compiled = GetCompiledTemplateFromCache(templateSource);
			if (compiled == null)
			{
				compiled = Compile(templateSource);
				AddCompiledTemplateToCache(templateSource, compiled);
			}

			return ReplaceTokens(compiled, tokens);
		}

		protected virtual Func<object, string> GetCompiledTemplateFromCache(string templateSource) => _compiledTemplateCache.Get(templateSource);

		protected virtual Func<object, string> Compile(string viewPath) => _handlebarsService.Compile(viewPath);

		protected virtual void AddCompiledTemplateToCache(string templateSource, Func<object, string> compiled) => _compiledTemplateCache.Add(templateSource, compiled);

		protected virtual string ReplaceTokens(Func<object, string> template, object tokens)
		{
			if(template == null)
			{
				return string.Empty;
			}

			return template(tokens);
		}

		public string ReplaceTokensFromView(string viewPath, IDictionary<string, string> tokens) => ReplaceTokensFromView(viewPath, (object)tokens);

		public string ReplaceTokensFromView(string viewPath, object tokens)
		{
			var compiled = GetCompiledViewTemplateFromCache(viewPath);
			if(compiled == null)
			{
				compiled = CompileView(viewPath);
				AddCompiledViewTemplateToCache(viewPath, compiled);
			}

			return ReplaceTokens(compiled, tokens);
		}

		protected virtual Func<object, string> GetCompiledViewTemplateFromCache(string viewPath) => _compiledViewTemplateCache.Get(viewPath);

		protected virtual Func<object, string> CompileView(string viewPath) => _handlebarsService.CompileView(viewPath);

		protected virtual void AddCompiledViewTemplateToCache(string viewPath, Func<object, string> compiled) => _compiledViewTemplateCache.Add(viewPath, compiled);

		protected virtual Action<TextWriter, object> Compile(TextReader template) => _handlebarsService.Compile(template);

		protected virtual void RegisterHelper(string helperName, HandlebarsHelper helperFunction) => _handlebarsService.RegisterHelper(helperName, helperFunction);

		protected virtual void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) => _handlebarsService.RegisterHelper(helperName, helperFunction);

		protected virtual void RegisterTemplate(string templateName, Action<TextWriter, object> template) => _handlebarsService.RegisterTemplate(templateName, template);

		protected virtual void RegisterTemplate(string templateName, string template) => _handlebarsService.RegisterTemplate(templateName, template);

		public void ClearAllCaches()
		{
			ClearTemplateCache();
			ClearViewsCache();
		}

		public void ClearTemplateCache() => _compiledTemplateCache.Clear();

		public void ClearViewsCache() => _compiledViewTemplateCache.Clear();
	}
}

Ultimately, the class above delegates calls to IHandlebarsService for the most part. Lookups are done to replace tokens in templates before compiling to see if these compilations were cached. If there were not cached, they are compiled and then cached. The resulting token replacement strings are returned for both template string replacements, and those sourced from template files.

I also have methods to clear the two caches.

Next, I created a custom FileWatcher to clear the Compile Views cache of Handlebars.Net template file compliations. Since FileWatchers are defined in the Web.config, I decided to create a service class which will control the logic which my cusotm FileWatcher will completely delegate its functionality to. The following interface is for that service class:

namespace Feature.Token.Services
{
	public interface IHandlebarsViewFilesWatcherService
	{
		void Created(string fullPath);

		void Deleted(string filePath);

		void Renamed(string filePath, string oldFilePath);
	}
}

The service class will take in a service Config Object defined in a Sitecore configuration file further below:

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;

namespace Feature.Token.Models.FileWatchers
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/feature/token/handlebarsViewFilesWatcherServiceSettings", Lifetime = Lifetime.Singleton)]
	public class HandlebarsViewFilesWatcherServiceSettings
	{
		public string CreatedLogInfoMessageFormat { get; set; }

		public string DeletedLogInfoMessageFormat { get; set; }

		public string RenamedLogInfoMessageFormat { get; set; }

		public string ClearViewsCacheLogInfoMessage { get; set; }
	}
}

Here is the implemenation of the interface above:

using Sitecore.Abstractions;

using Foundation.Token.Services;

using Feature.Token.Models.FileWatchers;

namespace Feature.Token.Services
{
	public class HandlebarsViewFilesWatcherService : IHandlebarsViewFilesWatcherService
	{
		private readonly HandlebarsViewFilesWatcherServiceSettings _settings;
		private readonly ITemplatedTokenReplacer _replacer;
		private readonly BaseLog _log;

		public HandlebarsViewFilesWatcherService(HandlebarsViewFilesWatcherServiceSettings settings, ITemplatedTokenReplacer replacer, BaseLog log)
		{
			_settings = settings;
			_replacer = replacer;
			_log = log;
		}

		public void Created(string fullPath)
		{
			LogInfo(string.Format(_settings.CreatedLogInfoMessageFormat, fullPath), this);
			ClearViewsCacheLogInfo();
		}

		public void Deleted(string filePath)
		{
			LogInfo(string.Format(_settings.DeletedLogInfoMessageFormat, filePath), this);
			ClearViewsCacheLogInfo();
		}

		public void Renamed(string filePath, string oldFilePath)
		{
			LogInfo(string.Format(_settings.RenamedLogInfoMessageFormat, filePath, oldFilePath), this);
			ClearViewsCacheLogInfo();
		}

		protected virtual void LogInfo(string message, object owner) => _log.Info(message, owner);

		protected virtual void ClearViewsCacheLogInfo()
		{
			ClearViewsCache();
			LogInfo(_settings.ClearViewsCacheLogInfoMessage, this);
		}

		protected virtual void ClearViewsCache() => _replacer.ClearViewsCache();
	}
}

If a Handlebars.Net template file is modified, added, renamed or deleted, the Handlebars.Net Views Cache is cleared so that changes will be reflected in the Content Tree immediately.

I then created the “real” FileWatcher class; this is the class which will be registered in my Web.config to make this work:

using Sitecore.DependencyInjection;
using Sitecore.IO;

using Feature.Token.Services;

namespace Feature.Token.FileWatchers
{
	public class HandlebarsViewFilesWatcher : FileWatcher
	{
		public HandlebarsViewFilesWatcher()
			: base("watchers/handlebars")
		{
		}

		protected override void Created(string fullPath) => GetService<IHandlebarsViewFilesWatcherService>().Created(fullPath);

		protected override void Deleted(string filePath) => GetService<IHandlebarsViewFilesWatcherService>().Deleted(filePath);

		protected override void Renamed(string filePath, string oldFilePath) => GetService<IHandlebarsViewFilesWatcherService>().Renamed(filePath, oldFilePath);

		protected TService GetService<TService>() where TService : class => ServiceLocator.ServiceProvider.GetService(typeof(TService)) as TService;
	}
}

With that in place, I defined it in my Web.config here:


  <!-- More stuff here -->
  
 <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
	
	  <!-- More stuff here -->
	  
	  <!-- Handlerbars view files watcher -->
	  <add type="Feature.Token.FileWatchers.HandlebarsViewFilesWatcher, Feature.Token" name="HandlebarsViewFilesWatcher"/>  

	    <!-- More stuff here -->
	</modules>
</system.webServer>

<!-- More stuff here -->

Now, we need a custom Sitecore patch configuration file to wire-up all of the services defined above for token replacement along with the settings object for the FileWatcher service:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore>
	  <moduleSettings>
		  <feature>
			  <token>
				  <handlebarsViewFilesWatcherServiceSettings type="Feature.Token.Models.FileWatchers.HandlebarsViewFilesWatcherServiceSettings, Feature.Token" singleInstance="true">
					  <CreatedLogInfoMessageFormat>Handlebars view file created or modified: {0}.</CreatedLogInfoMessageFormat>
					  <DeletedLogInfoMessageFormat>Handlebars view file deleted: {0}.</DeletedLogInfoMessageFormat>
					  <RenamedLogInfoMessageFormat>Handlebars view file renamed: {0}. Old path: {1}</RenamedLogInfoMessageFormat>
					  <ClearViewsCacheLogInfoMessage>Token Views Cache Cleared.</ClearViewsCacheLogInfoMessage>
				  </handlebarsViewFilesWatcherServiceSettings>
			  </token>	  
		  </feature>
	  </moduleSettings>	  
	  <services>
		  <register serviceType="Feature.Token.Services.IHandlebarsService, Feature.Token"
					implementationType="Feature.Token.Services.HandlebarsService, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Foundation.Token.Services.ITemplatedTokenReplacer, Foundation.Token"
					implementationType="Feature.Token.Services.HandlebarsTokenReplacer, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="HandlebarsDotNet.ViewEngineFileSystem, Handlebars"
					implementationType="Feature.Token.Services.HandlebarsViewEngineFileSystem, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Feature.Token.Services.IHandlebarsViewFilesWatcherService, Feature.Token"
					implementationType="Feature.Token.Services.HandlebarsViewFilesWatcherService, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Feature.Token.Services.Cachers.ICompiledTemplateCache, Feature.Token"
					implementationType="Feature.Token.Services.Cachers.CompiledTemplateCache, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Feature.Token.Services.Cachers.ICompiledViewTemplateCache, Feature.Token"
					implementationType="Feature.Token.Services.Cachers.CompiledViewTemplateCache, Feature.Token"
					lifetime="Singleton" />
	  </services>
	  <watchers>
		  <handlebars>
			  <folder>/Views</folder>
			  <filter>*.hbs</filter>
		  </handlebars>
	  </watchers>
  </sitecore>
</configuration>

Are you still with me? 😉

Now, let’s define some other service classes which just wrap static method calls on things in the Sitecore Api; I wanted to source these all from the Sitecore IoC container, and not all things in the Sitecore Api are in the IoC container so I’ll just stick them in there. I’m not going to explain too much about these except that they are used in subsequent classes further down:

This interface with its implemenation wraps calls on the Sitecore.Data.ID class:

using System;

using Sitecore.Data;

namespace Foundation.Kernel.Services.UniqueIdentifier
{
    public interface IIDService
    {
        bool IsNullOrEmpty(ID id);

        bool TryParse(string value, out ID id);

        bool TryParse(object value, out ID id);

        ID Parse(string value);

        ID Parse(object value);

        ID CreateNewID();

        ID CreateNewID(string id); // not used in this post

        ID CreateNewID(Guid id);  // not used in this post
    }
}
using System;

using Sitecore.Data;

using Foundation.Kernel.Services.UniqueIdentifier.Factories;

namespace Foundation.Kernel.Services.UniqueIdentifier
{
    public class IDService : IIDService
    {
        private readonly IIDFactory _idFactory;   // not used in this post

        public IDService(IIDFactory idFactory)
        {
            _idFactory = idFactory;   // not used in this post
        }

        public bool IsNullOrEmpty(ID id) => ID.IsNullOrEmpty(id);

        public bool TryParse(string value, out ID id) => ID.TryParse(value, out id);

        public bool TryParse(object value, out ID id) => ID.TryParse(value, out id);

        public ID Parse(string value) => ID.Parse(value);

        public ID Parse(object value) => ID.Parse(value);

        public ID CreateNewID() => _idFactory.CreateNewID();

        public ID CreateNewID(string id) => _idFactory.CreateNewID(id);   // not used in this post

        public ID CreateNewID(Guid id) => _idFactory.CreateNewID(id);   // not used in this post
    }
}

This interface with its implemenation creates a FieldList instance from a FieldCollection instance along with pointers to methods for changing field values being placed from the FieldCollection into a new FieldList instance (this will be used further down in this post when modifying how Items appear in the content tree):

using System;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;

namespace Foundation.Kernel.Services.Fields.Factories
{
	public interface IFieldsFactory
	{
		FieldList CreateNewFieldList(FieldCollection fieldCollection, Func<Field, ID> idProvider, Func<Field, string> valueProvider);

		FieldList CreateNewFieldList();
	}
}
using System;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;

namespace Foundation.Kernel.Services.Fields.Factories
{
	public class FieldsFactory : IFieldsFactory
	{
		public FieldList CreateNewFieldList(FieldCollection fieldCollection, Func<Field, ID> idProvider, Func<Field, string> valueProvider)
		{
			FieldList fieldList = CreateNewFieldList();
			foreach (Field field in fieldCollection)
			{
				ID id = idProvider(field);
				string value = valueProvider(field);
				fieldList.Add(id, value);
			}

			return fieldList;
		}

		public FieldList CreateNewFieldList() => new FieldList();
	}
}

This interface and its implementation wrap calls to Sitecore.Globalization.Translate:

namespace Foundation.Kernel.Services.Globalization
{
	public interface ITranslateService
	{
		string TranslateText(string key);
	}
}
namespace Foundation.Kernel.Services.Globalization
{
	public class TranslateService : ITranslateService
	{
		public string TranslateText(string key) => Sitecore.Globalization.Translate.Text(key);
	}
}

This interface and its implementation wrap calls to Sitecore.Resources.Images; this will be used when dealing with Icon paths:

using Sitecore.Web.UI;

namespace Foundation.Kernel.Services.Media.Resources
{
	public interface IImagesService
	{
		string GetThemedImageSource(string image);

		string GetThemedImageSource(string image, ImageDimension dimension);
	}
}
using Sitecore.Resources;
using Sitecore.Web.UI;

namespace Foundation.Kernel.Services.Media.Resources
{
	public class ImagesService : IImagesService
	{
		public string GetThemedImageSource(string image) => Images.GetThemedImageSource(image);

		public string GetThemedImageSource(string image, ImageDimension dimension) => Images.GetThemedImageSource(image, dimension);
	}
}

This interface and its implementation will create instances of the Item class but with modified values sourced from another Item instance — this is used when changing the Item name for display in the content tree, and these items are “temporary”:

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

namespace Foundation.Kernel.Services.Items.Factories
{
	public interface IItemFactory
	{
		Item CreateAlteredItem(Item item, string itemName, string iconPath, ID templateId, ID branchId, Language language, Version version);

		Item CreateNewItem(ID itemID, ItemData data, Database database, bool isTemporary);
		
		ItemDefinition CreateItemDefinition(ID itemID, string itemName, ID templateID, ID branchId);

		ItemData CreateItemData(ItemDefinition definition, Language language, Version version, FieldList fields);
	}
}
using System;

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

using Foundation.Kernel.Services.Globalization;
using Foundation.Kernel.Services.Media.Resources;
using Foundation.Kernel.Services.UniqueIdentifier;
using Foundation.Kernel.Services.Fields.Factories;

namespace Foundation.Kernel.Services.Items.Factories
{
	public class ItemFactory : IItemFactory
	{
		private readonly IIDService _idService;
		private readonly IFieldsFactory _fieldsFactory;
		private readonly ITranslateService _translateService;
		private readonly IImagesService _imagesService;

		public ItemFactory(IIDService idService, IFieldsFactory fieldsFactory, ITranslateService translateService, IImagesService imagesService)
		{
			_idService = idService;
			_fieldsFactory = fieldsFactory;
			_translateService = translateService;
			_imagesService = imagesService;
		}

		public Item CreateAlteredItem(Item item, string itemName, string iconPath, ID templateId, ID branchId, Language language, Sitecore.Data.Version version)
		{
			if (item == null || IsNullOrEmpty(templateId))
			{
				return null;
			}

			ItemDefinition definition = CreateItemDefinition(item.ID, itemName, templateId, branchId);
			if (definition == null)
			{
				return null;
			}

			FieldList fields = CreateNewFieldList(item?.Fields, field => field.ID, field => GetAlteredItemFieldValue(field, itemName, iconPath));
			if (fields == null)
			{
				return null;
			}

			return CreateNewItem(item.ID, CreateItemData(definition, language, version, fields), item.Database, true);
		}

		protected virtual bool IsNullOrEmpty(ID id) => _idService.IsNullOrEmpty(id);

		protected virtual string GetAlteredItemFieldValue(Field field, string itemName, string iconPath)
		{
			string value = field.Value;
			if (field.ID == GetDisplayNameFieldId() && !string.IsNullOrWhiteSpace(itemName))
			{
				value = TranslateText(itemName);
			}
			else if (field.ID == GetIconFieldId() && !string.IsNullOrWhiteSpace(iconPath))
			{
				value = GetThemedImageSource(iconPath);
			}

			return value;
		}

		protected virtual ID GetDisplayNameFieldId() => FieldIDs.DisplayName;

		protected virtual ID GetIconFieldId() => FieldIDs.Icon;

		protected virtual string TranslateText(string key) => _translateService.TranslateText(key);

		protected virtual string GetThemedImageSource(string image) => _imagesService.GetThemedImageSource(image);

		protected virtual FieldList CreateNewFieldList(FieldCollection fieldCollection, Func<Field, ID> idProvider, Func<Field, string> valueProvider) => _fieldsFactory.CreateNewFieldList(fieldCollection, idProvider, valueProvider);

		public Item CreateNewItem(ID itemID, ItemData data, Database database, bool isTemporary)
		{
			return new Item(itemID, data, database)
			{
				RuntimeSettings = { Temporary = isTemporary }
			};
		}

		public ItemDefinition CreateItemDefinition(ID itemID, string itemName, ID templateID, ID branchId) => new ItemDefinition(itemID, itemName, templateID, branchId);

		public ItemData CreateItemData(ItemDefinition definition, Language language, Sitecore.Data.Version version, FieldList fields) => new ItemData(definition, language, version, fields);
	}
}

This interface and its implementation wrap calls to the Sitecore.Data.Version class:

using Sitecore.Data;

namespace Foundation.Kernel.Services.Versoning
{
	public interface IVersionService
	{
		Version GetLatestVersion();
	}
}
using Sitecore.Data;

namespace Foundation.Kernel.Services.Versoning
{
	public class VersionService : IVersionService
	{
		public Version GetLatestVersion() => Version.Latest;
	}
}

This interface and its implementation wrap calls to the Sitecore.Globalization.Language class:

using Sitecore;
using Sitecore.Globalization;

namespace Foundation.Kernel.Services.Globalization
{
    public interface ILanguageService
    {
        bool TryParse(string name, out Language result);

        Language Parse(string name);

        Language GetContextLanguage();

		Language GetLanguageInvariant();
	}
}
using Sitecore;
using Sitecore.Globalization;

namespace Foundation.Kernel.Services.Globalization
{
    public class LanguageService : ILanguageService
    {
        public bool TryParse(string name, out Language result) => Language.TryParse(name, out result);

        public Language Parse(string name) => Language.Parse(name);

        public Language GetContextLanguage() => Context.Language;

		public Language GetLanguageInvariant() => Language.Invariant;
	}
}

Let’s tie everything together in the following implentation of the DataViewChildItemsProcessor class defined towards the top of this post:

using System;
using System.Collections.Generic;

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

using Foundation.Token.Services;

using Foundation.Kernel.Pipelines.DataViewChildItems;
using Foundation.Kernel.Services.FeatureToggle;
using Foundation.Kernel.Services.Items.Factories;
using Foundation.Kernel.Services.Versoning;
using Foundation.Kernel.Services.Globalization;

using Foundation.Validation.Services.TooManySubItems.Factories;
using Foundation.Validation.Services.TooManySubItems;
using Foundation.Validation.Models.TooManySubItems;

namespace Feature.ContentEditor.Pipelines.DataViewChildItems
{
	public class MediaItemTooManyChildItems : DataViewChildItemsProcessor, IMediaItemTooManyChildItems
	{
		private readonly ITooManySubItemsFeatureToggleService _tooManySubItemsFeatureToggleService;
		private readonly ITooManySubItemsServiceParametersFactory _tooManySubItemsServiceParametersFactory;
		private readonly ITooManySubItemsService _tooManySubItemsService;
		private readonly ITemplatedTokenReplacer _templatedTokenReplacer;
		private readonly IItemFactory _itemFactory;
		private readonly IVersionService _versionService;
		private readonly ILanguageService _languageService;

		private string MediaLibraryBasePath { get; set; }

		public List<string> TemplateIdsToIgnore { get; set; } = new List<string>();

		public int NumberOfItemsToStartWarningUser { get; set; }

		private string AlmostAtMaxiumIconPath { get; set; }

		private string AlmostAtMaxiumMessageViewPath { get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }

		private string AtMaxiumIconPath { get; set; }

		private string AtMaxiumMessageViewPath { get; set; }

		public MediaItemTooManyChildItems(IFeatureToggleService featureToggleService, ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService, ITemplatedTokenReplacer templatedTokenReplacer,  IItemFactory itemFactory, IVersionService versionService, ILanguageService languageService)
			: base(featureToggleService)
		{
			_tooManySubItemsFeatureToggleService = tooManySubItemsFeatureToggleService;
			_tooManySubItemsServiceParametersFactory = tooManySubItemsServiceParametersFactory;
			_tooManySubItemsService = tooManySubItemsService;
			_templatedTokenReplacer = templatedTokenReplacer;
			_itemFactory = itemFactory;
			_versionService = versionService;
			_languageService = languageService;
		}

		protected override bool ShouldExecute(DataViewChildItemsArgs args)
		{
			return IsMediaLibraryBasePathSet()
				&& HasRequiredParameters(args)
				&& IsInMediaLibrary(args?.Parent);
		}

		protected override bool IsEnabled() => _tooManySubItemsFeatureToggleService.IsEnabled(this);

		protected virtual bool IsMediaLibraryBasePathSet() => !string.IsNullOrWhiteSpace(GetMediaLibraryBasePath());

		protected virtual bool HasRequiredParameters(DataViewChildItemsArgs args) => args?.Parent != null && args?.Children != null;

		protected virtual bool HasAlmostTooManySubItems(TooManySubItemsServiceParameters parameters) => _tooManySubItemsService.HasAlmostTooManySubItems(parameters);

		protected virtual bool HasTooManySubItems(TooManySubItemsServiceParameters parameters) => _tooManySubItemsService.HasTooManySubItems(parameters);

		protected override void GetChildItems(DataViewChildItemsArgs args)
		{
			if (!HasRequiredParameters(args))
			{
				return;
			}

			for (int i = args.Children.Count - 1; i >= 0; i--)
			{
				Item child = args.Children[i];
				if(!IsInMediaLibrary(child) || IsMediaLibraryRootItem(child))
				{
					continue;
				}

				TooManySubItemsServiceParameters parameters = CreateParameters(child, this);
				bool hasAlmostTooManySubItems = HasAlmostTooManySubItems(parameters);
				bool hasTooManySubItems = HasTooManySubItems(parameters);
				if (!hasAlmostTooManySubItems && !hasTooManySubItems)
				{
					continue;
				}

				string itemName = string.Empty;
				string iconPath = string.Empty;
				if (hasAlmostTooManySubItems)
				{
					itemName = GetAlmostAtMaxiumMessage(child, GetAlmostAtMaxiumIconPath());
				}
				else if (hasTooManySubItems)
				{
					itemName = GetAtMaxiumMessage(child, GetAtMaxiumIconPath());
				}

				Item alteredItem = GetAlteredItem(child, itemName, iconPath, child.TemplateID);
				if(alteredItem == null)
				{
					return;
				}

				args.Children.RemoveAt(i);
				args.Children.Insert(i, alteredItem);
			}
		}

		protected virtual bool IsInMediaLibrary(Item item)
		{
			string mediaLibraryBasePath = GetMediaLibraryBasePath();
			if (item == null || string.IsNullOrWhiteSpace(mediaLibraryBasePath))
			{
				return false;
			}

			return item.Paths.FullPath.StartsWith(mediaLibraryBasePath, StringComparison.OrdinalIgnoreCase);
		}

		protected virtual bool IsMediaLibraryRootItem(Item item)
		{
			string mediaLibraryBasePath = GetMediaLibraryBasePath();
			if (item == null || string.IsNullOrWhiteSpace(mediaLibraryBasePath))
			{
				return false;
			}

			return item.Paths.FullPath.Equals(mediaLibraryBasePath, StringComparison.OrdinalIgnoreCase);
		}

		protected virtual string GetMediaLibraryBasePath() => MediaLibraryBasePath;

		protected virtual TooManySubItemsServiceParameters CreateParameters(Item item, ITooManySubItemsFeature feature) => _tooManySubItemsServiceParametersFactory.CreateParameters(item, feature);

		protected virtual string GetAlmostAtMaxiumIconPath() => AlmostAtMaxiumIconPath;

		protected virtual string GetAtMaxiumIconPath() => AtMaxiumIconPath;

		protected virtual string GetAlmostAtMaxiumMessage(Item item, string iconPath) => ReplaceTokensFromView(GetAlmostAtMaxiumMessageViewPath(), new { ItemName = item.Name, IconPath = iconPath, ChildItemsCount = GetChildItemsCount(item) });

		protected virtual string GetAlmostAtMaxiumMessageViewPath() => AlmostAtMaxiumMessageViewPath;

		protected virtual string GetAtMaxiumMessage(Item item, string iconPath) => ReplaceTokensFromView(GetAtMaxiumMessageViewPath(), new { ItemName = item.Name, IconPath = iconPath, ChildItemsCount = GetChildItemsCount(item) });

		protected virtual string GetAtMaxiumMessageViewPath() => AtMaxiumMessageViewPath;

		protected virtual int GetChildItemsCount(Item item) => item.Children.Count;

		protected virtual string ReplaceTokensFromView(string viewPath, object tokens) => _templatedTokenReplacer.ReplaceTokensFromView(viewPath, tokens);

		protected virtual Item GetAlteredItem(Item item, string itemName, string iconPath, ID templateId) => CreateAlteredItem(item, itemName, iconPath, templateId, ID.Null, GetLanguageInvariant(), GetLatestVersion());

		protected virtual Item CreateAlteredItem(Item item, string itemName, string iconPath, ID templateId, ID branchId, Language language, Sitecore.Data.Version version) => _itemFactory.CreateAlteredItem(item, itemName, iconPath, templateId, branchId, language, version);

		protected virtual Language GetLanguageInvariant() => _languageService.GetLanguageInvariant();

		protected virtual Sitecore.Data.Version GetLatestVersion() => _versionService.GetLatestVersion();
	}
}

I’m not going to explain this entire class as there’s a lot of service delegation happening here but the gist is we will only create an “altered” item when the item in the Children collection on the DataViewChildItemsArgs instance is in the media library, and has almost, or does have too many child items. We then replace the child item in the DataViewChildItemsArgs Children collection with the “altered” item whose name includes the original item’s name and the icon associated with “almost has too many child items” or “has too many child items”; these are retrieved from their respective methods.

I then registered the pipeline processor above in the Sitecore configuration patch file below:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
	<sitecore>
		<pipelines>
			<group name="masterDataView">
				<pipelines>
					<dataviewChildItems>
						<processor type="Feature.ContentEditor.Pipelines.DataViewChildItems.IMediaItemTooManyChildItems, Feature.ContentEditor" resolve="true">
							<Enabled>true</Enabled>
							<AbortPipelineAfterExecution>false</AbortPipelineAfterExecution>
							<MediaLibraryBasePath>/sitecore/media library/</MediaLibraryBasePath>
							<AlmostAtMaxiumIconPath>-/icon/Applicationsv2/16x16/sign_warning.png</AlmostAtMaxiumIconPath>
							<AtMaxiumIconPath>-/icon/Apps/16x16/Stop.png</AtMaxiumIconPath>
							<AlmostAtMaxiumMessageViewPath>/Views/DataViewChildItems/MediaItemTooManyChildItems/AlmostAtMaxiumMessageTemplate.hbs</AlmostAtMaxiumMessageViewPath>
							<AtMaxiumMessageViewPath>/Views/DataViewChildItems/MediaItemTooManyChildItems/AtMaxiumMessageTemplate.hbs</AtMaxiumMessageViewPath>
							<!-- By setting these, you can override the default values set for the entire feature set
										<NumberOfItemsToStartWarningUser>95</NumberOfItemsToStartWarningUser>
										<MaximumNumberOfItemsInFolder>100</MaximumNumberOfItemsInFolder>
							-->
						</processor>
					</dataviewChildItems>
				</pipelines>
			</group>
		</pipelines>
		<services>
			<register
				serviceType="Feature.ContentEditor.Pipelines.DataViewChildItems.IMediaItemTooManyChildItems, Feature.ContentEditor"
				implementationType="Feature.ContentEditor.Pipelines.DataViewChildItems.MediaItemTooManyChildItems, Feature.ContentEditor"
				lifetime="Singleton" />
		</services>
	</sitecore>
</configuration>

This is what’s inside of /Views/DataViewChildItems/MediaItemTooManyChildItems/AlmostAtMaxiumMessageTemplate.hbs; this a Handlebars.Net template file:

{{ItemName}}&nbsp;<img src="{{IconPath}}" />&nbsp;<span style="color: #999900;">({{ChildItemsCount}} child items)</span>

This is what’s inside of /Views/DataViewChildItems/MediaItemTooManyChildItems/AtMaxiumMessageTemplate.hbs; this a Handlebars.Net template file:

{{ItemName}}&nbsp;<img src="{{IconPath}}" />&nbsp;<span style="color: red;">({{ChildItemsCount}} child items)</span>

Let’s have a look at what this does. The following is what the tree in my Media Library looks like with this turned on:

One thing I want to point out is that I have not rigorously tested this solution so use at your own risk.

Also, after discussing this solution with Sitecore MVPs Akshay Sura and Kamruz Jaman, Akshay brought up a great idea of having a custom MasterDataView being driving by the Sitecore Rules Engine. I thought it was a great idea, and maybe some day in the future I might blog about this but, honestly, I think this might be something that you, my dear reader, should have a stab at, and give back to the community if you find such a solution.

Until next time, keep on Sitecoring.

Advertisement

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.