Search code examples
c#html.net-corecustom-componentblazor-component

How to create a custom Blazor multiple select HTML component? Error: MultipleSelect requires a value for the 'ValueExpression' parameter


I am working to create a custom Blazor multiple select HTML component. It works until I add the validation. Also if I disable multiple select and leave the validation on, it works.

When multiple select with validation is on I get this error:

InvalidOperationException: MultipleSelect requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.

I haven't been able to use the 'bind-Value' property because I get this error.

The documentation I have been able to find so far only address building a custom component from an HTML <select> element when the multiple select option is not in use.

How do I go about creating a <select> element with multiple select enabled?

Custom multiple select component

MultipleSelect.razor

@using CustomComponents.DataModels
@using System.Linq.Expressions
@using System
@using System.Collections.Generic
@inherits InputBase<string>

<div class="row">
    <div class="col-3">
        <select id="@Id" @bind=@CurrentValue class="form-control @CssClass" multiple="multiple" size="@BoxHieght" style="width:@BoxWidth">
            @foreach (var option in Options)
            {
                <option @onclick="@(() => SelectOption(option))" value="@option.Value">@option.Text</option>
            }
        </select>
    </div>
</div>

@code {
    [Parameter]
    public string Id { get; set; }
    [Parameter]
    public List<Option> Options { get; set; } = new List<Option>();
    [Parameter]
    public Option SelectedOption { get; set; } = new Option { Text = " ", Value = " " };
    [Parameter]
    public int BoxHieght { get; set; } = 5;
    [Parameter]
    public string BoxWidth { get; set; } = "auto";
    [Parameter, EditorRequired]
    public Expression<Func<string>> ValidationFor { get; set; } = default!;

    private void SelectOption(Option option)
    {
        SelectedOption = option;
    }
    protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
    {
        try
        {
            result = value;
            validationErrorMessage = null;
            return true;
        }
        catch (Exception exception)
        {
            result = null;
            validationErrorMessage = exception.Message;
            return false;
        }

    }    
}

Option data model object

Option.cs

namespace CustomComponents.DataModels
{
    public class Option
    {
        public string Text { get; set; }
        public string Value { get; set; }
    }
}

Web Form Model

FormModel.cs

using CustomComponents.Data.DataModels;

namespace BlazorApp2.Pages.PageModels
{
    public class FormModel
    {
        public Option Option { get; set; } = new Option();
    }
}

Data Model

State.cs

using System.ComponentModel.DataAnnotations;

namespace BlazorApp2.Data.DataModels
{
    public class State
    {
        [Required]
        public string Name { get; set; }
        [Required]
        public string Abbreviation { get; set; }
    }
}

Web Form

Index.razor

@page "/"
@using CustomComponents.Components
@using CustomComponents.Data.DataModels
@using CustomComponents.Pages.PageModels
<PageTitle>Mutiple Select Component</PageTitle>

<EditForm Model="@model" OnValidSubmit="ValidSubmit">  
<DataAnnotationsValidator /> 
<ValidationSummary />
<MyComponent Options="@options" @bind-Value="@model.Option" ValidationFor="() => State.Name"></MyComponent>
Selected option:
<div class="row">
    <div class="col">
        @model.Option.Value  @model.Option.Text
    </div>

</div>
<div class="row">
    <div class="col">
        <button class="btn btn-primary" type="submit">Submit</button>
    </div>
</div>
<div class="row">
    <div class="col">
        @if (formSubmitted) @FormSubmitted
    </div>
</div>
<div class="row">
    <div class="col">
        @if (formSubmitted) @StateSubmitted
    </div>
</div>
</EditForm>

    @code {
    private List<Option> options = new List<Option>();
    public FormModel model = new FormModel();
    public State State { get; set; } = new State();
    private List<State> states = new List<State>
    {
        new State { Name = "Utah", Abbreviation = "UT" },
        new State { Name = "Texas", Abbreviation = "TX" },
        new State { Name = "Florida", Abbreviation = "FL" }
    };
    public string FormSubmitted { get; set; } = "Form submitted.";
    public string StateSubmitted { get; set; } = string.Empty;
    private bool formSubmitted = false;

    protected override void OnInitialized()
    {
        foreach(State state in states)
        {
            options.Add(new Option{ Value = state.Abbreviation, Text = state.Name});
        }
        model.Option = options[0];
    }

    public void ValidSubmit()
    {
        State.Abbreviation = model.Option.Value;
        State.Name = model.Option.Text;
        formSubmitted = true;
        StateSubmitted = $"{State.Abbreviation}  {State.Name}";
    }
}

Solution

  • because you inherit the component InputBase, you must use bind-value. I edited a lot of the code to make it work

    @using BlazorApp2.Client.Components
    @using System.Linq.Expressions
    @using System
    @using System.Collections.Generic
    @using System.Diagnostics.CodeAnalysis
    @inherits InputBase<Option>
    
    <div class="row">
        <div class="col-3">
            <select id="@Id" class="form-control" size="@BoxHieght" style="width:@BoxWidth"
            @bind="OptionValueSelected"  @bind:event="oninput">
                @foreach (var option in Options)
                {
                    <option value="@option.Value">@option.Text</option>
                }
            </select>
    
            <p>Selected option:@SelectedOption.Value </p>
        </div>
    </div>
    
    @code {
        [Parameter]
        public string Id { get; set; }
        [Parameter]
        public List<Option> Options { get; set; } = new List<Option>();
        [Parameter]
        public Option SelectedOption { get; set; } = new Option { Text = " ", Value = " " };
        [Parameter]
        public int BoxHieght { get; set; } = 5;
        [Parameter]
        public string BoxWidth { get; set; } = "auto";
        [Parameter, EditorRequired]
        public Expression<Func<string>> ValidationFor { get; set; } = default!;
    
        private string OptionValueSelected
        {
            get => CurrentValue.Value;
            set
            {
                CurrentValue = Options.Find(o => o.Value == value);
            }
        }
    
        protected override bool TryParseValueFromString(string value,
            [MaybeNullWhen(false)] out Option result, [NotNullWhen(false)] out string validationErrorMessage)
        {
            try
            {
                result = Options.First(o => o.Value == value.ToString());
                validationErrorMessage = null;
                return true;
            }
            catch (Exception exception)
            {
                result = null;
                validationErrorMessage = exception.Message;
                return false;
            }
        }
    }
    

    After very long research, Here are some important changes I made:

    • Inherits from InputBase of type Option not string. Reason: so the form context knows the type and binds correctly
    • Bind value with a property with setters. Reason: To convert from string to option
    • Set the value of SelectedOption from the Input base CurrectValue. Reason: To alert the form context about the change so that it updates the main view

    I Tested the component using this page on new project:

    @page "/"
    @using BlazorApp2.Client.Components
    
    <PageTitle>Index</PageTitle>
    
    @code {
        List<Option> options = new List<Option>
        {
            new Option{Text = "Test1", Value = "Test1"},
            new Option{Text = "Test2", Value = "Test2"}
        };
        ExampleModel model;
    
        protected override void OnInitialized()
        {
            model = new ExampleModel();
        }
    }
    
    <h1>Hello, world!</h1>
    
    
    <EditForm Model="@model">   
    <MyComponent Options="@options" @bind-Value="@model.Option"></MyComponent>
    </EditForm>
    <p>@model.Option.Text : @model.Option.Value</p>
    

    Example Model:

    public class ExampleModel
    {
        public Option Option { get; set; } = new Option();
    }
    

    Resources helped me with my research :

    Blazor components

    Blazor form components binding

    Blazor Custom Binding