Reputation: 9332
I'm working on a Blazor Wasm. I would like to create my own custom select
component. The implementation below works but something is missing: there is no callback when something is selected inside my custom select component. I Would like to be able to catch the event from my TestView.razor component.
My custom implementation InputSelectCustom.cs
public class InputSelectCustom<T> : InputSelect<T>
{
protected override string FormatValueAsString(T value)
{
// Custom code ommitted for clarity
return base.FormatValueAsString(value);
}
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
{
if (typeof(T) == typeof(int) ||
typeof(T) == typeof(int?))
{
if (int.TryParse(value, out var resultInt))
{
result = (T)(object)resultInt;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage = "The chosen value is not valid.";
return false;
}
}
else
if (typeof(T).IsEnum)
{
if (CustomFunctions.EnumTryParse<T>(value, out var resultEnum))
{
result = (T)(object)resultEnum;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage = "The chosen value is not valid.";
return false;
}
}
else
if (typeof(T).IsNullableEnum())
{
if (CustomFunctions.NullableEnumTryParse<T>(value, out var resultEnum))
{
result = (T)(object)resultEnum;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage = "The chosen value is not valid.";
return false;
}
}
else
{
return base.TryParseValueFromString(value, out result, out validationErrorMessage);
}
}
protected override Task OnParametersSetAsync()
{
return base.OnParametersSetAsync();
}
}
My TestView.razor
@page "/test-view"
<EditForm Model="CurrentFilterModel">
<InputSelectCustom @bind-Value="@CurrentFilterModel.ClassmentFilter">
<option value="null"> Show all content </option>
<option value="@EnumClassment.Popular"> Popular </option>
<option value="@EnumClassment.Featured"> Featured </option>
<option value="@EnumClassment.Novelty"> Novelty </option>
</InputSelectCustom>
</EditForm>
@code
{
public FilterModel CurrentFilterModel = new FilterModel();
public class FilterModel
{
public EnumClassment? ClassmentFilter { get; set; }
}
}
My enum :
public enum EnumClassment
{
Featured,
Popular,
Novelty
}
UPDATE
So what I needed was a way to be notified whenever my selected value changed.
At first, I tried with this:
<InputSelectCustom @bind-Value="CurrentFilterModel.ClassmentFilter">
<option value=""> Show all content </option>
<option value="@EnumClassment.Popular"> Popular </option>
<option value="@EnumClassment.Featured"> Featured </option>
<option value="@EnumClassment.Novelty"> Novelty </option>
</InputSelectCustom>
Selected value changed correctly but with the code above I have no way to be notified of value change. Per info, I need to be notified because whenever user change selection, I would like to immediately perform server side calls for filtering data.
So below is my second attempt:
<InputSelectCustom @bind-Value="CurrentFilterModel.ClassmentFilter" ValueChanged="ValueChangedForClassmentFilter">
<option value=""> Show all content </option>
<option value="@EnumClassment.Popular"> Popular </option>
<option value="@EnumClassment.Featured"> Featured </option>
<option value="@EnumClassment.Novelty"> Novelty </option>
</InputSelectCustom>
As you can see I cannot use @bind-Value
and ValueChanged
at the same time.
So here comes my final solution:
<InputSelectCustom Value="@CurrentFilterModel.ClassmentFilter" ValueExpression="@( () => CurrentFilterModel.ClassmentFilter )" ValueChanged="@( (EnumClassment? s) => ValueChangedForClassmentFilter(s) )">
<option value=""> Show all content </option>
<option value="@EnumClassment.Popular"> Popular </option>
<option value="@EnumClassment.Featured"> Featured </option>
<option value="@EnumClassment.Novelty"> Novelty </option>
</InputSelectCustom>
protected async Task ValueChangedForClassmentFilter(EnumClassment? theUserInput)
{
// You have to update the model manually because handling the ValueChanged event does not let you use @bind-Value
// For the validation to work you must now also define the ValueExpression because @bind-Value did it for you
CurrentFilterModel.ClassmentFilter = theUserInput;
// Refresh data based on filters
await FilterCoursesServerSide();
}
This time, everything is working as expected: whenever user select another value, I am notified of the new value and I can immediately execute custom code.
Please correct me if I'm wrong or if this can be done a more elegant way because I am new to Blazor and maybe I missed something.
PS: Tested with strings, integers and enumerations.
Upvotes: 1
Views: 901
Reputation: 45596
You can't do that, and you shouldn't do that. What for ? You can, however, do that like you did with the radio button, but then your custom component should derive from the InputBase, and provides the Razor markups and logic.
there is no callback when something is selected inside my custom select component.
Of course there is a call back... otherwise how can your model be updated with the new values. It is triggered from the CurrentValue property of the InputBase component:
protected TValue CurrentValue
{
get => Value;
set
{
var hasChanged = !EqualityComparer<TValue>.Default.Equals(value,
Value);
if (hasChanged)
{
Value = value;
_ = ValueChanged.InvokeAsync(value);
EditContext.NotifyFieldChanged(FieldIdentifier);
}
}
}
This is the code that update your model:
_ = ValueChanged.InvokeAsync(value);
Here's the code for your custom select component. Note that InputSelect does not support numeric types such as int ( you can't apply it to an int property such as public int Country { get; set; }
. And perhaps (not sure, need to check...). enum type as well. My component supports such value:
InputSelectNumber.cs
public class InputSelectNumber<T> : InputSelect<T>
{
protected override bool TryParseValueFromString(string value, out T
result, out string validationErrorMessage)
{
if (typeof(T) == typeof(int))
{
if (int.TryParse(value, out var resultInt))
{
result = (T)(object)resultInt;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage = "The chosen value is not a valid
number.";
return false;
}
}
else
{
return base.TryParseValueFromString(value, out result, out
validationErrorMessage);
}
}
}
<EditForm EditContext="@EditContext">
<DataAnnotationsValidator />
<div class="form-group">
<label for="name">Enter your Name: </label>
<InputText Id="name" Class="form-control" @bind-Value="@comment.Name">
</InputText>
<ValidationMessage For="@(() => comment.Name)" />
</div>
<div class="form-group">
<label for="body">Select your country: </label>
<InputSelectNumber @bind-Value="@comment.Country">
<option value="">Select country...</option>
<option value="1">USA</option>
<option value="2">Britain</option>
<option value="3">Germany</option>
<option value="4">Israel</option>
</InputSelectNumber>
<label for="body">Select your country: </label>
<ValidationMessage For="@(() => comment.Country)" />
</div>
<p>
<button type="submit">Submit</button>
</p>
</EditForm>
<p>Name: @comment.Name</p>
<p>Country: @comment.Country</p>
@code
{
private EditContext EditContext;
private Comment comment = new Comment();
protected override void OnInitialized()
{
EditContext = new EditContext(comment);
}
public class Comment
{
public string Name { get; set; }
// Note that with the subclassed InputSelectNumber I can use either string
// or int. In both cases no error occurs.
public string Country { get; set; }
//public int Country { get; set; }
}
}
Update per request:
Please correct me if I'm wrong or if this can be done a more elegant way because I am new to Blazor and maybe I missed something.
Your code is fine, and this is how you should do it. See my answer here where I do exactly the same thing with the InputSelect component. As your custom component derives from the InputSelect component this is the best, most elegant way to do it. Perhaps the only way.
How new are you to Blazor ?;) Your knowledge surpasses that of most of the users here (I mean those who permanently answer questions here).
However, reading what you propose to achieve, on the surface of it, I believe your direction is mistaken. You don't need to know when the value of the select component has changed the way you do it...
Instead you should implement an event handler to the OnFieldChanged event exposed by the EditContext object, in which you can access the new value passed via event args, manipulate data, etc., even validating the EditContext object...
Some code to direct you:
private EditContext EditContext;
private Comment Model = new Comment();
protected override void OnInitialized()
{
EditContext = new EditContext(Model);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
base.OnInitialized();
}
// Note: The OnFieldChanged event is raised for each field in the
model
private void EditContext_OnFieldChanged(object sender,
FieldChangedEventArgs e)
{
Console.WriteLine(e.FieldIdentifier.FieldName);
// more code...
}
Hope this helps...
Upvotes: 2