Here is a validation attribute that I wrote in order to limit the number of items that editors may put in Optimizely ContentAreas to a specific set of numbers, instead of having max and min values.
The part that may be of interest to highlight is the possibility to skip validation in certain cases, for instance when combining the validation with the Hide-attribute discussed in the article Hiding and showing edit mode properties on same content type in Optimizely CMS depending on context. We need the validation on properties that previously was visible to all editors, but now are not. If the property did contain the wrong number of items, and was then hidden, the validation would fail, and the editors in question would not be able to do anything about it.
/// <summary>
/// Limit numbers of items in ContentArea to the ones set in this attribute.
/// Use countAllItems = true if you want to bypass personalization.
/// Add [AllowedNumberOfItems(2, 4, 6, 8)] to a prop-definition limits number of items to any of these.
///
/// [Display(Name = "Prop name")]
/// [AllowedNumberOfItems(2, 4, 6, 8)]
/// public virtual ContentArea SomeContentArea { get; set; }
/// </summary>
/// <remarks>
/// Should you wish to allow zero (0) blocks in the area, include zero in the number array ([AllowedNumberOfItems(0, 2, 4)]).
/// SkipValidationOnLocalContent option may be used for instance in cases where existing properties are hidden on local pages
/// and risk to fail the validation (preventing local editors from publishing their pages).
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
public sealed class AllowedNumberOfItemsAttribute : ValidationAttribute
{
private readonly int[] _validCounts;
private readonly bool _skipValidationOnLocalContent;
private readonly bool _countAllItems;
public AllowedNumberOfItemsAttribute(bool skipValidationOnLocalContent = false, bool countAllItems = false, params int[] validCounts)
{
_validCounts = validCounts;
_countAllItems = countAllItems;
_skipValidationOnLocalContent = skipValidationOnLocalContent;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (!ShouldValidate(value, validationContext))
{
return null;
}
if (!ValidateContentArea(value as ContentArea))
{
return new ValidationResult($"Only {string.Join(", ", _validCounts)} number of blocks area allowed in '{validationContext.DisplayName}'");
}
return null;
}
private bool ShouldValidate(object value, ValidationContext validationContext)
{
if (!_skipValidationOnLocalContent)
{
return true;
}
if (!(validationContext.ObjectInstance is PageDataBase pageDataBase))
{
return true;
}
return pageDataBase.GlobalPage == null || pageDataBase.CompanyId == 0;
}
private bool ValidateContentArea(ContentArea contentArea)
{
var allItems = contentArea?.Items ?? Enumerable.Empty<ContentAreaItem>();
// Count the personalization group names (distinct), replacing non personalized ones with a unique name.
var i = 0;
var maxNumberOfItemsShown = _countAllItems ? contentArea?.Items?.Count ?? 0 :
allItems.Select(x => string.IsNullOrEmpty(x.ContentGroup) ? i++.ToString() : x.ContentGroup).Distinct().Count();
return _validCounts.Contains(maxNumberOfItemsShown);
}
}
To achieve this, it is possible to look at the validationContext.ObjectInstance property. If it is our expected page type, we may use it to access properties our page type in the validation attribute, without having to implement IMetadataAware.