EPiServer 7.5 introduces the AllowedTypes attribute. This accepts an array of types, effectively making a whitelist of blocks that can be added to a ContentArea. An editor attempting to drag and drop a block not included in the type array will see the block turn grey and will not be able to place it. Here’s an example of its use:
Display( Name = "My Content Area", GroupName = SystemTabNames.Content, Order = 100)] [AllowedTypes(new[] { typeof(AllowedBlock), typeof(AlsoAllowedBlock) })] public virtual ContentArea MyContentArea { get; set; }
However there is a problem with this attribute. It only restricts block placement when dragging and dropping existing blocks in a content area. An editor can still create a new block directly on the content area, which is frankly a bit of a headache, as we can’t rely on the attribute to enforce block placement rules.
A crude workaround
I’ve worked around this issue by creating a custom validation attribute for content areas. This will prevent content from being saved if a content area contains a disallowed block. Here’s the code:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class AllowedBlocksAttribute : ValidationAttribute { private readonly Type[] _allowedBlocks; private List AllowedBlockTypeFullNames { get { return _allowedBlocks.Select(a => a.FullName).ToList(); } } public AllowedBlocksAttribute(Type[] allowedBlocks) { _allowedBlocks = allowedBlocks; ErrorMessage = "This content area can only accept the following block types: {0}"; } public override string FormatErrorMessage(string name) { return string.Format(CultureInfo.CurrentCulture, ErrorMessage, FormattedAllowedBlockTypes); } public override bool IsValid(object value) { var contentArea = value as ContentArea; if (contentArea == null) return true; foreach (var item in contentArea.Items) { if (!AllowedBlockTypeFullNames.Contains(item.GetContent().GetOriginalType().FullName)) return false; } return true; } private string FormattedAllowedBlockTypes { get { return string.Join(", ", _allowedBlocks.Select(s => s.ToString().Split('.').Last().ToCamelCase())); } } }
This functions like any other validation attribute. If validation fails while saving content, a notification is displayed in the notification area at the top right of the page. It complements the existing AllowedTypes attribute, so ideally it should be used wherever that attribute is placed, eg
Display( Name = "My Content Area", GroupName = SystemTabNames.Content, Order = 100)] [AllowedTypes(new[] { typeof(AllowedBlock), typeof(AlsoAllowedBlock) })] [AllowedBlocks(new[] { typeof(AllowedBlock), typeof(AlsoAllowedBlock) })] public virtual ContentArea MyContentArea { get; set; }
Suggested improvements
There’s some scope for improvement here. Firstly, having to add two attributes with the same array of allowed types is somewhat clunky. Any suggestions as to how I could combine this with the existing attribute are welcome.
Secondly, although it stops content being saved with unwanted blocks, it doesn’t prevent an editor from creating said blocks. So although it acts as a final gatekeeper, it can lead to a frustrating experience for editors. It would be better if there’s a way of preventing the editor from creating the block in the first place. Ideally the disallowed blocks would not be available in the list of blocks when creating one on the content area, but I haven’t figured out a way of doing that yet.
Finally, you may have noticed that I’m crudely constructing a block name for display in the error message from its type name. This is because I couldn’t work out how to get the block name from its type definition. Now, surely there’s a way of doing that so I’d be grateful if someone could point me in the right direction. It would also mean I could get rid of this wee beastie:
////// Splits a string on humps in a camel case word, eg camelCaseWord => camel Case Word /// /// The camel case string to split ///The string, with a space between every incidence of a lower case letter and an upper case letter public static string ToCamelCase(this string input) { return System.Text.RegularExpressions.Regex.Replace(input, "(?<=[a-z])([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim(); }