Steven Jon Smith
Steven Jon Smith

Reputation: 43

How to override InputBase<T> Value in Blazor, in order to validate

I am trying to build a new Blazor input component, which inherits from InputBase, so that I can provide a form input field for selecting employees in an organisation. I need to provide the ability to limit the input to a single person or allow multiple selections, dependant on use case.

The input component I am building accepts a List of type "Employee", which is a class built specifically for our employee dataset. I have setup a Boolean value to denote if multi-selection should be allowed. However, I can't intercept the change in the Value to stop additional people being added if it should be limited to a single selection.

I have tried the following override of the InputBase Value property:


    new public List<Employee> Value
    {
        get
        {
            return CurrentValue;
        }
        set
        {
            if (!Multiselect)
            {
                CurrentValue = value.Take(1).ToList();
            }
            else
            {
                CurrentValue = value;
            }
        }
    }

Due to having a List binding I have overridden the FormatValueAsString and TryParseValueFromString so that the email addresses can be seen in an input field, as part of the component. I can add multi-selection logic into these functions, however they still allow the Value to end up with multiple in, though the string shown in the UI will only contain one.

EDIT: Adding more code as requested.

.razor file:

@inherits InputBase<List<Employee>>
@using Project.Models.Employee

<div class="InputEmployee">
    <input class="InputEmployee" @bind="CurrentValueAsString" type="text" />
    <i class="oi oi-person" @onclick="() => openSearch()"></i>
</div>
<div class="InputEmployeeSearch @(displaySearch ? "": " collapse")" @onblur="() => toggleSearch()" id="searchInput">
    <div class="row">
        <div class="searchInput">
            <input @bind-Value="searchString" @bind-Value:event="oninput" placeholder="Search by name, email address, employee number" type="text" />
        </div>
    </div>
    @if (activeSearch)
    {
        <div class="row">
            <div class="searchActive">
                Searching...
            </div>
        </div>
    }
    @if (foundEmployees != null && foundEmployees.Any())
    {
        <div class="row">
            <div class="searchResults">
                @foreach (Employee employee in foundEmployees)
                {
                    <div class="employeeResult" @onclick="() => select(employee)">
                        @employee.DisplayName
                    </div>
                }
            </div>
        </div>
    }
    @if (errorMessage != null)
    {
        <div class="row">
            <div class="searchError">
                @errorMessage
            </div>
        </div>
    }
</div>

.razor.cs file:

using Project.Models.Employee;
using Project.Services.Employee;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;

namespace Project.Components
{
    public partial class InputEmployee : InputBase<List<Employee>>
    {
        [Inject]
        private IEmployeeDataService EmployeeDataService { get; set; }

        [Parameter]
        public int Debounce { get; set; } = 500;
        [Parameter]
        public int MaximumResults { get; set; } = 10;
        [Parameter]
        public int MinimumLength { get; set; } = 4;
        [Parameter]
        public bool Multiselect { get; set; } = false;
        new public List<Employee> Value
        {
            get
            {
                return CurrentValue;
            }
            set
            {
                if (!Multiselect)
                {
                    CurrentValue = value.Take(1).ToList();
                }
                else
                {
                    CurrentValue = value;
                }
            }
        }

        private string currentValueOverlay = "";
        private bool activeSearch = false;
        private bool displaySearch = false;
        private Timer debounceTimer;
        private string searchString
        {
            get
            {
                return searchText;
            }

            set
            {
                searchText = value;

                if (value.Length == 0)
                {
                    debounceTimer.Stop();
                    activeSearch = false;
                }
                else
                {
                    debounceTimer.Stop();
                    debounceTimer.Start();
                    activeSearch = true;
                }
            }
        }
        private string searchText = "";
        private string errorMessage;
        private List<Employee> foundEmployees;

        protected override void OnParametersSet()
        {
            base.OnParametersSet();
            debounceTimer = new Timer();
            debounceTimer.Interval = Debounce;
            debounceTimer.AutoReset = false;
            debounceTimer.Elapsed += search;
        }

        protected override bool TryParseValueFromString(string value, out List<Employee> result, out string validationErrorMessage)
        {
            result = new List<Employee>();
            validationErrorMessage = null;

            string[] valueArray = value.Split(";");
            List<Employee> output = new List<Employee>();

            foreach (string employeeEmail in valueArray)
            {
                try
                {
                    Employee employee = Task.Run(async () => await EmployeeDataService.FindEmployeesAsync(employeeEmail.Trim())).Result.FirstOrDefault();

                    if (employee != null)
                    {
                        output.Add(employee);
                    }
                }
                catch
                {
                    validationErrorMessage = $"User \"{employeeEmail}\" was not found.";
                }
            }

            result = output;
            return true;
        }

        protected override string FormatValueAsString(List<Employee> employees)
        {
            string employeeString = "";

            if (employees.Any() && employees.FirstOrDefault() != null)
            {
                employeeString = String.Join("; ", employees.Select(x => x.Email).ToArray());
            }

            return employeeString;
        }

        private async void toggleSearch()
        {
            displaySearch = !displaySearch;

            await InvokeAsync(StateHasChanged);
        }

        private void openSearch()
        {
            foundEmployees = new List<Employee>();

            if (searchString.Length >= MinimumLength)
            {
                search(null, null);
            }

            toggleSearch();
        }

#nullable enable
        private async void search(Object? source, ElapsedEventArgs? e)
        {
            errorMessage = null;
            foundEmployees = null;
            await InvokeAsync(StateHasChanged);

            if (int.TryParse(searchString, out int i))
            {
                if (searchString.Length == 9)
                {
                    List<string> searchList = new List<string>();
                    searchList.Add(searchString);
                    foundEmployees = (await EmployeeDataService.GetEmployeesAsync(searchList)).ToList();
                }
                else
                {
                    updateError($"Searching by employee number requires the full number.");
                }
            }
            else
            {
                if (searchString.Length < MinimumLength)
                {
                    updateError($"You must enter at least {MinimumLength} characters of their name or email address.");
                }
                else
                {
                    foundEmployees = (await EmployeeDataService.FindEmployeesAsync(searchString)).Take(MaximumResults).ToList();
                }
            }

            if (foundEmployees != null && !foundEmployees.Any())
            {
                updateError($"No employees found matching \"{searchString}\".");
            }

            activeSearch = false;
            await InvokeAsync(StateHasChanged);
        }

        private async void updateError(string message)
        {
            activeSearch = false;
            errorMessage = message;
            await InvokeAsync(StateHasChanged);
        }

        private void select(Employee employee)
        {
            Value.Add(employee);
        }
    }
}

Upvotes: 4

Views: 3747

Answers (2)

Mister Magoo
Mister Magoo

Reputation: 9029

It looks to me like you want to limit this method

private void select(Employee employee)
{
  if (MultiSelect)
  {
    Value.Add(employee);
  }
  else
  { 
    Value = new List<Employee> { employee };
  }
}

Upvotes: 0

Umair
Umair

Reputation: 5501

To add validation messages in your custom control you can override TryParseValueFromString method on the InputBase and write as following:

protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
{
    if (typeof(TValue) == typeof(string))
    {
        result = (TValue)(object)value;
        validationErrorMessage = null;
        return true;
    }
    else if (typeof(TValue).IsEnum)
    {
        var success = BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue);
        if (success)
        {
            result = parsedValue;
            validationErrorMessage = null;
            return true;
        }
        else
        {
            result = default;
            validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
            return false;
        }
    }

    throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TValue)}'.");
}

This is taken from and has more details on this blog from Chris Sainty.

Upvotes: 2

Related Questions