<< Back to Blog

28 AUGUST 2022

.NET 6: How to render a dropdown select list with optgroup sections from an Enum

Example

1. Create an Enum class using attributes to define the groups (e.g. you might create this in a folder called /Enums)

                        
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

public enum Vehicles
{
    // Group: Cars
    [Category("Cars")]
    [Display(Name = "Audi group")]
    AudiGroup = 0,
    [Category("Cars")]
    [Display(Name = "BMW motor group")]
    BMW = 1,
    [Category("Cars")]
    [Display(Name = "Mclaren sports racing")]
    MclarenSports = 2,
    [CategoryCategory("Cars")]
    [Display(Name = "Fiat cars")]
    Fiat = 3,

    // Group: Motorbikes
    [Category("Motorbikes")]
    [Display(Name = "Ducati Ltd")]
    DucatiLtd = 4,
    [Category("Motorbikes")]
    [Display(Name = "BMW Motorrad")]
    BMWMotorrad = 5,

    // Group: Trucks
    [Category("Trucks")]
    [Display(Name = "Mercedes HGV")]
    MercedesHgv = 6
}
    

Developer note:
You may want to consider whether you store the Enum string name value (e.g. 'AudiGroup') or the integer value (e.g. 0) in your datastore. For example, saving the string name value may be useful if you later want to use that data e.g. for data science and data engineering work where the data will interpreted by another system. The primary reason for this is to support better and easier discoverability, enum = 8 isnt easy to understand when all you have access to is the data. To save the string name text, your page Model class can the property as the Enum type (e.g. public Vehicles? Vehicle) but in your domain class give the property a type of 'string' and then map using Vehicles.ToString() when you are creating the domain object.

2. In order to render the select list in a view we will need some extension methods. Add the following extension classes to your Solution (e.g. in a folder called /Extensions)

EnumExtensions.cs class


using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

public static class EnumExtensions
{
    public static string GetDisplayName(this Enum enumValue)
    {
        return enumValue.GetType()
            .GetMember(enumValue.ToString())
            .First()
            .GetCustomAttribute<DisplayAttribute>()
            .GetName();
    }

    public static string GetCategoryName(this Enum enumValue)
    {
        var categoryAttribute = enumValue.GetType()
            .GetMember(enumValue.ToString())
            .First()
            .GetCustomAttribute<CategoryAttribute>();

        return categoryAttribute?.Category;
    }

    public static int GetValue(this Enum enumValue)
    {
        return (int)(object)enumValue;
    }
}

EnumSelectListEnumExtensions.cs class


using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Rendering;

public static class EnumSelectListEnumExtensions
{
    public static IEnumerable<SelectListItem> GetEnumSelectListWithGroups(this IHtmlHelper htmlHelper, Type enumType, Func<SelectListItem, string> valueFunc = null, bool includeEmptyFirstOption = false)
    {
        var selectListGroups = GetSelectListGroups(enumType);

        var options = htmlHelper.GetEnumSelectList(enumType)
            .Select(s =>
            {
                var selectListItem = new SelectListItem(s.Text, valueFunc?.Invoke(s) ?? s.Value, s.Value.Equals(htmlHelper.ViewData.Model)); 
                selectListItem.Group = GetSelectListGroup(enumType, s.Value, selectListGroups);
                return selectListItem;
            })
            .ToList();

        if (includeEmptyFirstOption)
        {
            var listWithEmptyOption = new List<SelectListItem>() { new SelectListItem() };
            listWithEmptyOption.AddRange(options);
            options = listWithEmptyOption;
        }

        return options;
    }

public static IEnumerable<SelectListItem> GetEnumSelectListWithGroups<TEnum>(this IHtmlHelper htmlHelper, bool includeEmptyFirstOption = false) where TEnum : struct
{
    return htmlHelper.GetEnumSelectListWithGroups(typeof(TEnum), null, includeEmptyFirstOption);
}

public static IEnumerable<SelectListItem> GetEnumSelectListWithGroups<TEnum>(this IHtmlHelper htmlHelper, Func<SelectListItem, string> valueFunc = null) where TEnum : struct
{
    return htmlHelper.GetEnumSelectListWithGroups(typeof(TEnum), valueFunc);
}

private static SelectListGroup? GetSelectListGroup(Type enumType, string enumIntVal, List<SelectListGroup> groups)
{
    var enumValues = Enum.GetValues(enumType);

    foreach (var enumValue in enumValues)
    {
        var val = ((Enum)enumValue).GetValue().ToString();

        if (val == enumIntVal)
        {
            var category = ((Enum)enumValue).GetCategoryName();
            var group = groups.FirstOrDefault(x => x.Name == category);
            return group;
        }
    }

    return null;
}

private static List<SelectListGroup> GetSelectListGroups(Type enumType)
{
    var groups = new List<SelectListGroup>();
    var categoryNames = GetCategoryAttributeNames(enumType);

    foreach (var categoryName in categoryNames.Where(x => x != null))
    {
        var group = new SelectListGroup() { Name = categoryName };
        groups.Add(group);
    }

    return groups;
}

private static List<string> GetCategoryAttributeNames(Type enumType)
{
    var categories = new List<string>();
    var values = Enum.GetValues(enumType);

    foreach (var enumValue in values)
    {
        var category = ((Enum)enumValue).GetCategoryName();
        categories.Add(category);
    }

    categories = categories.Distinct().ToList();

    return categories;
    }
}

3. Create an Editor Template partial view which will render the Enum type to a display page (e.g. create a file in /Views/Shared/EditorTemplates called Vehicles.cshtml):


@using {{e.g. Project.Code.Enums}}
@model Vehicles?
@{
    var vehicleOptions = Html.GetEnumSelectListWithGroups(true);
}

<fieldset class="fieldset form-group">
    <legend class="legend radio__legend">
        <span class="legend__text">@Html.DisplayNameForModel()</span>
        <hint-label asp-for="@Model"></hint-label>
        <span asp-validation-for="@Model"></span>
    </legend>
    <select class="select form-group" asp-for="@Model" asp-items="@vehicleOptions">
    </select>
</fieldset>

4. Add the Enum to your form page view model


using {{e.g. Project.Code.Enums}};
using System.ComponentModel;

public class Model
{
    ...
    [DisplayName("Vehicles")]
    public Vehicles? Vehicles { get; set; }
}

5. Finally, add the editor to the View that renders the form


@model {{e.g. Project.MyPage.Model}}

<div> 
    ...
    <div class="form-row">
        @Html.EditorFor(m => m.Vehicle)
    </div>
    ...
</div>