Handling EPiServer UI descriptors for built-in types like PageData

Handling EPiServer UI descriptors for built-in types like PageData

When you want to make content types appear or behave differently in EPiServer - for example changing the default view or disable sticky view - you would create a UI Descriptor for that specific content type. If you want to change the default for all content types without having to remember to add an interface or base class then you could create a UI Descriptor for PageData.

However, you might experience weird behavior where it only works sometimes. This post is about explaining why and how to work around it.

The issue

Let us start with an example of changing the publish view (the view used after publishing a page) by using an interface IAllPropertiesPublishView.

/// <summary>
/// Interface for changing PublishView to AllPropertiesView.
/// </summary>
public interface IAllPropertiesPublishView { }

[UIDescriptorRegistration]
public class AllPropertiesPublishViewDescriptor : UIDescriptor<IAllPropertiesPublishView>
{
    public AllPropertiesPublishViewDescriptor()
    {
        PublishView = CmsViewNames.AllPropertiesView;
    }
}

We can add this interface to all the page types where we want to change the publish view to the All Properties view.

Now, if we want this to apply to all page types extending PageData instead of having to remember to add the interface, then we can change this to inherit UIDescriptor<PageData> instead. That should work, right? It will. Sometimes. It depends on the order of initialization/assembly scanning.

If you enable debug logging you should see a message in the log like Discarding ui descriptor registration for type {0} provided by {1} since a desciptor for the type is already registered.

EPiServer already has an internal UI Descriptor, PageUIDescriptor, which has some default configuration for PageData:

[UIDescriptorRegistration]
public class PageUIDescriptor : UIDescriptor<PageData>, IEditorDropBehavior
{
    public PageUIDescriptor()
      : base("epi-iconObjectPage")
    {
        IsPrimaryType = true;
        CommandIconClass = "epi-iconPage";
        AllowSelectItSelf = true;
        ContainerTypes = new [] { typeof (PageData) };
        EditorDropBehaviour = EditorDropBehavior.CreateLink;
    }

    public bool AllowSelectItSelf { get; private set; }

    public EditorDropBehavior EditorDropBehaviour { get; set; }
}

When the site starts it is uncertain if the default UI Descriptor or our custom one will be loaded first. Only the first one will be registered and initialized; others for the same specific type will be skipped and a debug message will be written to the logs.

A solution

One way around this issue is to create an initialization module and modify the already registered and initialized UI Descriptors.

[InitializableModule]
[ModuleDependency(typeof(DataInitialization))]
public class PageDataDefaultViewsInitialization : IInitializableModule
{
    public override void Initialize(InitializationEngine context)
    {
        // Get the UIDescriptorRegistry instance
        var registry = context.Locate.Advanced.GetInstance<UIDescriptorRegistry>();

        // Get the UIDescriptor for PageData
        var descriptor = registry.UIDescriptors
            .FirstOrDefault(x => x.ForType == typeof(PageData));

        // Abort if it didn't exist
        if (descriptor == null)
            return;

        // Modify the descriptor as you please
        descriptor.DefaultView = CmsViewNames.AllPropertiesView;
        descriptor.PublishView = CmsViewNames.AllPropertiesView;
    }

    public void Uninitialize(InitializationEngine context)
    {

    }
}

The code is rather simple. We are just getting the UIDescriptorRegistry instance and then getting the first UIDescriptor for PageData (if there is any). If it exists we then modify it however we please, as we would have done in our custom UIDescriptor.

Accessing the UIDescriptorRegistry.UIDescriptors property is what triggers the loading/initialization of all the UI Descriptors.

This approach can also be used if you need some more generic logic for customizing UI Descriptors, e.g. depending on type name or for 3rd party types that you don't control.

Conclusion

There can only be one UI Descriptor per specific type/interface. If there are multiple descriptors only one will be registered and initialized; the others will be skipped. It looks like it depends on a race condition in the type scanning which descriptor will be loaded first, so it can change between site restarts.

One solution is to instead modify the registered UI Descriptors by finding the ones you care about from the UIDescriptorRegistry.

Bonus info

The priority of UI Descriptors, i.e. which UI Descriptor is chosen for a specific type, is controlled by the (private) UIDescriptorRegistry.TypeComparer:

public override int Compare(UIDescriptor x, UIDescriptor y)
{
    if (x == y)
        return 0;
    Type forType1 = x.ForType;
    Type forType2 = y.ForType;
    return forType1.IsInterface && !forType2.IsInterface || forType1.IsAssignableFrom(forType2) ? 1 : -1;
}

In short, the more specific your UI Descriptor is, the higher priority.

Let's say we the following classes and interfaces:

public class CustomPage : BasePage, ISomeInterface
{
}

public interface ISomeInterface : IBase { }

UI Descriptors for all those would be prioritized in this order:

  1. UIDescriptor<CustomPage>
  2. UIDescriptor<BasePage>
  3. UIDescriptor<ISomeInterface>
  4. UIDescriptor<IBase>

So, concrete types are above interfaces. Interfaces inheriting another interface before the inherited interface etc.