How to restrict number of items in ContentArea based on item type

ContentArea in Episerver is and have been a very popular property since its arrival years ago. Editors like them, and they are great for creating webpages from the editing perspective.

The problem

It can become too easy to add content to a page. With no restrictions, it is easy for an editor to add too much content to a page, and it can also break the design.

The solution

Restrict the number of items editors can put inside of a ContentArea. And if you go a bit further, you could restrict the number of items of each type allowed in the ContentArea. Here is a little attribute I’ve written that limits usage of the different content types inside a ContentArea.

Combined with the AllowedTypes-attribute, the ContentArea can be easily controlled.

I've made this attribute to be able to be used multiple times on each property. The reason for that is simple, I want to be able to restrict different content types with their own "maximum limit".

An important thing to remember when creating an AllowMultiple-attribute, is to implement the TypeId. If this is not done, only the last of the multiple validations will run. This old blog post explains more. The TypeId-issue is taken care of with this this one-liner:

public override object TypeId { get; } = new object();

The complete attribute in code:


    /// <summary>    
    /// Limit numbers of items of a specific type in ContentArea. This support allowmultiple for restriction of multiple items. 
    /// Add [MaxOfItem(typeof(ArticleTeaserBlock), 2)] to a prop-definition limits the number of type ArticleTeaserBlock to two inside the ContentArea;
    /// Example of usage:
    ///   [Display(Name = "Articles")]
    ///   [MaxOfItem(typeof(ArticleTeaserBlock), 3)]
    ///   [MaxOfItem(typeof(ProfilePage), 2)]
    ///   public virtual ContentArea MyProp { get; set; }
    /// </summary>  
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
    public sealed class MaxOfItemAttribute : ValidationAttribute
    {
        public MaxOfItemAttribute(Type itemType, int itemMax)
        {
            ItemType = itemType;
            ItemMaximum = itemMax;
        }
        
        private Type ItemType { get; set; }

        private int ItemMaximum { get; set; }
        
        public override string FormatErrorMessage(string name)
        {
            var message = LocalizationService.Current.GetString("/genericValidator/maxItemOfTypeCountFormat");
            return string.Format(message, name, ItemMaximum, ItemType.Name);
        }

        public override bool IsValid(object value)
        {
            return ValidateContentArea(value as ContentArea);
        }

        public override object TypeId { get; } = new object();

        private bool ValidateContentArea(ContentArea contentArea)
        {
            if (contentArea?.Items == null || !contentArea.Items.Any())
                return true;

            int counter = contentArea.FilteredItems.Select(x => x.GetContent()).Count(c => c.GetType().BaseType == ItemType);
            return counter <= ItemMaximum;
        }
    }
How to use the attribute on a property

For each type you need to be restricted inside the ContentArea, the property must be tagged like this:

[MaxOfItem(itemType: typeof([selected type]), itemMax: 3)]

To restrict more types, just add one MaxOfItem per type.

    [AllowedTypes(typeof(ArticleTeaserBlock), typeof(ProfilePage))]
    [Display(Order = 100, GroupName = SystemTabNames.Content)]
    [MaxOfItem(typeof(ProfilePage), itemMax: 2)]
    [MaxOfItem(itemType: typeof(ArticleTeaserBlock), itemMax: 5)]
    public virtual ContentArea MyProp { get; set; }

I’m here using content in language file for giving the error message, as this is made for a multi-lingual site. The result looks like this:

maxitemerror

If you for some strange reason do not want/can use language files for this, just change FormatErrorMessage to this:

public override string FormatErrorMessage(string name)
{
    return $"Too many elements selected in field {name}. Maximum is {ItemMaximum} of type {ItemType.Name}";
}