Home » Commands » Yet Another Post on Sitecore Content Editor Warnings

Yet Another Post on Sitecore Content Editor Warnings

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

Over the years, I’ve written posts on adding custom Content Editor Warnings. Content Editor Warnings give visual cues at the top of an Item in the Content Editor to either take action on something about the Item, or to just convey information about the Item — perhaps the Item is read-only, or maybe there is something wrong with the item; you can do all kinds of stuff with these, just use your imagination on what you would like to convey to your content authors.

The way to add these is to create a custom processor for the <getContentEditorWarnings> pipeline in Sitecore. There really isn’t anything more to it.

Moreover, you can even use Sitecore PowerShell Extensions to create these though I did not do this for this post. Sorry, Sitecore MVP Michael West. 😉

Today, I am sharing a recent example of two Content Editor Warnings which I had worked on which convey to content authors they almost have, or have too many child items within a Media Library folder.

Some code on this post reuses service classes found on my previous post where I discussed how to create a custom MasterDataView driven by a custom pipeline; I recommend having a read of that previous post first before proceeding further in order to have complete grounding on some of the service classes I am using in the solution below.

I had to make a tweak to the ITooManySubItemsService service found on my previous post — I had to make the GetNumberOfItemsToStartWarningUser() and GetMaximumNumberOfItemsInFolder() methods public as I needed to get these two values within the Content Editor Warnings in order to display these values in messages to the content authors via the Content Editor Warning. This service determines these values based on a Config Object service which can also be overriden by each individual piece of functionality in my solution (i.e. the maximum number of child items set on the pipeline processor would override the Config Object service’s value):

using Foundation.Validation.Models.TooManySubItems;

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

		bool HasTooManySubItems(TooManySubItemsServiceParameters parameters);

		int GetNumberOfItemsToStartWarningUser(TooManySubItemsServiceParameters parameters); // I've added this 

		int GetMaximumNumberOfItemsInFolder(TooManySubItemsServiceParameters parameters); // I've added this 
	}
}

Here is the modified implementation of the interface above. All I did was change the signature on GetNumberOfItemsToStartWarningUser() and GetMaximumNumberOfItemsInFolder() to be public instead of protected:

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;
		}

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

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

			return _settings.NumberOfItemsToStartWarningUser;
		}

		public 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 processors which are defined below will give content authors the ability to take action on the Content Editor Warnings. The options are basically just as collection of link text and Sheer UI commands. In this example, content authors are given the option to create new Media Library folders. The following class is used when sourcing these from each processor’s configuration definition (see the Sitecore patch configuration file further down in this post):

namespace Foundation.Kernel.Models.Pipelines.ContentEditorWarnings
{
	public class ContentEditorWarningOption
	{
		public string Text { get; set; }
		
		public string Link { get; set; }
	}
}

Since the two Content Editor Warning processors are doing very similiar things, I abstracted out most of their logic into a base abstract class, and put it hooks for overriding methods on it; this is an example of the Template Method Pattern for you Design Pattern junkies 😉

using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Pipelines.GetContentEditorWarnings;

using Foundation.Kernel.Models.Pipelines.ContentEditorWarnings;

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

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public abstract class BaseTooManyChildItemsWarningProcessor : IFeatureToggleable, ITooManySubItemsFeature
	{
		private readonly ITooManySubItemsFeatureToggleService _tooManySubItemsFeatureToggleService;
		private readonly ITooManySubItemsServiceParametersFactory _tooManySubItemsServiceParametersFactory;
		private readonly ITooManySubItemsService _tooManySubItemsService;

		public bool Enabled { get; set; }

		protected string MediaLibraryBasePath { get; set; }

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

		public int NumberOfItemsToStartWarningUser { get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }

		protected List<ContentEditorWarningOption> ContentEditorWarningOptions { get; set; } = new List<ContentEditorWarningOption>();

		public BaseTooManyChildItemsWarningProcessor(ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService)
		{
			_tooManySubItemsFeatureToggleService = tooManySubItemsFeatureToggleService;
			_tooManySubItemsServiceParametersFactory = tooManySubItemsServiceParametersFactory;
			_tooManySubItemsService = tooManySubItemsService;
		}

		public void Process(GetContentEditorWarningsArgs args)
		{
			if(!IsEnabled() || !IsInMediaLibrary(args?.Item))
			{
				return;
			}

			TooManySubItemsServiceParameters parameters = CreateParameters(args?.Item, this);
			if(parameters == null)
			{
				return;
			}

			if(!ShouldDisplayWarning(parameters))
			{
				return;
			}

			int maximumNumberOfItemsInFolder = GetMaximumNumberOfItemsInFolder(parameters);
			string warningTitle = GetWarningTitle(args, maximumNumberOfItemsInFolder);
			string warningMessage = GetWarningMessage(args, maximumNumberOfItemsInFolder);
			AddWarning(args, warningTitle, warningMessage, ContentEditorWarningOptions);
		}

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

		protected virtual bool IsInMediaLibrary(Item item)
		{
			if(string.IsNullOrWhiteSpace(item.Paths.FullPath))
			{
				return false;
			}

			return item.Paths.FullPath.Contains(MediaLibraryBasePath);
		}

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

		protected abstract bool ShouldDisplayWarning(TooManySubItemsServiceParameters parameters);

		protected abstract string GetWarningTitle(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder);

		protected abstract string GetWarningMessage(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder);

		protected virtual int GetMaximumNumberOfItemsInFolder(TooManySubItemsServiceParameters parameters) =>_tooManySubItemsService.GetMaximumNumberOfItemsInFolder(parameters);

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

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

		protected virtual void AddWarning(GetContentEditorWarningsArgs args, string title, string message, IEnumerable<ContentEditorWarningOption> options)
		{
			if(args == null)
			{
				return;
			}
			
			GetContentEditorWarningsArgs.ContentEditorWarning warning = CreateNewContentEditorWarning(args); 
			if(warning == null)
			{
				return;
			}

			warning.Title = title;
			warning.Text = message;
			AddOptions(warning, options);
		}

		protected virtual GetContentEditorWarningsArgs.ContentEditorWarning CreateNewContentEditorWarning(GetContentEditorWarningsArgs args) => args?.Add();

		protected virtual void AddOptions(GetContentEditorWarningsArgs.ContentEditorWarning warning, IEnumerable<ContentEditorWarningOption> options)
		{
			if(warning == null || options == null)
			{
				return;
			}

			foreach(ContentEditorWarningOption option in options)
			{
				if(string.IsNullOrWhiteSpace(option.Text) || string.IsNullOrWhiteSpace(option.Link)) continue;
				warning.AddOption(option.Text, option.Link);
			}
		}
	}
}

The class above will only display the Content Editor Warning when the feature is enabled (see my previous post where I discuss how this works using Sitecore configuration feature toggles); the item is in the Media Library; and the ShouldDisplayWarning() method returns true — this must be implemented by subclasses of this abstract base class.

If the Content Editor Warning should display, the Content Editor Warning’s title is retrieved via the GetWarningTitle() method, and its message is retrieved via the GetWarningMessage() method — both of these methods must be implemented by its subclasses.

These are then added to the a new GetContentEditorWarningsArgs.ContentEditorWarning instance created from the GetContentEditorWarningsArgs instance with the options defined in the configuration for the processor.

I then created the following interface for the purposes of registering its implementation in the Sitecore IoC container; this is optional as you could just register it with its implementation type being its service type.

using Sitecore.Pipelines.GetContentEditorWarnings;

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public interface IAlmostTooManyChildItemsWarningProcessor
	{
		void Process(GetContentEditorWarningsArgs args);
	}
}

Here is the implementation of the interface above. This is the processor class to show content authors when they are approaching too many child items:

using Sitecore.Pipelines.GetContentEditorWarnings;

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

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public class AlmostTooManyChildItemsWarningProcessor : BaseTooManyChildItemsWarningProcessor, IAlmostTooManyChildItemsWarningProcessor
	{
		private string AlmostAtMaxiumTitle { get; set; }

		private string AlmostAtMaxiumMessageFormat { get; set; }

		public AlmostTooManyChildItemsWarningProcessor(ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService)
			: base(tooManySubItemsFeatureToggleService, tooManySubItemsServiceParametersFactory, tooManySubItemsService)
		{
		}

		protected override bool ShouldDisplayWarning(TooManySubItemsServiceParameters parameters) => HasAlmostTooManySubItems(parameters);

		protected override string GetWarningTitle(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => AlmostAtMaxiumTitle;

		protected override string GetWarningMessage(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => string.Format(AlmostAtMaxiumMessageFormat, args?.Item?.Children?.Count, maximumNumberOfItemsInFolder);
	}
}

The implementation above is using the AlmostAtMaxiumTitle and AlmostAtMaxiumMessageFormat strings set via the Sitecore Configuration Factory onto the class instance; these are used when displaying messaging to content authors.

Also, this implementation is using the HasAlmostTooManySubItems() method defined on its base class which ultimately makes a call to the ITooManySubItemsService service class to ascertain whether the folder/item almost has too many child items.

Just as I had done above, I created an interface for the other Content Editor Warning, only for the purpose of registering it in the Sitecore IoC container.

using Sitecore.Pipelines.GetContentEditorWarnings;

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public interface ITooManyChildItemsWarningProcessor
	{
		void Process(GetContentEditorWarningsArgs args);
	}
}

Here is the implementation of the interface above:

using Sitecore.Pipelines.GetContentEditorWarnings;

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

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public class TooManyChildItemsWarningProcessor : BaseTooManyChildItemsWarningProcessor, ITooManyChildItemsWarningProcessor
	{
		private string AtMaxiumTitle { get; set; }

		private string AtMaxiumMessageFormat { get; set; }

		public TooManyChildItemsWarningProcessor(ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService)
			: base(tooManySubItemsFeatureToggleService, tooManySubItemsServiceParametersFactory, tooManySubItemsService)
		{
		}

		protected override bool ShouldDisplayWarning(TooManySubItemsServiceParameters parameters) => HasTooManySubItems(parameters);

		protected override string GetWarningTitle(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => AtMaxiumTitle;

		protected override string GetWarningMessage(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => string.Format(AtMaxiumMessageFormat, args?.Item?.Children?.Count, maximumNumberOfItemsInFolder);
	}
}

The class above is using the AtMaxiumTitle and AtMaxiumMessageFormat strings set via the Sitecore Configuration Factory from the processor’s configuration definition (see the Sitecore configuration patch file below for both processor definitions), and uses the HasTooManySubItems() method defined on the base class which also just delegates to the ITooManySubItemsService service class.

I then registered everything above in the following Sitecore patch configuration file:

<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>
			<getContentEditorWarnings>
				<processor type="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.IAlmostTooManyChildItemsWarningProcessor, Feature.ContentEditor" patch:before="processor[1]" resolve="true">
					<Enabled>true</Enabled>
					<MediaLibraryBasePath>/sitecore/media library/</MediaLibraryBasePath>
					<AlmostAtMaxiumTitle>This Item Almost Has Too Many Subitems Underneath It!</AlmostAtMaxiumTitle>
					<AlmostAtMaxiumMessageFormat>This item has {0} media libary items underneath it!  The maximum number of subitems allowed is {1}. Consider creating a new media library folder at this time.</AlmostAtMaxiumMessageFormat>
					<!-- By setting these, you can override the default values set for the entire feature set
										<NumberOfItemsToStartWarningUser>95</NumberOfItemsToStartWarningUser>
										<MaximumNumberOfItemsInFolder>100</MaximumNumberOfItemsInFolder>
					-->
					<ContentEditorWarningOptions hint="list">
						<Option type="Foundation.Kernel.Models.Pipelines.ContentEditorWarnings.ContentEditorWarningOption, Foundation.Kernel">
							<Text>New Media Folder</Text>
							<Link>media:newfolder</Link>
						</Option>
					</ContentEditorWarningOptions>
				</processor>
				<processor type="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.ITooManyChildItemsWarningProcessor, Feature.ContentEditor" patch:before="processor[1]" resolve="true">
					<Enabled>true</Enabled>
					<MediaLibraryBasePath>/sitecore/media library/</MediaLibraryBasePath>
					<AtMaxiumTitle>This Item Has Too Many Subitems Underneath It!</AtMaxiumTitle>
					<AtMaxiumMessageFormat>This item has {0} media libary items underneath it! The maximum number of subitems allowed is {1}. It's time to create a new media library folder for more items to upload.</AtMaxiumMessageFormat>
					<!-- By setting these, you can override the default values set for the entire feature set
										<NumberOfItemsToStartWarningUser>95</NumberOfItemsToStartWarningUser>
										<MaximumNumberOfItemsInFolder>100</MaximumNumberOfItemsInFolder>
					-->
					<ContentEditorWarningOptions hint="list">
						<Option type="Foundation.Kernel.Models.Pipelines.ContentEditorWarnings.ContentEditorWarningOption, Foundation.Kernel">
							<Text>New Media Folder</Text>
							<Link>media:newfolder</Link>
						</Option>
					</ContentEditorWarningOptions>
				</processor>
			</getContentEditorWarnings>	
		</pipelines>
		<services>
			<register
				serviceType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.IAlmostTooManyChildItemsWarningProcessor, Feature.ContentEditor"
				implementationType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.AlmostTooManyChildItemsWarningProcessor, Feature.ContentEditor"
				lifetime="Singleton" />
			<register
				serviceType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.ITooManyChildItemsWarningProcessor, Feature.ContentEditor"
				implementationType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.TooManyChildItemsWarningProcessor, Feature.ContentEditor"
				lifetime="Singleton" />
		</services>
	</sitecore>
</configuration>

Let’s see what these two processors do.

When looking at an Item which is approaching too many child items, we see the following:

When we have a look at an Item which has more than the maximum number set for child items, we see the following:

Please drop a comment if you have questions/comments, or would like to share how you’ve used Content Editor Warnings on projects you have worked on.


Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.