Reputation: 43
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
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
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