Reputation: 13233
I want to write a custom drop down list component for Blazor partly due to the fact that the existing InputSelect component does not bind to anything other than string and enum types. This is not good enough for me since my models have int and nullable int type properties that I want bound to the drop down list. So far I have this:
@using System.Globalization
@typeparam TValue
@typeparam TData
@inherits InputBase<TValue>
<select id="@Id" @bind="CurrentValueAsString" class="f-select js-form-field">
@if (!string.IsNullOrWhiteSpace(OptionLabel) || Value == null)
{
<option value="">@(OptionLabel ?? "-- SELECT --")</option>
}
@foreach (var item in Data)
{
<option value="@GetPropertyValue(item, ValueFieldName)">@GetPropertyValue(item, TextFieldName)</option>
}
</select>
<span>Component Value is: @Value</span>
@code {
[Parameter]
public string Id { get; set; }
[Parameter]
public IEnumerable<TData> Data { get; set; } = new List<TData>();
[Parameter]
public string ValueFieldName { get; set; }
[Parameter]
public string TextFieldName { get; set; }
[Parameter]
public string OptionLabel { get; set; }
private Type ValueType => IsValueTypeNullable() ? Nullable.GetUnderlyingType(typeof(TValue)) : typeof(TValue);
protected override void OnInitialized()
{
base.OnInitialized();
ValidateInitialization();
}
private void ValidateInitialization()
{
if (string.IsNullOrWhiteSpace(ValueFieldName))
{
throw new ArgumentNullException(nameof(ValueFieldName), $"Parameter {nameof(ValueFieldName)} is required.");
}
if (string.IsNullOrWhiteSpace(TextFieldName))
{
throw new ArgumentNullException(nameof(TextFieldName), $"Parameter {nameof(TextFieldName)} is required.");
}
if (!HasProperty(ValueFieldName))
{
throw new Exception($"Data type {typeof(TData)} does not have a property called {ValueFieldName}.");
}
if (!HasProperty(TextFieldName))
{
throw new Exception($"Data type {typeof(TData)} does not have a property called {TextFieldName}.");
}
}
protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
{
validationErrorMessage = null;
if (ValueType == typeof(string))
{
result = (TValue)(object)value;
return true;
}
if (ValueType == typeof(int))
{
if (string.IsNullOrWhiteSpace(value))
{
result = default;
}
else
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue))
{
result = (TValue)(object)parsedValue;
}
else
{
result = default;
validationErrorMessage = $"Specified value cannot be converted to type {typeof(TValue)}";
return false;
}
}
return true;
}
if (ValueType == typeof(Guid))
{
validationErrorMessage = null;
if (string.IsNullOrWhiteSpace(value))
{
result = default;
}
else
{
if (Guid.TryParse(value, out var parsedValue))
{
result = (TValue)(object)parsedValue;
}
else
{
result = default;
validationErrorMessage = $"Specified value cannot be converted to type {typeof(TValue)}";
return false;
}
}
return true;
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TValue)}'. Supported types are string, int and Guid.");
}
private string GetPropertyValue(TData source, string propertyName)
{
return source.GetType().GetProperty(propertyName)?.GetValue(source, null).ToString();
}
private bool HasProperty(string propertyName)
{
return typeof(TData).GetProperty(propertyName) != null;
}
private bool IsValueTypeNullable()
{
return Nullable.GetUnderlyingType(typeof(TValue)) != null;
}
}
And in the parent component I can use it like this:
<DropDownList Id="@nameof(Model.SelectedYear)"
@bind-Value="Model.SelectedYear"
Data="Model.Years"
ValueFieldName="@nameof(Year.Id)"
TextFieldName="@nameof(Year.YearName)">
</DropDownList>
This works very well, the model binds to the drop down list and the value is changed on the parent model when the drop down list value is changed. However I now want to capture this value change event on my parent and do some custom logic, mainly load some additional data based on the selected year. My guess is that I need a custom EventCallback but everything I tried causes some sort of build or runtime error. It seems that if my component inherits from InputBase then I am very much limited in what I can do.
Can anyone tell me how can I capture the value change from a child component in a parent component?
Upvotes: 7
Views: 3129
Reputation: 31625
My guess is that I need a custom EventCallback
You sure need a EventCallback
, but the thing is, you already have one, just don't see it.
To be able to use @bind-Value
you need two parameters, T Value
and EventCallback<T> ValueChanged
.
When you pass @bind-Foo
, blazor sets these two parameters, Foo
and FooChanged
and in the FooChanged
it will simply set the new value to Foo
.
So when you do @bind-Foo="Bar"
what blazor does under the hood is pass these two parameters
Foo="@Bar"
FooChanged="@(newValue => Bar = newValue)"
So in your case, what you need to do is pass your own ValueChanged
function, that sets the new value in Value
but also do some extra things you want.
<DropDownList Id="@nameof(Model.SelectedYear)"
Value="Model.SelectedYear"
ValueChanged="@((TYPE_OF_VALUE newValue) => HandleValueChanged(newValue))"
Data="Model.Years"
ValueFieldName="@nameof(Year.Id)"
TextFieldName="@nameof(Year.YearName)">
</DropDownList>
@code
{
void HandleValueChanged(TYPE_OF_VALUE newValue)
{
// do what you want to do
// set the newValue if you want
Model.SelectedYear = newValue;
}
}
In TYPE_OF_VALUE
, you just replace it with the type of Model.SelectedYear
.
You can take a look at this explanation in the docs.
Because you want to use nullable types, you also need to pass FooExpression
which in your case will be Expression<Func<T>> ValueExpression
.
<DropDownList Id="@nameof(Model.SelectedYear)"
Value="Model.SelectedYear"
ValueChanged="@((TYPE_OF_VALUE newValue) => HandleValueChanged(newValue))"
ValueExpression="@(() => Model.SelectedYear)"
Data="Model.Years"
ValueFieldName="@nameof(Year.Id)"
TextFieldName="@nameof(Year.YearName)">
</DropDownList>
Upvotes: 6