Search code examples
c#selectenumsblazordropdown

Blazor custom dropdown with HTML select and sorting by text, not value for enums


I'm trying to build a custom Blazor dropdown component for enums that is able to sort the enum members by their display value / text. I have the following code:

<div>
    <select value="@this.SelectedValue" @onchange="@this.OnSelectedValueChanged" @key="@this.Items">
        @foreach (var value in this.Items)
        {
            // Use enum value as value and display name as text.
            if (value is Enum enumName)
            {
                <option id="@value" value="@value">@enumName.GetDisplayName(CultureInfo.CurrentCulture)</option>
            }
        }
    </select>
</div>

the sort order enum:

public enum SortOrder
{
    None,
    Ascending,
    Descending
}

and in the code behind file:

private List<TItem?> OriginalItems { get; set; } = [];

[Parameter]
[EditorRequired]
public IEnumerable<TItem?> Items { get; set; } = [];

[Parameter]
public TItem? SelectedValue { get; set; }

[Parameter]
public EventCallback<TItem?> SelectedValueChanged { get; set; }

[Parameter]
public EventCallback<TItem?> OnValueChanged { get; set; }

[Parameter]
public SortOrder SortOrder { get; set; }

protected override void OnInitialized()
{
    base.OnInitialized();
    this.OriginalItems = this.Items.ToList();
    this.SortItems();
}

protected async Task OnSelectedValueChanged(ChangeEventArgs args)
{
    if (args.Value is not string value)
    {
        return;
    }

    if (this.SelectedValue is Enum _)
    {
        try
        {
            var result = (TItem?)Enum.Parse(typeof(TItem?), value);
            await this.SelectedValueChanged.InvokeAsync(result);
            await this.OnValueChanged.InvokeAsync(result);
        }
        catch
        {
            // ignored
        }
    }
}

private void SortItems()
{
    if (this.SortOrder == SortOrder.None)
    {
        return;
    }

    if (typeof(TItem).BaseType == typeof(Enum))
    {
        this.Items = this.SortOrder == SortOrder.Ascending ?
            [.. this.Items.OrderBy(f => (f as Enum)?.GetDisplayNameForEnum(CultureInfo.CurrentCulture))] :
            [.. this.Items.OrderByDescending(f => (f as Enum)?.GetDisplayNameForEnum(CultureInfo.CurrentCulture))];
    }
}

The GetDisplayValue function just gets a display value for the enums and is implemented for all enums that are put into the dropdown, e.g.:

private static string GetDisplayName(this SortOrder order, CultureInfo culture)
{
    var de = culture.Name.StartsWith("de");

    return order switch
    {
        SortOrder.None => de ? "Keine" : "None",
        SortOrder.Ascending => de ? "Aufsteigend" : "Ascending",
        SortOrder.Descending => de ? "Absteigend" : "Descending",
        _ => $"[[{type}]]"
    };
}

There is also an input field to filter, a filter method, filter size and several other things like ids that are set. However, I removed these as they're not relevant for the question. TItem is a generic member (And only used for Enums in my case).

When I add the component to a page, initial sorting works. Searching works. However, when I select one of the items, the sorting is ignored and the select seems to fall back / "Sort" by the enum member value (e.g. what is specified in value= of the option). Is there a way to always have the select sort by the display name even after the value selection has changed (After OnSelectedValueChanged() was called)?

Hint: Calling SortItems() at the end of OnSelectedValueChanged() doesn't work. Hint 2: Calling SortItems() before this.OriginalItems = this.Items.ToList(); in OnInitialized() doesn't help either. Hint 3: Using the list index of this.Items as value in the select after sorting the list doesn't work either plus has the downgrade that the selected value is properly set, but not shown in the dropdown anymore.


Solution

  • The solution I came up with now was to rewrite the SortItems function to return a value whether the collection was changed (e.g. resorted) and then use OnAfterRender() to call StateHasChanged().

    Changes:

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender)
        {
            if (this.SortItems())
            {
                this.StateHasChanged();
            }
        }
    
        base.OnAfterRender(firstRender);
    }
    
    private bool SortItems()
    {
        if (this.SortOrder == SortOrder.None)
        {
            return false;
        }
    
        if (typeof(TItem).BaseType == typeof(Enum))
        {
            var initialItems = this.Items.ToList();
            this.Items = this.SortOrder == SortOrder.Ascending ?
                [.. this.Items.OrderBy(f => (f as Enum)?.GetDisplayName(CultureInfo.CurrentCulture))] :
                [.. this.Items.OrderByDescending(f => (f as Enum)?.GetDisplayName(CultureInfo.CurrentCulture))];
            return !Enumerable.SequenceEqual(initialItems, this.Items);
        }
    
        return false;
    }