<< 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,
    [Category("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 text (e.g. 'AudiGroup') in your data store or the integer number (e.g. 0) in the database. For example, saving the string name text may be useful if you want to use that data at a later point e.g. for data science and data engineering working with that data, the primary reason for this is to support better and easier discoverability, enum = 8 isnt easy to understand when all you see is the data. You can save the string name text by having your page Model class have the enum field as the Enum type (e.g. public Vehicles? Vehicle) but have your domain class have the field as a 'string' and then map using Vehicles.ToString() when you are creating the new 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>