Как написать обработчик событий изменения пользовательского значения в 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, то я очень ограничен в том, что могу делать.

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


person Marko    schedule 11.06.2020    source источник
comment
Почему бы не унаследовать от InputText?   -  person enet    schedule 11.06.2020
comment
@enet Мне нужно отобразить тег выбора HTML, а не текст типа ввода. На самом деле я безуспешно пытался наследовать от InputSelect, потому что он обрабатывает только привязку строк и перечислений - не работает, когда вы пытаетесь привязать к свойству любого другого типа.   -  person Marko    schedule 12.06.2020
comment
Ваш код работает? См. Мой образец ответа ... Вот как вы должны делать это с другими типами, если это необходимо.   -  person enet    schedule 12.06.2020
comment
@enet Да, мой код работает. Я не пробовал ваш, но он кажется разумным. Я полагаю, что мог бы сделать это так, но мне не нравится идея генерировать теги опций самостоятельно через какой-то цикл. Я хотел, чтобы компонент делал это и за меня.   -  person Marko    schedule 12.06.2020


Ответы (1)


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

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

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

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

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

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.

Вы можете взглянуть на это объяснение в docs.

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

Поскольку вы хотите использовать типы, допускающие значение 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>
person Vencovsky    schedule 11.06.2020
comment
Да, я тоже так думал, но когда я добавляю ValueChanged = MyChangeHandler, а затем создаю этот обработчик с одним параметром типа nullable int, я получаю следующую ошибку компиляции: Аргумент 2: Невозможно преобразовать из группы методов в EventCallback. После дополнительных исследований выяснилось, что мне нужно сделать это: ValueChanged = @ ((int? Param) = ›HandleValueChanged (param)). Можете ли вы обновить свой ответ, и я приму его как правильный? - person Marko; 11.06.2020
comment
Вы получаете аргумент 2: невозможно преобразовать из группы методов в EventCallback, но как вы определили MyChangeHandler? И есть разница между ValueChanged="MyChangeHandler" без @ и ValueChanged="@MyChangeHandler" с @. Пожалуйста, покажите мне, как вы определили MyChangeHandler - person Vencovsky; 11.06.2020
comment
Независимо от того, включаю ли я @ или нет, ошибка одна и та же. Обработчик определяется как: private void HandleValueChanged (int? NewValue) {Model.SelectedClassYear = newValue; } - person Marko; 11.06.2020
comment
На основании github.com/dotnet/aspnetcore/issues/12226 это известная проблема, которую я Угадай. - person Marko; 11.06.2020
comment
@Marko На основании этого ответа на проблему необходимо передать ValueExpression в компонент, что также является другой вещью, которую Blazor создает при использовании @bind-, и это необходимо, потому что вы передаете свойство, допускающее значение NULL. Я дополню свой ответ этой информацией - person Vencovsky; 11.06.2020
comment
Да, вы правы, мне нужна директива ValueExpression. Теперь это работает, я могу это подтвердить. - person Marko; 11.06.2020