Как написать обработчик событий изменения пользовательского значения в Blazor?

Я хочу написать настраиваемый компонент раскрывающегося списка для Blazor отчасти из-за того, что существующий компонент InputSelect не привязывается ни к чему, кроме типов string и enum. Для меня этого недостаточно, поскольку мои модели имеют свойства типа int и обнуляемого типа int, которые я хочу привязать к раскрывающемуся списку. Пока у меня есть это:

@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;
    }

}

А в родительском компоненте я могу использовать это так:

<DropDownList Id="@nameof(Model.SelectedYear)"
    @bind-Value="Model.SelectedYear"
    Data="Model.Years"
    ValueFieldName="@nameof(Year.Id)"
    TextFieldName="@nameof(Year.YearName)">
</DropDownList>

Это работает очень хорошо, модель привязывается к раскрывающемуся списку, и значение в родительской модели изменяется при изменении значения раскрывающегося списка. Однако теперь я хочу зафиксировать это событие изменения значения для своего родителя и выполнить некоторую настраиваемую логику, в основном загрузить некоторые дополнительные данные на основе выбранного года. Я предполагаю, что мне нужен собственный EventCallback, но все, что я пробовал, вызывает какую-то ошибку сборки или выполнения. Кажется, что если мой компонент наследуется от InputBase, то я очень ограничен в том, что могу делать.

Может ли кто-нибудь сказать мне, как я могу зафиксировать изменение значения дочернего компонента в родительском компоненте?

1 ответ

Решение

Я предполагаю, что мне нужен собственный EventCallback

Вам обязательно нужен EventCallback, но дело в том, что он у вас уже есть, просто не смотрите.

Чтобы иметь возможность использовать @bind-Value вам нужно два параметра, T Value а также EventCallback<T> ValueChanged.

Когда вы пройдете @bind-Foo, blazor устанавливает эти два параметра, Foo а также FooChanged и в FooChanged он просто установит новое значение на Foo.

Итак, когда вы это сделаете @bind-Foo="Bar" то, что делает блейзер под капотом, передает эти два параметра

Foo="@Bar"
FooChanged="@(newValue => Bar = newValue)"

Итак, в вашем случае вам нужно передать свой собственный ValueChanged функция, которая устанавливает новое значение в Value но также делайте некоторые дополнительные вещи, которые хотите.

<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;
    }
}

В TYPE_OF_VALUE, вы просто замените его типом Model.SelectedYear.

Вы можете посмотреть это объяснение в документации.

редактировать

Поскольку вы хотите использовать типы, допускающие значение NULL, вам также необходимо передать FooExpression который в вашем случае будет 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>
Другие вопросы по тегам