Home » Tokens

Category Archives: Tokens

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.

Expand New Tokens Added to Standard Values on All Items Using Its Template in Sitecore

If you have read some of my older posts, you probably know by now how much I love writing code that expands tokens on Items in Sitecore, and decided to build another solution that expands new tokens added to Standard Values Items of Templates — out of the box, these aren’t expanded on preexisting Items that use the Template of the Standard Values Item, and end up making their way in fields on those preexisting Items (for an alternative solution, check out this older post I wrote some time ago).

In the following solution — this solution is primarily composed of a custom pipeline — tokens that are added to fields on the Standard Values Item will be expanded on all Items that use the Template of the Standard Values Item after the Standard Values Item is saved in the Sitecore client (I hook into the <saveUI> pipeline for this action on save).

We first need a class whose instance serves as the custom pipeline’s argument object:

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

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class ExpandNewTokensOnAllItemsArgs : PipelineArgs
    {
        public Item StandardValuesItem { get; set; }

        private List<Item> items;
        public List<Item> Items 
        {
            get
            {
                if(items == null)
                {
                    items = new List<Item>();
                }

                return items;
            }
            set
            {
                items = value;
            }
        }
    }
}

The caller of the custom pipeline is required to pass the Standard Values Item that contains the new tokens. One of the processors of the custom pipeline will collect all Items that use its Template — these are stored in the Items collection property.

The instance of the following class serves as the first processor of the custom pipeline:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class EnsureStandardValues
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            if(IsStandardValues(args.StandardValuesItem))
            {
                return;
            }

            args.AbortPipeline();
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }
    }
}

This processor basically just ascertains whether the Item passed as the Standard Values Item is indeed a Standard Values Item — the code just delegates to the static IsStandardValuesHolder() method on Sitecore.Data.StandardValuesManager (this lives in Sitecore.Kernel.dll).

The instance of the next class serves as the second step of the custom pipeline:

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

using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class EnsureUnexpandedTokens
    {
        private List<string> Tokens { get; set; }

        public EnsureUnexpandedTokens()
        {
            Tokens = new List<string>();
        }

        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            if (!Tokens.Any())
            {
                args.AbortPipeline();
                return;
            }

            args.StandardValuesItem.Fields.ReadAll();
            foreach(Field field in args.StandardValuesItem.Fields)
            {
                if(HasUnexpandedTokens(field))
                {
                    return;
                }
            }
            
            args.AbortPipeline();
        }

        protected virtual bool HasUnexpandedTokens(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            foreach(string token in Tokens)
            {
                if(field.Value.Contains(token))
                {
                    return true;
                }
            }

            return false;
        }
    }
}

A collection of tokens are injected into the class’ instance via the Sitecore Configuration Factory — see the patch configuration file further down in this post — and determines if tokens exist in any of its fields. If no tokens are found, then the pipeline is aborted. Otherwise, we exit the Process() method immediately.

The instance of the following class serves as the third processor of the custom pipeline:

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

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class CollectAllItems
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            args.Items = GetAllItemsByTemplateID(args.StandardValuesItem.TemplateID);
            if(args.Items.Any())
            {
                return;
            }

            args.AbortPipeline();
        }

        protected virtual List<Item> GetAllItemsByTemplateID(ID templateID)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(templateID), "templateID", "templateID cannot be null or empty!");
            using (var context = ContentSearchManager.GetIndex("sitecore_master_index").CreateSearchContext())
            {
                var query = context.GetQueryable<SearchResultItem>().Where(i => i.TemplateId == templateID);
                return query.ToList().Select(result => result.GetItem()).ToList();
            }  
        }
    }
}

This class uses the Sitecore.ContentSearch API to find all Items that use the Template of the Standard Values Item. If at least one Item is found, we exit the Process() method immediately. Otherwise, we abort the pipeline.

The instance of the class below serves as the fourth processor of the custom pipeline:

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

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class FilterStandardValuesItem
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Items, "args.Items");
            if(!args.Items.Any())
            {
                return;
            }

            args.Items = args.Items.Where(item => !IsStandardValues(item)).ToList();
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }
    }
}

The code in this class ensures the Stardard Values Item is not in the collection of Items. It’s probably not a good idea to expand tokens on the Standard Values Item. 🙂

The instance of the next class serves as the final processor of the custom pipeline:

using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Data;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class ExpandTokens
    {
        private MasterVariablesReplacer TokenReplacer { get; set; }

        public ExpandTokens()
        {
            TokenReplacer = GetTokenReplacer();
        }

        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Items, "args.Items");
            if (!args.Items.Any())
            {
                args.AbortPipeline();
                return;
            }

            foreach(Item item in args.Items)
            {
                ExpandTokensOnItem(item);
            }
        }

        protected virtual void ExpandTokensOnItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            item.Fields.ReadAll();
            item.Editing.BeginEdit();
            TokenReplacer.ReplaceItem(item);
            item.Editing.EndEdit();
        }

        protected virtual MasterVariablesReplacer GetTokenReplacer()
        {
            return Factory.GetMasterVariablesReplacer();
        }
    }
}

The code above uses the instance of Sitecore.Data.MasterVariablesReplacer (subclass or otherwise) — this is defined in your Sitecore configuration at settings/setting[@name=”MasterVariablesReplacer”] — and passes all Items housed in the pipeline argument instance to its ReplaceItem() method — each Item is placed in an editing state before having their tokens expanded.

I then built the following class to serve as a <saveUI> pipeline processor (this pipeline is triggered when someone saves an Item in the Sitecore client):

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Pipelines.Save;

using Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems;

namespace Sitecore.Sandbox.Pipelines.SaveUI
{
    public class ExpandNewStandardValuesTokens
    {
        private string ExpandNewTokensOnAllItemsPipeline { get; set; }

        public void Process(SaveArgs args)
        {
            Assert.IsNotNullOrEmpty(ExpandNewTokensOnAllItemsPipeline, "ExpandNewTokensOnAllItemsPipeline must be set in configuration!");
            foreach (SaveArgs.SaveItem saveItem in args.Items)
            {
                Item item = GetItem(saveItem);
                if(IsStandardValues(item))
                {
                    ExpandNewTokensOnAllItems(item);
                }
            }
        }

        protected virtual Item GetItem(SaveArgs.SaveItem saveItem)
        {
            Assert.ArgumentNotNull(saveItem, "saveItem");
            return Client.ContentDatabase.Items[saveItem.ID, saveItem.Language, saveItem.Version];
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }

        protected virtual void ExpandNewTokensOnAllItems(Item standardValues)
        {
            CorePipeline.Run(ExpandNewTokensOnAllItemsPipeline, new ExpandNewTokensOnAllItemsArgs { StandardValuesItem = standardValues });
        }
    }
}

The code above invokes the custom pipeline when the Item being saved is a Standard Values Item — the Standard Values Item is passed to the pipeline via a new ExpandNewTokensOnAllItemsArgs instance.

I then glued all of the pieces above together in the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <expandNewTokensOnAllItems>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureStandardValues, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureUnexpandedTokens, Sitecore.Sandbox">
          <Tokens hint="list">
            <Token>$name</Token>
            <Token>$id</Token>
            <Token>$parentid</Token>
            <Token>$parentname</Token>
            <Token>$date</Token>
            <Token>$time</Token>
            <Token>$now</Token>
          </Tokens>
        </processor>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.CollectAllItems, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.FilterStandardValuesItem, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.ExpandTokens, Sitecore.Sandbox" />
      </expandNewTokensOnAllItems>
    </pipelines>
    <processors>
      <saveUI>
        <processor patch:before="saveUI/processor[@type='Sitecore.Pipelines.Save.Save, Sitecore.Kernel']" 
                   mode="on" type="Sitecore.Sandbox.Pipelines.SaveUI.ExpandNewStandardValuesTokens">
          <ExpandNewTokensOnAllItemsPipeline>expandNewTokensOnAllItems</ExpandNewTokensOnAllItemsPipeline>
        </processor>  
      </saveUI>
    </processors>
  </sitecore>
</configuration>

Let’s see this in action!

I added three new fields to a template, and added some tokens in them:

added-new-tokens

After clicking save, I navigated to one of the content Items that use this Template:

tokens-expanded

As you can see, the tokens were expanded. 🙂

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

Expand Tokens on Items Using a Sitecore PowerShell Extensions Toolbox Script

Last Wednesday I had the opportunity of presenting Sitecore PowerShell Extensions (SPE) at the Milwaukee Sitecore Meetup. During this presentation, I demonstrated how quickly and easily one can add, execute and reuse PowerShell scripts in SPE, and I did this using version 3.0 of SPE on Sitecore XP 8.

During one segment of the presentation, I shared how one can seamlessly add scripts to the SPE Toolbox — a repository of utility scripts if you will — and used the following script when showing this:

<#
    .NAME
        Expand tokens in all content items
 
    .SYNOPSIS
        Expand tokens in all fields in all content items
    .NOTES
        Mike Reynolds
#>
 
$items = Get-ChildItem -Path "master:\sitecore\content" -Recurse
$items | ForEach-Object { $_.Fields.ReadAll() }
$items |  Expand-Token
Close-Window

The script above grabs all descendant Items under /sitecore/content/; iterates over them to ensure all field values are available — the ReadAll() method on the FieldCollection instance will ensure values from fields on the Item’s template’s Standard Values Item are pulled in for processing; and sends in these Items into the Expand-Token commandlet which comes “out of the box” with SPE.

The script also closes the processing dialog.

I then saved the above script into my Toolbox library in my SPE module:

toolbox-script-ise-save

Let’s try this out. Let’s find some Items with tokens in some fields. It looks like the Home Item has some:

home-tokens

Here’s another Item that also has tokens:

descendant-tokens

Let’s go to the SPE Toolbox, and click on our Toolbox utility:

toolbox-expand-tokens-click

As you can see the tokens were expanded on the Home Item:

home-tokens-expanded

Tokens were also expanded on the descendant Item:

descendant-tokens-expanded

If you have any thoughts and/or suggestions on this, or have ideas for other SPE Toolbox scripts, please drop a comment.

If you would like to watch the Milwaukee Sitecore Meetup presentation where I showed the above — you’ll also get to see some epic Sitecore PowerShell Extensions stuff from Adam Brauer, Senior Product Engineer at Active Commerce, in this presentation as well — have a look below:

If you would like to see another example of adding a script to the SPE Toolbox, please see my previous post on this subject.

Until next time, have a scriptaculous day!

Make Bulk Item Updates using Sitecore PowerShell Extensions

In my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014, I demonstrated how simple it is to make bulk Item updates — perform the same update to multiple Sitecore items — using a simple PowerShell script, and thought I would write down what I had shown.

Sadly, I do not remember which script I had shared with the audience — the scratchpad text file I referenced during my presentation contains multiple scripts for making bulk updates to Items (if you attended my talk, and remember exactly what I had shown, please drop a comment).

Since I cannot recall which script I had shown — please forgive me 😉 — let’s look at the following PowerShell script (this might be the script I had shown):

@(Get-Item .) + (Get-ChildItem -r .) | ForEach-Object { Expand-Token $_ }

This script grabs the context Item — this is denoted by a period — within the PowerShell ISE via the Get-Item command, and puts it into an array so that we can concatenate it with an array of all of its descendants — this is returned by the Get-ChildItem command with the -r parameter (r stands for recursive). The script then iterates over all Items in the resulting array, passes each to the Expand-Token command — this command is offered “out of the box” in Sitecore PowerShell Extensions — which expands tokens in every field on the Item.

Let’s see this in action!

My home Item has some tokens in its Title field:

home-tokens

One of its descendants also has tokens in its Title field:

descendant-tokens

I opened up the PowerShell ISE, wrote my script, and executed:

powershell-ise-tokens

As you can see, the tokens on the home Item were expanded:

home-tokens-expanded

They were also expanded on the home Item’s descendant:

descendant-tokens-expanded

If you have any thoughts or questions on this, please share in a comment.

Launch PowerShell Scripts in the Item Context Menu using Sitecore PowerShell Extensions

Last week during my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014 — a conference held in Utrecht, Netherlands — I demonstrated how to invoke PowerShell scripts from the Item context menu in Sitecore, and felt I should capture what I had shown in a blog post — yes, this is indeed that blog post. 😉

During that piece of my presentation, I shared the following PowerShell script to expands tokens in fields of a Sitecore item (if you want to learn more about tokens in Sitecore, please take a look at John West’s post about them, and also be aware that one can also invoke the Expand-Token PowerShell command that comes with Sitecore PowerShell Extensions to expand tokens on Sitecore items — this makes things a whole lot easier 😉 ):

$item = Get-Item .
$tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
$item.Editing.BeginEdit()
$tokenReplacer.ReplaceItem($item)
$item.Editing.EndEdit()
Close-Window

The script above calls Sitecore.Configuration.Factory.GetMasterVariablesReplacer() for an instance of the MasterVariablesReplacer class — which is defined and can be overridden in the “MasterVariablesReplacer” setting in your Sitecore instance’s Web.config — and passes the context item — this is denote by a period — to the MasterVariablesReplacer instance’s ReplaceItem() method after the item has been put into editing mode.

Once the Item has been processed, it is taken out of editing mode.

So how do we save this script so that we can use it in the Item context menu? The following screenshot walks you through the steps to do just that:

item-context-menu-powershell-ise

The script is saved to an Item created by the dialog above:

expand-tokens-item

Let’s test this out!

I selected an Item with unexpanded tokens:

home-tokens-to-expand

I then launched its Item context menu, and clicked the option we created to ‘Expand Tokens’:

home-item-context-menu-expand-tokens

As you can see the tokens were expanded:

home-tokens-expanded

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

Until next time, have a scriptolicious day 😉

Expand Tokens on Sitecore Items Using a PowerShell Function in Sitecore PowerShell Extensions

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had briefly showcased a custom PowerShell function that expands Sitecore tokens in fields of a supplied item, and how I had saved this function into the Functions section of the Script Library — /sitecore/system/Modules/PowerShell/Script Library/Functions — of the Sitecore PowerShell Extensions module. This blog post captures what I had shown.

This is the custom function I had shown — albeit I changed its name to adhere to the naming convention in PowerShell for functions and commands (Verb-SingularNoun):

function Expand-SitecoreToken {
	<#
        .SYNOPSIS
             Expand tokens on the supplied item
              
        .EXAMPLE
            Expand tokens on the home item.
             
            PS master:\> Get-Item "/sitecore/content/home" | Expand-SitecoreToken
    #>
	[CmdletBinding()]
    param( 
		[ValidateNotNull()]
		[Parameter(ValueFromPipeline=$True)]
        [Sitecore.Data.Items.Item]$item
    )
	
    $item.Editing.BeginEdit()
    
    Try
    {
        $tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
        $tokenReplacer.ReplaceItem($item)
        $result = $item.Editing.EndEdit()
        "Expanded tokens on item " + $item.Paths.Path
    }
    Catch [system.Exception]
    {
        $item.Editing.CancelEdit()
        "Failed to expand tokens on item"
        "Reason: " + $error
    }
}

The function above calls Sitecore.Configuration.Factory.GetMasterVariablesReplacer() for an instance of the MasterVariablesReplacer class — which is defined and can be overridden in the “MasterVariablesReplacer” setting in your Sitecore instance’s Web.config — and passes the item supplied to the function to the MasterVariablesReplacer instance’s ReplaceItem() method after the item has been put into editing mode.

Once tokens have been expanded, a confirmation message is sent to the Results window.

If an exception is caught, we display it — the exception is captured in the $error global variable.

I saved the above function into the Script Library of my copy of the Sitecore PowerShell Extensions module:

spe-save-function

An item was created in the Script Library to house the function:

Expand-SitecoreToken-item

Let’s try it out.

Let’s expand tokens on the Home item:

spe-home-unexpanded-tokens

In the Integrated Scripting Environment of the Sitecore PowerShell Extensions module, I typed in the following code:

Execute-Script "master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken"
Get-Item . | Expand-SitecoreToken

You can consider the Execute-Script “master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken” line of code to be comparable to a javascript “script” tag — it will execute the script thus defining the function so we can execute it.

I then ran that code above:

excuted-Expand-SitecoreToken-on-home

Once the script finished running, I went back over to the Content Editor, and saw that tokens were expanded on the Home item:

spe-function-home-tokens-expanded

You might be thinking “Mike, I really don’t want to be bothered with expanding these tokens, and would rather have our Content Editors/Authors do it. is there something we can set up to make that happen?”

You bet. 🙂

In the Sitecore PowerShell Extension module, you can save PowerShell into the Script Library to be executed via an item context menu option click. All you have to do is save it into the Script Library under the Content Editor Context Menu item:

spe-context-menu-option

The script is then saved in a new item created under Content Editor Context Menu item in the Script Library:

spe-content-menu-option-item

Let’s see it in action.

I chose the following page at random to expand tokens:

spe-inner-page-three-unexpanded-tokens

I right-clicked on the item to launch it’s context menu, opened up scripts, and saw a new “Expand Tokens” option:

spe-new-context-menu-option

I clicked it, and was given a dialog with a status bar:

spe-context-menu-expanding-tokens

I refreshed the item, and saw that all tokens were expanded:

spe-context-menu-tokens-expanded

If you have any thoughts or questions on this, please leave a comment.

Until next time, have a scriptabulous day!

Expand Tokens on Sitecore Items Using a Custom Command in Sitecore PowerShell Extensions

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had shown attendees how they could go about adding a custom command into the Sitecore PowerShell Extensions module.

This blog post shows what I had presented — although the code in this post is an improved version over what I had presented at my talk. Many thanks to Sitecore MVP Adam Najmanowicz for helping me make this code better!

The following command will expand “out of the box” tokens in all fields of a supplied Sitecore item — check out Expand Tokens on Sitecore Items Using a Custom Command in Revolver where I discuss the problem commands like this address, and this article by Sitecore MVP Jens Mikkelsen which lists “out of the box” tokens available in Sitecore:

using System;
using System.Management.Automation;

using Sitecore.Configuration;

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

using Cognifide.PowerShell.PowerShellIntegrations.Commandlets;

namespace CommandLineExtensions.PowerShell.Commandlets
{
    [Cmdlet("Expand", "Token")]
    [OutputType(new[] { typeof(Item) })]
    public class ExpandTokenCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
        public Item Item { get; set; }

        protected override void ProcessRecord()
        {
            Item.Editing.BeginEdit();
            try
            {
                TokenReplacer.ReplaceItem(Item);
                Item.Editing.EndEdit();
            }
            catch (Exception ex)
            {
                Item.Editing.CancelEdit();
                throw ex;
            }

            WriteItem(Item);
        }
    }
}

The command above subclasses Cognifide.PowerShell.PowerShellIntegrations.Commandlets.BaseCommand — the base class for most (if not all) commands in Sitecore PowerShell Extensions.

An item is passed to the command via a parameter, and is magically set on the Item property of the command class instance.

The ValueFromPipeline parameter being set to “true” on the Item property’s Parameter attribute will allow for chaining of this command with others so that items can be fed into it via a pipe bridging the commands together in PowerShell.

An instance of the Sitecore.Data.MasterVariablesReplacer class — which is created by the GetMasterVariablesReplacer() method of the Sitecore.Configuration.Factory class based on the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — is used to expand tokens on the supplied Sitecore item after the item was flagged for editing.

Once tokens have been expanded on the item — or not in the event an exception is encountered — the item is written to the Results window via the WriteItem method which is defined in the BaseCommand class.

I then had to wire up the custom command via a patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
		    <add Name="Custom Commandlets" type="*, CommandLineExtensions" />
      </commandlets>
    </powershell>
  </sitecore>                                                                                     
</configuration>

Let’s take this custom command for a spin.

I created a bunch of test items, and set tokens in their fields. I then selected the following page at random for testing:

page-one-unexpanded-tokens

I opened up the Integrated Scripting Environment of Sitecore PowerShell Extensions, typed in the following PowerShell code, and executed by pressing Ctrl-E:

spe-ise-expand-tokens-page-one

As you can see tokens were expanded on the Page One item:

page-one-expanded-tokens

How about expanding tokens on all descendants of the Home item? Let’s see an example of how we can do that.

I chose the following content item — a grandchild of the Home item — for testing:

inner-page-one-unexpanded-tokens

I switched back over to the Integrated Scripting Environment, wrote the following code for testing — the Get-ChildItem command with the -r parameter (this means do this recursively) will grab all descendants of the Home item, and pipe each item in the result set into the Expand-Token command — and clicked the Execute button:

spe-ise-expand-on-descendants

I then went back to the grandchild item of the Home page in the content tree, and saw that tokens were expanded in its fields:

spe-expanded-on-descendants

If you have any thoughts or comments on this, or ideas for new commands in Sitecore PowerShell Extensions, please share in a comment.

Until next time, have a scriptolicious day!

Expand Tokens on Sitecore Items Using a Custom Command in Revolver

On September 18, 2013, I presented Sitecore from the Command Line at the Sitecore User Group – New England.

During my presentation, I gave an example of creating a custom command in Revolver — the first scripting platform for Sitecore built by Alistair Deneys — and thought I would write something up for those who had missed the presentation, or wanted to revisit what I had shown.

One thing that plagues some Sitecore developers — if you disagree please leave a comment — is not having a nice way to expand tokens on items when tokens are added to Standard Values after items had been created previously.

Newly added tokens “bleed” into preexisting items’ fields, and I’ve seen developers perform crazy feats of acrobatic gymnastics to expand them — writing a standalone web form to recursive crawl the content tree to expand these is such an example (take a look at Empower Your Content Authors to Expand Standard Values Tokens in the Sitecore Client where I offer an alternative way to expand tokens on content items).

The following custom Revolver command will expand tokens on a supplied Sitecore item, and help out on the front of expanding newly added tokens on preexisting items:

using System;
using Sitecore.Configuration;
using System.Linq;

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

using Revolver.Core;
using Revolver.Core.Commands;

namespace CommandLineExtensions.Revolver.Commands
{
    public class ExpandTokensCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        public override string Description()
        {
            return "Expand tokens on an item";
        }

        public override HelpDetails Help()
        {
            HelpDetails details = new HelpDetails
            {
                Description = Description(),
                Usage = "<cmd> [path]"
            };
            
            details.AddExample("<cmd>");
            details.AddExample("<cmd> /item1/item2");
            return details;
        }

        public override CommandResult Run(string[] args)
        {
            string path = string.Empty;
            if (args.Any())
            {
                path = args.FirstOrDefault();
            }

            using (new ContextSwitcher(Context, path))
            {
                if (!Context.LastGoodPath.EndsWith(path, StringComparison.CurrentCultureIgnoreCase))
                {
                    return new CommandResult
                    (
                        CommandStatus.Failure, 
                        string.Format("Failed to expand tokens on item {0}\nReason:\n\n An item does not exist at that location!", path)
                    );
                }

                CommandResult result;
                Item item = Context.CurrentItem;
                item.Editing.BeginEdit();
                try
                {
                    TokenReplacer.ReplaceItem(item);
                    result = new CommandResult(CommandStatus.Success, string.Concat("Expanded tokens on item ", Context.LastGoodPath));
                    item.Editing.EndEdit();
                }
                catch (Exception ex)
                {
                    item.Editing.CancelEdit();
                    result = new CommandResult(CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n{1}", path, ex));
                }

                return result;
            }
        }
    }
}

Tokens are expanded using an instance of the Sitecore.Data.MasterVariablesReplacer class — you can roll your own, and wire it up in the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — which is provided by Sitecore.Configuration.Factory.GetMasterVariablesReplacer().

All custom commands in Revolver must implement the Revolver.Core.ICommand interface. I subclassed Revolver.Core.Commands.BaseCommand — which does implement this interface — since it seemed like the right thing to do given that all “out of the box” commands I saw in Revolver were subclassing it, and then implemented the Description(), Help() and Run() abstract methods.

I then had to bind the custom command to a new name — I chose “et” for “Expand Tokens”:

@echooff
@stoponerror

bind CommandLineExtensions.Revolver.Commands.ExpandTokensCommand,CommandLineExtensions et

@echoon

Since it wouldn’t be efficient to type and run this bind script every time I want to use the “et” command, I added it into a startup script in the core database:

bind-custom-commands-script

I then had to create a user script for the startup script to run. I chose the Everyone role here for demonstration purposes:

bind-custom-commands-user-script

The above startup script will be invoked when Revolver is opened, and our custom command will be bound.

Let’s see all of the above in action.

I added some tokens in my home item:

home-item-unexpanded-tokens

I then opened up Revolver, navigated to /sitecore/content, and ran the custom command on the home item:

ran-et-revolver

As you can see the tokens were expanded:

home-item-expanded-tokens

You might be thinking “that’s wonderful Mike — except now I have to navigate to every item in my content tree using Revolver, and then run this custom command on it”.

Well, I do have a solution for this: a custom script that grabs an item and all of its descendants using a Sitecore query, and passes them to the custom command to expand tokens:

@echooff
@stoponerror

if ($1$ = \$1\$) (exit (Missing required parameter path))

@echoon

query -ns $1$/descendant-or-self::* et

I put this script in the core database, and named it “etr” for “Expand Tokens Recursively”:

etr-script

I navigated to a descendant of /sitecore/content/home, and see that it has some unexpanded tokens on it:

descendant-unexpanded-tokens

I went back to Revolver, and ran the “etr” command on the home item:

etr-revolver

As you can see tokens were expanded on the descendant item:

descendant-expanded-tokens

If you have any thoughts on this, or have ideas for other custom commands in Revolver, please share in a comment.