Handling EPiServer UI descriptors for built-in types like PageData

Search for a command to run...

No comments yet. Be the first to comment.
Polly is great library for easily configuring different kinds of policies around code execution, be it retry, circuit breaker, caching or lots of other things. It's probably most often used around HTTP requests. If you haven't heard about it you defi...

This is part 2 of my series on unit testing in .NET (C#) and will be about my conventions regarding naming unit tests, how to structure them and which parts to test. And lastly I will help you increase readability of assertions and potential error me...

I feel like unit testing is very important and very beneficial in a lot of ways. It helps with not breaking existing functionality and thereby also making you more confident in changing code without fear of breaking important stuff. Often it also mak...

I've been working with Swagger through NSwag a lot recently and also needed to get it to work when hidden behind a reverse proxy - i.e. another service forwarding a request to the service exposing the Swagger UI. Some of the issues I've had along the...

The other day I was writing a unit test to verify what was supposed to happen when a Task would throw an exception when awaited. It wasn't as straight forward as I first assumed, but I quickly found this Stack Overflow post that helped me: https://st...

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.
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
debuglogging you should see a message in the log likeDiscarding 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.
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.UIDescriptorsproperty 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.
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.
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:
UIDescriptor<CustomPage>UIDescriptor<BasePage>UIDescriptor<ISomeInterface>UIDescriptor<IBase>So, concrete types are above interfaces. Interfaces inheriting another interface before the inherited interface etc.