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?
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.cs
namespace CustomComponents.DataModels
{
public class Option
{
public string Text { get; set; }
public string Value { get; set; }
}
}
FormModel.cs
using CustomComponents.Data.DataModels;
namespace BlazorApp2.Pages.PageModels
{
public class FormModel
{
public Option Option { get; set; } = new Option();
}
}
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; }
}
}
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}";
}
}
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:
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 :