SXA Tenant Specific Field Validation

SXA includes a large number of extremely helpful modules and it would be wise to utilize as many as you can.  However a problem arises if you have multiple tenants or sites who want to both apply different field validation rules to fields that come from an SXA base template.

An extra huge thanks to @Sitecorey for a huge amount of help in working through this problem.

By default validation rules are applied on the template field item under the template.  This means that every other template that inherits yours will automatically get the validation rules applied to it.

Installation instructions and full source

Download the Sitecore package

My solution is to pull the validation rule definitions optionally out of the template field and into a global library of items that contain a template to template field mapping. The field can be defined in the template or in any of the base templates or the base templates base templates and so on.

validatorDiagram

An Example

I have two tenants both using the SEO Metadata module to get keywords and page description fields on their page template. Using this technique i was able to have one tenant define a 125 character limit while having the other tenant not have any validation. This was done by specifying the base template for pages as the template target and the SXA meta description field as the field. Even though the template doesn’t directly define this field we’re still able to apply validation to it.
fieldvalidationitem

How it’s done

The magic is done by overriding the default validation manager and adding functionality on top of it.  Basically what we want to do is augment the default functionality of the validator by looking into the library of global validators defined in the settings section of our SXA site.  To do that we have to follow a few steps:

  1. Get the root of the applicable site’s global field validator definitions root.
  2. Grab all the validator definitions from under that root.
  3. Build validators for all the definitions
  4. Return those validators
using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Abstractions;
using Sitecore.CodeDom.Scripts;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Validators;
using Sitecore.XA.Foundation.Multisite;

namespace JeffDarchuk.Foundation.ContentValidation
{
	public class GlobalFieldValidatorManager : DefaultValidatorManager
	{
		private readonly IMultisiteContext _multisiteContext;
		private readonly BaseTemplateManager _templateManager;

		public GlobalFieldValidatorManager(BaseItemScripts itemScripts, IMultisiteContext multisiteContext, BaseTemplateManager templateManager) : base(itemScripts)
		{
			_multisiteContext = multisiteContext ?? throw new ArgumentNullException(nameof(multisiteContext));
			_templateManager = templateManager ?? throw new ArgumentNullException(nameof(templateManager));
		}

		public override ValidatorCollection BuildValidators(ValidatorsMode mode, Item item)
		{
			var validators = base.BuildValidators(mode, item);
			var globalFieldRulesFolder = GetGlobalFieldRulesFolder(item);
			if (globalFieldRulesFolder == null) return validators;
			foreach (var validator in GetAdditionalValidators(item, globalFieldRulesFolder, mode))
			{
				validators.Add(validator);
			}
			return validators;
		}

		private Item GetGlobalFieldRulesFolder(Item item)
		{
			return _multisiteContext.GetSettingsItem(item)?.Children.FirstOrDefault(x =>
				x.TemplateID.ToString() == Templates.GlobalFieldRuleFolder.Id);
		}

		private IEnumerable GetAdditionalValidators(Item item, Item globalFieldRulesFolder, ValidatorsMode mode)
		{
			var baseTemplates = new HashSet(_templateManager.GetTemplate(item).GetBaseTemplates().Select(x => x.ID.ToString()));
			foreach (var globalFieldRule in GetGlobalFieldRules(globalFieldRulesFolder))
			{
				var template = globalFieldRule[Templates.GlobalFieldRule.Fields.Template];
				if (!FieldRuleAppliesToItem(item, globalFieldRule, template, baseTemplates)) continue;
				MultilistField validators = null;
				switch (mode)
				{
					case ValidatorsMode.Gutter:
						validators = globalFieldRule.Fields[Templates.FieldTypeValidationRules.Fields.QuickValidationBar];
						break;
					case ValidatorsMode.ValidateButton:
						validators = globalFieldRule.Fields[Templates.FieldTypeValidationRules.Fields.ValidateButton];
						break;
					case ValidatorsMode.ValidatorBar:
						validators = globalFieldRule.Fields[Templates.FieldTypeValidationRules.Fields.ValidatorBar];
						break;
					case ValidatorsMode.Workflow:
						validators = globalFieldRule.Fields[Templates.FieldTypeValidationRules.Fields.Workflow];
						break;
				}
				foreach (var validator in validators?.GetItems() ?? Enumerable.Empty())
				{
					var baseValidator = BuildValidator(validator, item);
					baseValidator.FieldID = item.Fields[globalFieldRule[Templates.GlobalFieldRule.Fields.Field]].ID;
					yield return baseValidator;
				}
			}
		}

		private IEnumerable GetGlobalFieldRules(Item globalFieldRulesFolder)
		{
			return globalFieldRulesFolder.Axes.GetDescendants().Where(x => x.TemplateID.ToString() == Templates.GlobalFieldRule.Id);
		}

		private bool FieldRuleAppliesToItem(Item item, Item globalFieldRule, string template, HashSet baseTemplates)
		{
			var useInheritedTemplates = ((CheckboxField)globalFieldRule.Fields[Templates.GlobalFieldRule.Fields.ApplyToInheritedTemplates]).Checked;
			return item.TemplateID.ToString() == template || useInheritedTemplates && baseTemplates.Contains(template);
		}
	}
}

The end result is that we get to define validation rules in whatever template we wish. Even if that template doesn’t directly define the field but only inherits it.