Episerver Commerce custom tab ordering

Changing the order of built-in groups is extremely simple in Episerver, it's just a case of assigning a string the same value, decorating the class with a GroupDefinitionsAttribute and using a DisplayAttribute to set the new order. You can find the full details in the documentation so I'm not going to delve into it here.

I assumed the same would work for Commerce tabs, however, doing the following:

using EPiServer.Commerce.Catalog.DataAnnotations;

[GroupDefinitions]
public static class CustomTabNames
{
    [Display(Order = 10)]
    public const string Assets = TabNames.Assets;
}

Produced an exception:

EPiServer.DataAbstraction.RuntimeModel.SynchronizationException: The 'Assets' has been defined more than once, in the 'CustomTabNames' and  in the 'EPiServer.Commerce.Catalog.DataAnnotations.TabNames'.

This exception is unique to the default Commerce tabs beacause they are decorared with a GroupDefinitionsAttribute, which isn't actually the case for the CMS tab names (EPiServer.DataAbstraction.SystemTabNames).

I found an old-ish blog post by Māris Krivtežs documenting the same thing, but his usage of an initializable module and ITabDefinitionRepository had absolutely no impact on the order of my Episerver Commerce tabs even though I could see the database had updated (it's also worth nothing that the blog post was originally for Episerver 7.5). I couldn't understand how this could be the case, so I decided to dig a little deeper into it (i.e. opening a decompiler).

Group Definitions Initialization

It made sense to start from the beginning and investigate what happens when you decorate a class with the GroupDefinitionsAttribute. To simplify, it looks something like this:

Group Definitions initialization sequence

Interestingly it never calls the ITabDefinitionRepository and, on top of that, the DefaultGroupDefinitionRepository (EPiServer.DataAbstraction.Internal) just stores all the GroupDefinitions in a ConcurrentDictionary when calling Save().

The TabDefinitionRepository behavior

Ok, now we're getting somewhere. But why, if we've managed to update the database, aren't those changes reflected in edit mode? Well, that's because the TabDefinitionRepository List() method calls the GroupDefinitionRepository and merges in several GroupDefinition properties including the SortIndex/Order:

protected virtual void Merge(TabDefinition tabDefinition)
{
    GroupDefinition groupDefinition = _groupDefinitionRepository.Load(tabDefinition.Name);
    if (groupDefinition != null)
    {
        tabDefinition.DisplayName = groupDefinition.DisplayName;
        tabDefinition.SortIndex = groupDefinition.Order;
        tabDefinition.RequiredAccess = groupDefinition.RequiredAccess;
    }
}

That explains why updating a value in the ITabDefinitionRepository didn't change anything, the repository (by default) prefers GroupDefinition values over whatever is persisted in the database (tblPropertyDefinitionGroup).

A solution

The obvious solution is therefore just to create an initializable module with a ModuleDependency on ModelSyncInitalization that updates the sort order to whatever we want. However, doing that also didn't work as expected, until I realized that the TabDefinitionRepository was caching results—the final solution (that I went with) therefore looks like this:

public class CommerceTabOrderInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
		var groupDefinitionRepository = context.Locate.Advanced.GetInstance<GroupDefinitionRepository>();

		var tabOrders = new Dictionary<string, int>
        {
			{ TabNames.Assets, 5 },
			{ TabNames.Variants, 10 }
		};

		foreach (var tabOrder in tabOrders)
		{
			var groupDefinition = groupDefinitionRepository.Load(tabOrder.Key);

			if (groupDefinition == null || groupDefinition.Order == tabOrder.Value)
			{
				continue;
			}

			groupDefinitionRepository.Delete(groupDefinition);

			groupDefinition = new GroupDefinition(groupDefinition.Name, groupDefinition.DisplayName,
				groupDefinition.RequiredAccess, tabOrder.Value);

			groupDefinitionRepository.Save(groupDefinition);
		}

        // Ensure we clear the cache
		var cachedRepository = context.Locate.Advanced.GetInstance<ITabDefinitionRepository>() as ICachedRepository;
		cachedRepository?.ClearCache();
	}

    public void Uninitialize(InitializationEngine context)
    {
    }
}

This worked for me, and I finally got to customize the order of the Commerce tabs. I'd be interested if there is a quicker way of doing this, but I couldn't see anything easier after looking at the code.

Comments

There are zero comments 😢