Injected AllowedTypes/AvailableContentTypes Revisited

I wrote a pretty long post about how you could "inject" AllowedTypes/AvailableContentTypes at runtime.

I got it to work for AvailableContentTypes but yesterday when I was about to try it out in a real project I noticed that my AllowedTypes implementation worked only until I tried to save the page. I would get an validation error saying that my injected allowedtypes wasn't allowed to be saved.

My class looks like this, when creating the contenttype I specify that my ContentArea can only contain VideoBlocks.

[ContentType(DisplayName = "Media Page", GUID = "5b393025-d856-4392-aa99-61f1a468ea79", Description = "")]
[AvailableContentTypes(Include = new [] {typeof(MediaPage) })]
public class MediaPage : PageData
{
    [AllowedTypes(AllowedTypes = new[] {typeof(VideoBlock)})]
    public virtual ContentArea ContentArea { get; set; }
    public virtual IList<ContentReference> Items { get; set; }
    public virtual ContentReference ContentReference { get; set; }
}

I would then inject another type when starting up the application(see my previous post) and that would allow me to add other types to the contentarea, it would however NOT allow me to save the page.

I did some decompiling of the AllowedTypesAttribute and found the following:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
    if (value == null)
        return (ValidationResult) null;
    foreach (IAllowedTypesValidator allowedTypesValidator in this.AllowedTypesValidators.Services)
    {
        if (allowedTypesValidator.CanValidate(value))
            return allowedTypesValidator.IsValid(value, validationContext, (IEnumerable<Type>) this.AllowedTypes, (IEnumerable<Type>) this.RestrictedTypes);
    }
  return (ValidationResult) null;
}

See the problem? The validation uses this.AllowedTypes and this.RestrictedTypes when getting the Allowed/RestrictedTypes. This means that it will ONLY allow types specified directly on the property with the AllowedTypesAttribute, NOT the types that I inject. That sucks.

Since the AllowedTypesAttribute is sealed I can't do anything with that class, I can't inherit/override anything.

So I created my own attribute, InjectedAllowedTypes.
It behaves almost exactly like the AllowedTypesAttribute but I've changed the OnMetadataCreated and IsValid-method a bit.

OnMetaDataCreated

public void OnMetadataCreated(ModelMetadata metadata)
{
    var extendedMetadata = metadata as ExtendedMetadata;
    if (extendedMetadata == null)
    {
        return;
    }

    var suffix = this.TypesFormatSuffix ?? (extendedMetadata.AdditionalValues.ContainsKey("TypesFormatSuffix") ?
                extendedMetadata.AdditionalValues["TypesFormatSuffix"] as string :
                null);

    var allowedTypes = this.AllowedTypes.Select(a => a.FullName.ToLowerInvariant()).ToList();
    var restrictedTypes = this.RestrictedTypes.Select(r => r.FullName.ToLowerInvariant()).ToList();

    var injectedAllowedTypesAttribute = InjectedAllowedTypes.GetInjectedAllowedTypesAttribute(extendedMetadata.ContainerType, extendedMetadata.PropertyName);

    if (injectedAllowedTypesAttribute != null)
    {
        allowedTypes =
                allowedTypes.Concat(
                    injectedAllowedTypesAttribute.AllowedTypes.Select(x => x.FullName.ToLowerInvariant()))
                    .Distinct()
                    .ToList();

            restrictedTypes =
                restrictedTypes.Concat(injectedAllowedTypesAttribute.RestrictedTypes.Select(x => x.FullName.ToLowerInvariant()))
                    .Distinct()
                    .ToList();
   }


   if (!string.IsNullOrEmpty(suffix))
   {
       allowedTypes = allowedTypes.ToArray().Select(a => a + "." + suffix).ToList();
       restrictedTypes = restrictedTypes.ToArray().Select(a => a + "." + suffix).ToList();
   }

   var allowedTypesArray = allowedTypes.ToArray();
   var restrictedTypesArray = restrictedTypes.ToArray();

   extendedMetadata.EditorConfiguration["AllowedTypes"] = allowedTypesArray;
   extendedMetadata.EditorConfiguration["RestrictedTypes"] = restrictedTypesArray;
   extendedMetadata.EditorConfiguration["AllowedDndTypes"] = allowedTypesArray;
   extendedMetadata.EditorConfiguration["RestrictedDndTypes"] = restrictedTypesArray;
   extendedMetadata.OverlayConfiguration["AllowedDndTypes"] = allowedTypesArray;
   extendedMetadata.OverlayConfiguration["RestrictedDndTypes"] = restrictedTypesArray;
}

IsValid

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
    if (value == null) return null;

    var contentLocator = ServiceLocator.Current.GetInstance<IContentLoader>();
    var validationMessage = string.Empty;
    var stringBuilder = new StringBuilder();
    var allowedTypes = this.AllowedTypes;
    var restrictedTypes = this.RestrictedTypes;
    var contentReferences = new List<ContentReference>();

    if (value is ContentArea)
    {
        var contentArea = value as ContentArea;
        contentReferences = contentArea.Items.Select(x => x.ContentLink).ToList();
    }
    else if (value is IEnumerable<ContentReference>)
    {
        var references = value as IEnumerable<ContentReference>;
        contentReferences = references.ToList();
    }
    else if (value is ContentReference)
    {
        var contentReference = value as ContentReference;
        contentReferences.Add(contentReference);
    }

    foreach (var contentReference in contentReferences)
    {
        var content = contentLocator.Get<IContent>(contentReference);
        var type = content.GetOriginalType();
        var injectedAllowedTypesAttribute = InjectedAllowedTypes.GetInjectedAllowedTypesAttribute(validationContext.ObjectInstance.GetOriginalType(),
                    validationContext.MemberName);

        if (injectedAllowedTypesAttribute != null)
        {
            allowedTypes =            allowedTypes.Concat(injectedAllowedTypesAttribute.AllowedTypes).Distinct().ToArray();
            restrictedTypes =            restrictedTypes.Concat(injectedAllowedTypesAttribute.RestrictedTypes).Distinct().ToArray();
        }

        if (restrictedTypes.Contains(type) || !allowedTypes.Contains(type))
        {
            var message =
                    string.Format(        LocalizationService.Current.GetString("/injectedallowedtypes/errormessages/notallowed"), type.Name, validationContext.MemberName);
            validationMessage = stringBuilder.Append(message).AppendLine(".").ToString();
        }
    }

    if (string.IsNullOrEmpty(validationMessage))
    {
        return null;
    }

    var validationResult = new ValidationResult(validationMessage);
    return validationResult;
}

I've basically made OnMetaDataCreated and IsValid aware of the injected types as well, not only the types specified with attributes.

To inject some types you would install my nuget packet and then create a InitializationModule and register your own types like this.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Shell.ShellInitialization))]
public class InjectedAllowedTypesInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        InjectedAvailableModelSettings.RegisterCustomAvailableModelSettings(new Dictionary<Type, ContentTypeAvailableModelSetting>
        {

            {
                typeof(MediaPage) , new ContentTypeAvailableModelSetting
                                    {
                                        Availability = Availability.Specific,
                                        Included = new HashSet<Type> {typeof(CoolPage) }
                                    }
            }
        });

        InjectedAllowedTypes.RegisterInjectedAllowedTypesAttributes(new Dictionary<string, AllowedTypesAttribute>
        {
            {
                string.Format("{0}.{1}",typeof(MediaPage).Name, "ContentArea"), new AllowedTypesAttribute
                {
                    AllowedTypes = new [] {typeof(MusicBlock)}
                }
            },
            {
                string.Format("{0}.{1}", typeof(MediaPage).Name, "Items"), new AllowedTypesAttribute
                {
                    AllowedTypes = new [] {typeof(CoolPage)}
                }
            },
            {
                string.Format("{0}.{1}", typeof(MediaPage).Name, "ContentReference"), new AllowedTypesAttribute
                {
                    AllowedTypes = new [] {typeof(CoolPage)}
                }
            }
        });
    }

    public void Uninitialize(InitializationEngine context)
    {
    }

    public void Preload(string[] parameters)
    {
    }
}

So my MediaPage now looks like this

[ContentType(DisplayName = "Media Page", GUID = "5b393025-d856-4392-aa99-61f1a468ea79", Description = "")]
[AvailableContentTypes(Include = new [] {typeof(MediaPage) })]
public class MediaPage : PageData
{
    [InjectedAllowedTypes(AllowedTypes = new[] {typeof(VideoBlock)})]
    public virtual ContentArea ContentArea { get; set; }
    public virtual IList<ContentReference> Items { get; set; }
    public virtual ContentReference ContentReference { get; set; }
}

The ContentArea would look like this in Editmode, allowing me to create a VideoBlock(because of the attribute) and also a MusicBlock(because Im injecting it in RegisterInjectedAllowedTypesAttributes above)
Filtered blockview in editmode when creating new blocks

The ContentReferenceList would look like this because of the injected allowedtypes(only allowing the CoolPagetype)
Filtered contentreferencelist only allowing CoolPage

The ContentReference would look like this because of the injected allowedtypes(only allowing the CoolPagetype)
Filtered contentreference picker only allowing CoolPage

You can find all code on github and you can get the nuget package here

Injected AllowedTypes/AvailableContentTypes Revisited
Share this