skip to Main Content

Need a little help creating custom select component

I am attempting to create a custom form select component. The component will contain my own custom markup rather than using the tag as it needs a completely different UI beyond which I can style with css.

The component should be able to bind it’s value to a string / int / decimal model property which is where I am having trouble.

This is what I have so far:

MySelect.razor

@typeparam TValue
@inherits InputBase<TValue>
@namespace Accounting.Web.Components

@foreach (var option in Options)
{
    <button @onclick="OnClick(option.Value)">@option.Value</button>
}

MySelect.razor.cs

namespace Accounting.Web.Components
{
    public partial class MySelectOption<TValue>
    {
        public int Id { get; set; }
        public TValue Value { get; set; }
    }

    public partial class MySelect<TValue> : InputBase<TValue>
    {
        [Parameter]
        public string Id { get; set; } = "ESelect";

        [Parameter]
        public List<MySelectOption<TValue>> Options { get; set; }
        protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
        {
            throw new NotImplementedException();
        }

        public void OnClick(TValue value)
        {
            Value = value;
        }
    }
}

And then in Index.razor:

<MySelect Options="@options" @bind-Value="AddDto.Description" TValue="string">

So when using the component I should be able to bind it to a property of any type (but usually int or string) which I pass as the type param TValue.

However, the line below is causing an issue:

<button @onclick="OnClick(option.Value)">@option.Value</button>

Argument 2: cannot convert from ‘void’ to ‘Microsoft.AspNetCore.Components.EventCallback’

How can I pass the option.Value (which is always a string) to the onCLick event? Or alternatively modify the code above so that I can accomplish my initially stated goal?

2

Answers


  1. Chosen as BEST ANSWER

    With help and suggestions from previous answers, below is the solution I arrived at:

    Index.razor

    <MySelect Options="@options" @bind-Value="AddDto.InvestmentEntityId">
    </MySelect>
    
    @AddDto.InvestmentEntityId    // integer property
    
    <MySelect Options="@options" @bind-Value="AddDto.Description">
    </MySelect>
    
    @AddDto.Description    // string property
    

    MySelect.razor

    @typeparam TValue
    @inherits InputBase<TValue>
    @namespace Accounting.Web.Components
    
    @foreach (var option in Options)
    {
        <button @onclick="() => OnClick(option.Value)">@option.Value</button>
    }
    

    MySelect.razor.cs

    namespace Accounting.Web.Components
    {
        public partial class MySelectOption
        {
            public int Id { get; set; }
            public string Value { get; set; }
        }
    
        public partial class MySelect<TValue> : InputBase<TValue>
        {
            [Parameter]
            public List<MySelectOption> Options { get; set; }
    
            protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
            {
                if (BindConverter.TryConvertTo<TValue>(value, null, out result))
                {
                    validationErrorMessage = null;
                }
                else
                {
                    validationErrorMessage = "Err : Select value";
                }
            }
    
            public void OnClick(string value)
            {
                TValue tmpValue;
                BindConverter.TryConvertTo<TValue>(value, null, out tmpValue);
                CurrentValue = tmpValue;
            }
        }
    }
    

    It's probably not perfect but I hope it helps anyone looking to do the same.


  2. You have more that one issue, but the important one is trying to update Value. Value is an "input" into the control. The updated value is passed back to parent by calling ValueChanged. However, calling ValueChanged directly bypasses the built in functionality in InputBase and it’s interaction with the EditContext.

    This demonstrates the basics of inheriting from InputBase.

    To leverage the built in functionality, you need to either:

    1. Set the value by setting CurrentValueAsString from the markup and then providing a custom TryParseValueFromString to convert from a string to your type (there’s a BindConverter helper you can use – it’s what InputNumber and other input controls use).
    2. Set the value directly by setting CurrentValue. This bypasses TryParseValueFromString.

    Your MySelect.

    I’ve prettied up your buttons and abstracted your list to an IEnumerable.

    @typeparam TValue
    @inherits InputBase<TValue>
    @using Microsoft.AspNetCore.Components.Forms;
    @using Microsoft.AspNetCore.Components;
    @using System.Diagnostics.CodeAnalysis;
    
    <div class="btn-group" role="group">
    @foreach (var option in Options)
    {
        <button class="@btnColour(option.Value)" @onclick="() => OnClick(option.Value)">@option.Value</button>
    }
    </div>
    
    @code {
        [Parameter] public IEnumerable<MySelectOption<TValue>> Options { get; set; } = new List<MySelectOption<TValue>>();
    
    
        private string btnColour(TValue? value)
        {
            if (this.Value is null) 
             return "btn btn-outline-primary";
    
            return this.Value.Equals(value) 
            ? "btn btn-primary" 
            : "btn btn-outline-primary";
    
        }
    
        protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
        {
            throw new NotImplementedException();
        }
    
        public void OnClick(TValue? value)
        {
            CurrentValue = value;
        }
    }
    

    And then here’s a demo page to show it in use.

    @page "/"
    
    <PageTitle>Index</PageTitle>
    
    <h1>Hello, world!</h1>
    
    <EditForm Model=@model>
        <MySelect Options="@options" @bind-Value=model.Description TValue="string" />
    </EditForm>
    
    <div class="alert alert-info m-3">
        Description: @model.Description 
    </div>
    
    @code {
        private Model model = new();
    
        IEnumerable<MySelectOption<string>> options =
            new List<MySelectOption<string>>() {
            new MySelectOption<string> { Id = 1, Value = "France" },
            new MySelectOption<string> { Id = 1, Value = "Spain" },
            new MySelectOption<string> { Id = 1, Value = "Portugal" },
        };
    
        public class Model
        {
            public string? Description { get; set; }
        } 
    }
    

    For reference you can find the source code for all the standard InputBase controls here: https://github.com/dotnet/aspnetcore/tree/main/src/Components/Web/src/Forms

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search