如何在 Blazor 中编写自定义值更改事件处理程序?

Posted

技术标签:

【中文标题】如何在 Blazor 中编写自定义值更改事件处理程序?【英文标题】:How to write a custom value change event handler in Blazor? 【发布时间】:2020-09-30 23:56:38 【问题描述】:

我想为 Blazor 编写一个自定义下拉列表组件,部分原因是现有的 InputSelect 组件不绑定到字符串和枚举类型以外的任何东西。这对我来说还不够好,因为我的模型具有我希望绑定到下拉列表的 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,那么我能做的事情就非常有限。

谁能告诉我如何从父组件中捕获子组件的值变化?

【问题讨论】:

为什么不从 InputText 派生? @enet 我需要呈现 html 选择标签而不是输入类型文本。我实际上尝试从 InputSelect 继承失败,因为它只处理字符串和枚举绑定 - 当您尝试绑定到任何其他类型的属性时失败。 您的代码有效吗?请参阅我的答案示例...如果需要,这就是您应该使用其他类型的方法。 @enet 是的,我的代码正在运行。我没有尝试过你的,但它似乎很合理。我想我可以这样做,但我不喜欢必须通过某种循环自己生成选项标签的想法。我也希望该组件为我做到这一点。 【参考方案1】:

我的猜测是我需要一个自定义 EventCallback

你确实需要EventCallback,但问题是,你已经有了,只是没看到。

为了能够使用@bind-Value,您需要两个参数,T ValueEventCallback&lt;T&gt; ValueChanged

当您传递 @bind-Foo 时,blazor 会设置这两个参数,FooFooChanged,而在 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这个解释。

编辑

因为您想使用可空类型,您还需要传递FooExpression,在您的情况下为Expression&lt;Func&lt;T&gt;&gt; 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>

【讨论】:

是的,我也这么认为,但是当我添加 ValueChanged="MyChangeHandler" 然后使用可为空 int 类型的单个参数创建该处理程序时,我收到以下编译错误:“参数 2:无法从方法组到 EventCallback”。经过更多研究发现我需要这样做: ValueChanged="@((int?param) => HandleValueChanged(param))."你能更新你的答案,我会接受它是正确的吗? 您得到“参数 2:无法从方法组转换为 EventCallback”,但是您如何定义 MyChangeHandlerValueChanged="MyChangeHandler" 没有@ValueChanged="@MyChangeHandler"@ 是有区别的。请告诉我你是如何定义MyChangeHandler 我是否包含 @ 没有区别,错误是相同的。处理程序定义为: private void HandleValueChanged(int? newValue) Model.SelectedClassYear = newValue; 基于github.com/dotnet/aspnetcore/issues/12226 我猜这是一个已知问题。 是的,你是对的,我确实需要 ValueExpression 指令。它现在有效,我可以确认它。

以上是关于如何在 Blazor 中编写自定义值更改事件处理程序?的主要内容,如果未能解决你的问题,请参考以下文章

Blazor University (35)表单 —— 编写自定义验证

如何从视图中触发事件(自定义事件)并在 Ext JS 的控制器中处理它们?

如何在Extjs 4中使用自定义检查或选中的radiogroup事件

Blazor 事件处理开发指南

Hello Blazor:(15)使用bUnit进行单元测试

如何在Blazor服务器端处理窗口或主体滚动?