Sanjay Kumar
+3
Jun 9, 2026
visibility 455
star star star star star
(2 votes)

Extending SelectMany for Multi-Column Checkbox Layouts in Optimizely CMS 12

By default, a SelectMany property is rendered as a vertical checkbox list in the CMS editor. While this works well for a small number of options, usability quickly declines as the list grows. Editors may need to scroll through dozens or even hundreds of items presented in a single column, making selection time-consuming and difficult to navigate. A multi-column layout provides a much better editing experience by improving readability, reducing scrolling, and making large option sets easier to scan and manage.

A similar approach was originally described by Per-Magne Skuseth's 2016 post for legacy EPiServer versions, using a custom  SelectManyExtended  attribute that implemented IMetadataAware While the core concept remains relevant in CMS 12, the underlying APIs and framework architecture have evolved significantly. As a result, the original implementation cannot be copied directly into a modern .NET 8/10 solution without modification, and several parts must be adapted to align with current CMS 12 extensibility patterns.

This post walks through a CMS 12 compatible version and calls out exactly what differs from the original approach.

Goal



The underlying editor (CheckBoxListEditor) stays the same. We only change metadata configuration and add a small CSS rule to float each checkbox container.

CMS 12 implementation

The solution has four parts:

  • A marker attribute that carries column count and selection factory type
  • An EditorDescriptor that applies editor metadata (replacing IMetadataAware)
  • A [UIHint] on the property to route to the custom descriptor
  • CSS registered in module.config at the application root


SelectManyExtendedAttribute

The attribute holds configuration only. It does not implement IMetadataAware.

[AttributeUsage(AttributeTargets.Property)]
public class SelectManyExtendedAttribute : Attribute
{
    public virtual Type? SelectionFactoryType { get; set; }
    public virtual NumberOfColumns NumberOfColumns { get; set; } = NumberOfColumns.One;
}

public enum NumberOfColumns
{
    One,
    Two,
    Three
}

SelectManyExtendedEditorDescriptor

All metadata mutation happens here, in ModifyMetadata:

[EditorDescriptorRegistration(
    TargetType = typeof(string),
    UIHint = SelectManyExtendedEditorDescriptor.UIHint,
    EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
public class SelectManyExtendedEditorDescriptor : EditorDescriptor
{
    public const string UIHint = "selectmanyextended";

    public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
    {
        var extendedAttribute = attributes.OfType<SelectManyExtendedAttribute>().FirstOrDefault();
        if (extendedAttribute is null)
        {
            return;
        }

        metadata.ClientEditingClass = "epi-cms/contentediting/editors/CheckBoxListEditor";
        metadata.SelectionFactoryType = extendedAttribute.SelectionFactoryType;

        if (extendedAttribute.NumberOfColumns == NumberOfColumns.One)
        {
            return;
        }

        var width = extendedAttribute.NumberOfColumns == NumberOfColumns.Two ? "600px" : "900px";
        metadata.EditorConfiguration["style"] = $"width: {width}";
        metadata.EditorConfiguration["class"] = "selectmanyextended";
    }
}

Property usage

Both the custom attribute and [UIHint] are required on CMS 12. The UI hint connects the property to the editor descriptor.

[CultureSpecific]
[Display(Order = 10)]
[SelectManyExtended(NumberOfColumns = NumberOfColumns.Three,  SelectionFactoryType = typeof(CountrySelectionFactory))]
[UIHint(SelectManyExtendedEditorDescriptor.UIHint)]
public virtual string? Countries{ get; set; }

CSS 

.selectmanyextended .epi-checkboxContainer {
    float: left;
    width: 290px;
}

The selectmanyextended class scopes the layout so standard single-column SelectMany fields elsewhere in the UI are unaffected.

module.config

Place module.config in the application project root (alongside Program.cs). Optimizely treats this as the application default module. The client resource base path defaults to wwwroot/ClientResources, so paths are relative to that folder.

<?xml version="1.0" encoding="utf-8"?>
<module>
  <clientResources>
    <add name="epi-cms.widgets.base"
         path="styles/country-columns.css"
         resourceType="Style"
         sortIndex="100" />
  </clientResources>
</module>

Registering the stylesheet under the existing epi-cms.widgets.base resource name ensures it loads with the CMS shell UI. Multiple entries can share the same resource name; they are grouped and loaded together.

Restart the application after adding or changing module.config.

~Thank you for reading this post. I hope you found it useful. Have a great day!



Jun 09, 2026

Comments

Manoj Kumawat
Manoj Kumawat Jun 9, 2026 07:46 AM

Very practicle issue Sanjay. Thanks for putting your time in this beast.

error Please login to comment.
Latest blogs
Composition Over Inheritance: Why Your Content Contract Is Not a Base Class

Inside the structural conformance pattern in SaaS CMS — from server-side inheritance to pipeline governance. A contract in Optimizely SaaS CMS is...

Vipin Banka | Sep 19, 2026

Optimizely Opti ID Integration: Why the API Key Was Missing from the DXP Management Portal

While working on an Optimizely CMS 12 Opti ID integration recently, we ran into an issue that initially appeared to be related to SSO authenticatio...

Madhu | Sep 18, 2026 |

Extending a Contract: What You Can Override

Contracts share fields across content types — but not every field setting can be overridden. Push the same contract onto several components, and CM...

Chirag Khanna | Sep 18, 2026 |

Building a Blazor Server UI Inside the Optimizely CMS 13 Admin Shell

What I learned shipping a Blazor Server UI inside Optimizely CMS 13 — Razor components in a NuGet package, the Blazor hub next to MapContent(),...

Stanisław Szołkowski | Sep 17, 2026 |

Giving Optimizely 13 Content Types Meaningful Icons and Thumbnails

When working with an Optimizely CMS solution that has evolved over time, it is common to end up with many content types. As the number grows, the...

Pär Wissmark | Sep 16, 2026 |

force-dynamic vs connection() for Optimizely Graph

If you have built an Optimizely CMS SDK site, you already know getContentByPath on a catch-all. This post is about the part that file never says ou...

Chirag Khanna | Sep 16, 2026 |