0

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.

2
  • I'm confused and probably totally missing the point. If you want to create a select for an enum, then why are you passing in public IEnumerable<TItem?> Items { get; set; } = []? Surely you just pass in the enum type and iterate Enum.GetValues in the control to get the possible values? Commented Mar 21, 2024 at 16:30
  • @MrCakaShaunCurtis Theoretically yes, but I wanted to exclude some enum members in some cases and in some cases not. Therefore I can decide in the parent page whether all members should be used or not. A restriction like TItem: Enum would be useful, but doesn't work somehow with Blazor and nullables. Commented Mar 21, 2024 at 16:33

1 Answer 1

0

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;
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.