Imagine the following project structure(every project starting with Feature is a nuget package referenced in the www project in the real solution):

Project structure

A feature could be a Page or a Block or something similar, you get the idea.
Since all features have their own project, they don't know about each other.

This structure allows us to cherry pick features when creating a new site by simply adding/removing stuff from our packages.config. It's pretty sweet.

There's a problem though. Since our features doesn't know about each other, we can't use the AllowedTypes/AvailableContentTypes attributes. Or can we?

I had a discussion with a colleague at work about how to solve this, this is what we came up with, kind of. :)

We're going to focus on the MediaPage. It looks like this:

[ContentType(DisplayName = "Media Page", GUID = "5b393025-d856-4392-aa99-61f1a468ea79", Description = "A page listing media.")]
public class MediaPage : PageData  
{
    [AllowedTypes(AllowedTypes = new [] {typeof(VideoBlock)})]
    public virtual ContentArea ContentArea { get; set; }
}

The AvailableContentTypes problem.

We want to restrict which pagetypes the editors can create under the MediaPage. We only want them to be able to create pages of the CoolPage type below our MediaPages.

If our features knew about each other we would simply do it like this:

[ContentType(DisplayName = "Media Page", GUID = "5b393025-d856-4392-aa99-61f1a468ea79", Description = "A page listing media.")]
[AvailableContentTypes(Include = new []{typeof(CoolPage)})]
public class MediaPage : PageData  
{
    [AllowedTypes(AllowedTypes = new [] {typeof(VideoBlock)})]
    public virtual ContentArea ContentArea { get; set; }
}

But since our features doesn't know about each other, we can't do that.

The AvailableContentTypes solution.

Note: All files for this solution can be found on GitHub.

I did some reflection and looked for usages of the AvailableContentTypesattribute. I found a repository named AvailableModelSettingsRepository. That repository contains the following public virtual methods:

  • void RegisterSetting(Type model, IContentTypeAvailableModelSetting modelSetting)
  • IContentTypeAvailableModelSetting GetSetting(Type model)
  • ContentTypeAvailableModelSetting GetRuntimeSetting(Type model)
  • IDictionary<Type, ContentTypeAvailableModelSetting> ListRuntimeSettings()

The RegisterSetting method takes a IContentTypeAvailableModelSetting and saves it. This looks like a good method to override and add some custom logic. I did just that and implemented some of the private classes that the method used, but I just couldn't get it to work(my custom modelSetting got ignored). I got really tired of fiddling with that method so I changed my approach.

Meet ListRuntimeSettings.

When going into Edit mode in EPiServer, this method gets called once and returns a list of all registered ContentTypes and their AvailableModelSettings.
List of ContentTypes and their AvailableModelSettings

To solve my problem, I needed to somehow modify that list and add my own AvailableModelSettings for specific ContentTypes. This is how I did it.

I created a InjectedAvailableModelSettingsRepository which inherits from AvailableModelSettingsRepository.

I then created the following method

public override IDictionary<Type, ContentTypeAvailableModelSetting> ListRuntimeSettings()  
{
    var runtimeSettings = base.ListRuntimeSettings();
    foreach (var customSetting in CustomSettings)
    {
        if (runtimeSettings.ContainsKey(customSetting.Key))
        {
            var merged = MergeSettings(customSetting.Value, runtimeSettings[customSetting.Key]);
            runtimeSettings[customSetting.Key] = merged;
        }
    }

    return runtimeSettings;
}

The method calls the base implementation and then modifies the return value before returning it.
CustomSettings is a static field looking like this: private static readonly Dictionary<Type, ContentTypeAvailableModelSetting> CustomSettings = InjectedAvailableModelSettings.GetCustomAvailableModelSettings();

The MergeSettings method looks like this:

private ContentTypeAvailableModelSetting MergeSettings(ContentTypeAvailableModelSetting customSetting, ContentTypeAvailableModelSetting runtimeSetting)  
{
    var mergedSetting = new ContentTypeAvailableModelSetting();
    mergedSetting.Excluded = new HashSet<Type>(customSetting.Excluded.Concat(runtimeSetting.Excluded).Distinct());
    mergedSetting.Included = new HashSet<Type>(customSetting.Included.Concat(runtimeSetting.Included).Distinct());
    mergedSetting.IncludedOn = new HashSet<Type>(customSetting.IncludedOn.Concat(runtimeSetting.IncludedOn).Distinct());
    mergedSetting.Availability = customSetting.Availability;

    return mergedSetting;
}

InjectedAvailableModelSettings is a static class where all custom "injection" of AvailableModelSettings happens, it looks like this:

public static class InjectedAvailableModelSettings  
{
    public static Dictionary<Type, ContentTypeAvailableModelSetting> GetCustomAvailableModelSettings()
    {
        var mappedModelSettings = MappedModelSettings();
        return mappedModelSettings;
    }

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

In the MappedModelSettings you specify which type(MediaPage in this case) and how the ContentTypeAvailableModelSetting should look like.
Since we wanted to restrict the pagetypes under the MediaPage to only allow the CoolPagetype we set the Included property to new HashSet<Type>{typeof(CoolPage)}

And that's pretty much it, only thing left to do is replacing the AvailableModelSettingsRepository with our own implementation.

The following code is added to the ConfigureContainer method in the DependencyResolverInitializationfile in the Alloy project.
container.For<IAvailableModelSettingsRepository>().Use<InjectedAvailableModelSettingsRepository>();

If we now try to create a new page under a MediaPage in edit mode it will look like this
Creation of new CoolPage under a MediaPage instead of this
Creation of a new page under a MediaPage, not filtered view

SUCCESS!


The AllowedTypes problem.

Note: If there's a simpler/cleaner way of doing this, please let me know :).

We are using the AllowedTypes attribute to restrict what the editors can add to the ContentArea, we are only allowing the VideoBlock type in the ContentArea.

Now the editors wants to add MusicBlocks as well, can you fix that Josef? Quickfix, right?

Sure, let's just add the MusicBlock type to the AllowedTypes like this:

[ContentType(DisplayName = "Media Page", GUID = "5b393025-d856-4392-aa99-61f1a468ea79", Description = "A page listing media.")]
public class MediaPage : PageData  
{
    [AllowedTypes(AllowedTypes = new [] {typeof(VideoBlock), typeof(MediaBlock)})]
    public virtual ContentArea ContentArea { get; set; }
}

but...that wont work without adding a reference to the Feature.MusicBlock. And we don't want to do that since our features should be isolated from each other.

So, let's fix the problem.

The AllowedTypes solution.

*Note: *All code for this section can be found on Github

I did some decompiling(and some praying) and looked for usages of the AllowedTypes attribute. I found the class ContentDataAttributeScanningAssigner and the method AssignValuesToPropertyDefinition.
It looks like this(decompiled so it looks a bit...odd):

public virtual void AssignValuesToPropertyDefinition(PropertyDefinitionModel propertyDefinitionModel, PropertyInfo property, ContentTypeModel parentModel)  
{
    ...//Some code above, not important
    foreach (Attribute attribute1 in Attribute.GetCustomAttributes((MemberInfo) property, true))
    {
        ...//Some code above not important
        var attribute2 = attribute1 as AllowedTypesAttribute;
        if (attribute2 != null)
        {
            VerifyAllowedTypesAttribute(attribute2, property);
        }
        ...//More code below not important
        propertyDefinitionModel.Attributes.AddAttribute(attribute1);
    }
}

The code loops through all CustomAttributes and does some type checking. If the attribute is a AllowedTypesAttribute it will call the method VerifyAllowedTypesAttribute. That method just validates that the type that the attribute is placed on is of a valid type(ContentReference, ContentArea or IEnumerable<ContentReference>) and also that the types specified in the AllowedTypes and RestrictedTypes on the AllowedTypesAttribute are valid(needs to inherit from IContent).

It will then add the attribute to the propertyDefinitionModel.Attributes.

Im interested in altering the attribute before it gets added to the propertyDefinition.
The AssignValuesToPropertyDefinition method is both public and virtual, so it looks like a good method to ATTACK.

I created the following class(just scroll through it, I will break it down further down):
Full file: InjectedContentDataAttributeScanningAssigner

public class InjectedContentDataAttributeScanningAssigner : ContentDataAttributeScanningAssigner  
{
    /// <summary>
    /// Almost exact implementation of the AssignValuesToPropertyDefinition in the ContentDataAttributeScanningAssigner
    /// the only thing that differs is the added call to CustomAllowedTypes.GetMergedAllowedTypesAttribute.
    /// That call allows us to add more types to the Allowed/RestricedTypes without using the AllowedTypes attribute.
    /// </summary>
    /// <param name="propertyDefinitionModel"></param>
    /// <param name="property"></param>
    /// <param name="parentModel"></param>
    public override void AssignValuesToPropertyDefinition(PropertyDefinitionModel propertyDefinitionModel, PropertyInfo property, ContentTypeModel parentModel)
    {
        if (property.IsAutoGenerated() && !property.IsAutoVirtualPublic())
        {
            var exceptionMessage = string.Format(CultureInfo.InvariantCulture,
                    "The property '{0}' on the content type '{1}' is autogenerated but not virtual declared.",
                    property.Name, property.DeclaringType.Name);
            throw new InvalidOperationException(exceptionMessage);
        }
        //This is our added logic to merge a predefined AllowedTypes attribute with our own AllowedTypes specified in code.
        #region InjectedAllowedTypes
        var customAttributes = Attribute.GetCustomAttributes(property, true).ToList();
        if (customAttributes.Any(x => x is AllowedTypesAttribute))
        {
            var existingAllowedTypesAttribute =
                customAttributes.FirstOrDefault(x => x is AllowedTypesAttribute) as AllowedTypesAttribute;

            var mergedAllowedTypesAttribute = InjectedAllowedTypes.GetMergedAllowedTypesAttribute(existingAllowedTypesAttribute, parentModel, property);
            customAttributes.Remove(existingAllowedTypesAttribute);
            customAttributes.Add(mergedAllowedTypesAttribute);
        }
        else
        {
            var mergedAllowedTypesAttribute = InjectedAllowedTypes.GetMergedAllowedTypesAttribute(null, parentModel, property);

            if (mergedAllowedTypesAttribute != null)
            {
                customAttributes.Add(mergedAllowedTypesAttribute);
            }
        }
        #endregion

        foreach (var attribute in customAttributes)
        {
            if (attribute is BackingTypeAttribute)
            {
                var backingTypeAttribute = attribute as BackingTypeAttribute;
                if (backingTypeAttribute.BackingType != null)
                {
                    if (!typeof(PropertyData).IsAssignableFrom(backingTypeAttribute.BackingType))
                    {
                        var exceptionMessage = string.Format(CultureInfo.InvariantCulture,
                                "The backing type '{0}' attributed to the property '{1}' on '{2}' does not inherit PropertyData.",
                                backingTypeAttribute.BackingType.FullName, property.Name, property.DeclaringType.Name);
                        throw new TypeMismatchException(exceptionMessage);
                    }

                    if (property.IsAutoVirtualPublic())
                    {
                        ValidateTypeCompability(property, backingTypeAttribute.BackingType);
                    }
                }
                propertyDefinitionModel.BackingType = backingTypeAttribute.BackingType;
            }
            else if (attribute is AllowedTypesAttribute)
            {
                var allowedTypesAttribute = attribute as AllowedTypesAttribute;
                VerifyAllowedTypesAttribute(allowedTypesAttribute, property);
            }
            else if (attribute is DisplayAttribute)
            {
                var displayAttribute = attribute as DisplayAttribute;
                propertyDefinitionModel.DisplayName = displayAttribute.GetName();
                propertyDefinitionModel.Description = displayAttribute.GetDescription();
                propertyDefinitionModel.Order = displayAttribute.GetOrder();
                propertyDefinitionModel.TabName = displayAttribute.GetGroupName();
            }
            else if (attribute is ScaffoldColumnAttribute)
            {
                var scaffoldColumnAttribute = attribute as ScaffoldColumnAttribute;
                propertyDefinitionModel.AvailableInEditMode = scaffoldColumnAttribute.Scaffold;
            }
            else if (attribute is CultureSpecificAttribute)
            {
                var specificAttribute = attribute as CultureSpecificAttribute;
                ThrowIfBlockProperty(specificAttribute, property);
                propertyDefinitionModel.CultureSpecific = specificAttribute.IsCultureSpecific;
            }
            else if (attribute is RequiredAttribute)
            {
                var requiredAttribute = attribute as RequiredAttribute;
                ThrowIfBlockProperty(requiredAttribute, property);
                propertyDefinitionModel.Required = true;
            }
            else if (attribute is SearchableAttribute)
            {
                var searchableAttribute = attribute as SearchableAttribute;
                ThrowIfBlockProperty(searchableAttribute, property);
                propertyDefinitionModel.Searchable = searchableAttribute.IsSearchable;
            }
            else if (attribute is UIHintAttribute)
            {
                var uiHintAttribute = attribute as UIHintAttribute;
                if (!string.IsNullOrEmpty(uiHintAttribute.UIHint))
                {
                    if (string.Equals(uiHintAttribute.PresentationLayer, "website"))
                    {
                        propertyDefinitionModel.TemplateHint = uiHintAttribute.UIHint;
                    }
                    else if (string.IsNullOrEmpty(uiHintAttribute.PresentationLayer) &&
                                 string.IsNullOrEmpty(propertyDefinitionModel.TemplateHint))
                    {
                        propertyDefinitionModel.TemplateHint = uiHintAttribute.UIHint;
                    }
                }
            }

            propertyDefinitionModel.Attributes.AddAttribute(attribute);
        }
    }

    //A bunch of private helper functions below just calling the parent class with reflection. Check the gist to see all code.
}

The AssignValuesToPropertyDefinition is overridden and does the exact same thing as the base method except for one thing.
I've added some code before the looping of the CustomAttributes. Im looking for the AllowedTypesAttribute. If found, I'll replace it with a merged one specified in GetMergedAllowedTypesAttribute.
If not found, I'll still look in the GetMergedAllowedTypesAttribute and add a new AllowedTypes attribute if I've specified anything for that particular contenttype/property.

...//More code above
//This is our added logic to merge a predefined AllowedTypes attribute with our own AllowedTypes specified in code.
#region InjectedAllowedTypes
var customAttributes = Attribute.GetCustomAttributes(property, true).ToList();  
if (customAttributes.Any(x => x is AllowedTypesAttribute))  
{
    var existingAllowedTypesAttribute = customAttributes.FirstOrDefault(x => x is AllowedTypesAttribute) as AllowedTypesAttribute;
    var mergedAllowedTypesAttribute = InjectedAllowedTypes.GetMergedAllowedTypesAttribute(existingAllowedTypesAttribute, parentModel, property);
    customAttributes.Remove(existingAllowedTypesAttribute);
    customAttributes.Add(mergedAllowedTypesAttribute);
}
else  
{
    var mergedAllowedTypesAttribute = InjectedAllowedTypes.GetMergedAllowedTypesAttribute(null, parentModel, property);
    if (mergedAllowedTypesAttribute != null)
    {
        customAttributes.Add(mergedAllowedTypesAttribute);
    }
}
#endregion
foreach (var attribute in customAttributes)  
....//More code below

The method InjectedAllowedTypes.GetMergedAllowedTypesAttribute looks for a AllowedTypesAttribute specified in that file, if found, it will merge it with the existing attribute and then return it.
The class looks like this:

public static class InjectedAllowedTypes  
{
    public static AllowedTypesAttribute GetMergedAllowedTypesAttribute(AllowedTypesAttribute allowedTypesAttribute, ContentTypeModel contentTypeModel, PropertyInfo property)
    {
        var allCustomAttributes = GetCustomAllowedTypesAttributes();
        var key = string.Format("{0}.{1}", contentTypeModel.ModelType.Name, property.Name);
        var customAttributeForType = allCustomAttributes.FirstOrDefault(x => x.Key == key);
        if (customAttributeForType.Value == null)
        {
            return allowedTypesAttribute;
        }

        var existingAllowedTypes = allowedTypesAttribute != null ? allowedTypesAttribute.AllowedTypes : new Type[] {};
        var existingRestrictedTypes = allowedTypesAttribute != null ? allowedTypesAttribute.RestrictedTypes : new Type[] {};
        var mergedAllowedTypesAttribute = new AllowedTypesAttribute
        {
                AllowedTypes = existingAllowedTypes.Concat(customAttributeForType.Value.AllowedTypes).Distinct().ToArray(),
                RestrictedTypes = existingRestrictedTypes.Concat(customAttributeForType.Value.RestrictedTypes).Distinct().ToArray()
        };

        //It seems like EPiServer adds IContentData automatically, so we remove that one if we have a custom "attribute" value for the AllowedTypes.
        if (customAttributeForType.Value.AllowedTypes.Any())
        {
            mergedAllowedTypesAttribute.AllowedTypes =
                mergedAllowedTypesAttribute.AllowedTypes.Where(x => x != typeof (IContentData)).ToArray();
        }                

        return mergedAllowedTypesAttribute;
    }

    private static Dictionary<string, AllowedTypesAttribute> GetCustomAllowedTypesAttributes()
    {
        return new Dictionary<string, AllowedTypesAttribute>
        {
            {
                string.Format("{0}.{1}",typeof(MediaPage).Name, "ContentArea"), new AllowedTypesAttribute
                {
                    AllowedTypes = new[] {typeof (MusicBlock)}
                }
            }
        };
    }
}

In the GetCustomAllowedTypesAttributes im specifying that the property ContentArea on the MediaPage should have MusicBlock as an AllowedType. That value will then be merged with the already existing value on the ContentArea property(set with the AllowedTypesAttribute) and then returned to the AssignValuesToPropertyDefinition where it will get saved.

The only thing left to do now is replacing the ContentTypeModelAssigner with my own implementation. In the Alloy project it's easy to do, just add the following in the ConfigureContainer method in the DependencyResolverInitialization.cs file

container.For<IContentTypeModelAssigner>().Use<InjectedContentDataAttributeScanningAssigner>();  

If we now load up the MediaPage in Edit mode it will look like this when trying to add blocks to the contentarea:
Filtered block view for MediaPage

Success!

Some notes.

It sucked that I needed to override the whole AssignValuesToPropertyDefinition method, it would be nice if there were a public virtual SetAllowedTypesAttributes(AllowedTypesAttributes attribute) or something similar.

I also needed to create my own implementation of IsAutoGenerated and IsAutoVirtualPublic since they where marked Internal. I just copied the existing implementation like this:

using System;  
using System.Linq;  
using System.Reflection;  
using System.Runtime.CompilerServices;

namespace ModularAllowedTypes  
{
    public static class PropertyInfoExtensions
    {
        public static bool IsAutoGenerated(this PropertyInfo p)
        {
            if (p.GetGetMethod() != null && p.GetSetMethod() != null && p.GetGetMethod().GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length == 1)
                return p.GetSetMethod().GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length == 1;
            return false;
        }

        public static bool IsAutoVirtualPublic(this PropertyInfo self)
        {
            if (self == null)
                throw new ArgumentNullException("self");
            if (self.IsAutoGenerated())
                return (self.GetAccessors(true)).All(m =>
                {
                    if (m.IsVirtual)
                        return m.IsPublic;
                    return false;
                });
            return false;
        }
    }
}