In this new post about Blazor, I’m going to give you the code for InputSelect component for enumerations in Blazor. In the last few weeks I’m starting to talk about Blazor and here you have the other posts:
- Getting Started With C# And Blazor
- Setting Up A Blazor WebAssembly Application
- Working With Blazor’s Component Model
- Secure Blazor WebAssembly With IdentityServer4
- Blazor Using HttpClient With Authentication
It’s common to use a dropdown list to select a value from an enumeration. In ASP.NET Core MVC you can use Html.GetEnumSelectList
to create the items for an enumeration. This is very convenient and very productive. However, this helper doesn’t exist in Blazor. In this post, we’ll create something even easier to use.
The component InputSelect
allows binding a property of type Enum. However, you need to provide all options manually which is error-prone and not very productive:
<EditForm Model="model">
<InputSelect @bind-Value="model.Season">
<option>Spring</option>
<option>Summer</option>
<option>Autumn</option>
<option>Winter</option>
</InputSelect>
</EditForm>
@code {
Model model = new Model();
class Model
{
public Season Season { get; set; }
}
enum Season
{
Spring,
Summer,
Autumn,
Winter
}
}
You can make this code more generic by iterating on Enum.GetValues
:
<EditForm Model="model">
<InputSelect @bind-Value="model.Season">
@foreach (var value in Enum.GetValues(typeof(Season)))
{
<option>@value</option>
}
</InputSelect>
</EditForm>
This way you can copy/paste the code for any enumeration you want to bind to a select. However, the display text is not customizable, so not very use friendly. It is also not localized. As always in Blazor, the solution is to create a component! Components allow encapsulating reusable behaviors. Then, you can use them in your other components and avoid duplicating code.
InputSelectEnum Blazor component
To create this component, I checked how the InputSelect
component is made on GitHub. The code is very simple. It contains 2 methods: BuildRenderTree
and TryParseValueFromString
. We’ll change the first one to populate the option
elements when creating the tree instead of using the template ChildContent
. The TryParseValueFromString
method converts the string value from the select
element to a valid enumeration value. We’ll adapt this method to support nullable types.
A few points to note in the implementation:
- This component supports nullable types which is not the case of the
InputSelect
component. - This component read the
[Display]
attribute to create the option display names. If no attribute is defined, it decamelizes the enumeration member name. This attribute allows localizing the application.
In previous posts, we have created components using the razor syntax. In this case, it is easier to create the component in C# code only. You can add the code in the “Shared” folder, so the component is accessible in all the views. I’ve added a few comments to explain the code. But don’t worry, there’s nothing complicated in this component.
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using Humanizer;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
// Inherit from InputBase so the hard work is already implemented 😊
// Note that adding a constraint on TEnum (where T : Enum) doesn't work when used in the view, Razor raises an error at build time. Also, this would prevent using nullable types...
public sealed class InputSelectEnum<TEnum> : InputBase<TEnum>
{
// Generate html when the component is rendered.
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "select");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string>(this, value => CurrentValueAsString = value, CurrentValueAsString, null));
// Add an option element per enum value
var enumType = GetEnumType();
foreach (TEnum value in Enum.GetValues(enumType))
{
builder.OpenElement(5, "option");
builder.AddAttribute(6, "value", value.ToString());
builder.AddContent(7, GetDisplayName(value));
builder.CloseElement();
}
builder.CloseElement(); // close the select element
}
protected override bool TryParseValueFromString(string value, out TEnum result, out string validationErrorMessage)
{
// Let's Blazor convert the value for us 😊
if (BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out TEnum parsedValue))
{
result = parsedValue;
validationErrorMessage = null;
return true;
}
// Map null/empty value to null if the bound object is nullable
if (string.IsNullOrEmpty(value))
{
var nullableType = Nullable.GetUnderlyingType(typeof(TEnum));
if (nullableType != null)
{
result = default;
validationErrorMessage = null;
return true;
}
}
// The value is invalid => set the error message
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
// Get the display text for an enum value:
// - Use the DisplayAttribute if set on the enum member, so this support localization
// - Fallback on Humanizer to decamelize the enum member name
private string GetDisplayName(TEnum value)
{
// Read the Display attribute name
var member = value.GetType().GetMember(value.ToString())[0];
var displayAttribute = member.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
return displayAttribute.GetName();
// Require the NuGet package Humanizer.Core
// <PackageReference Include = "Humanizer.Core" Version = "2.8.26" />
return value.ToString().Humanize();
}
// Get the actual enum type. It unwrap Nullable<T> if needed
// MyEnum => MyEnum
// MyEnum? => MyEnum
private Type GetEnumType()
{
var nullableType = Nullable.GetUnderlyingType(typeof(TEnum));
if (nullableType != null)
return nullableType;
return typeof(TEnum);
}
}
You can now use this component in another Blazor component:
<EditForm Model="model">
<div>
@* The type of the enum (TEnum) is detected by the type of the bound property which is just awesome! *@
<InputSelectEnum @bind-Value="model.Season" />
<span>Selected value: @model.Season</span>
</div>
</EditForm>
@code {
Model model = new Model();
class Model
{
public Season Season { get; set; }
}
enum Season
{
[Display(Name = "Spring", ResourceType = typeof(Resources))]
Spring,
[Display(Name = "Summer", ResourceType = typeof(Resources))]
Summer,
[Display(Name = "Autumn", ResourceType = typeof(Resources))]
Autumn,
[Display(Name = "Winter", ResourceType = typeof(Resources))]
Winter,
}
}
4 thoughts on “InputSelect component for enumerations in Blazor”