如何在Blazor组件上进行双向绑定

63

我希望创建自定义输入,所以我创建了这个组件:

MyInputComponent.razor

<div>
  <input type="text" @bind="BindingValue" />
</div>

@code {
    [Parameter]
    public string BindingValue { get; set; }
}

接下来是使用方法:

<EditForm Model="model" OnValidSubmit="Submit">
    <MyInputComponent BindingValue="model.Name" />
</EditForm>

@code {
    User model = new User() { Name = "My Name" };

    private void Submit()
    {
       // here I found model.Name = null;
   }
}

当我调试 MyInputComponent 时,发现值与我输入的值相同。 但是当我提交表单时,该值为null。缺少什么?
4个回答

105

快速答案

引用Blazor文档

组件参数

绑定可识别组件参数,其中@bind-{property} 可跨组件绑定属性值。

对于您的页面:

<EditForm Model="model" OnValidSubmit="Submit">
    <MyInputComponent @bind-BindingValue="model.Name" />
</EditForm>

子组件MyInputComponent

<div>
  <InputText type="text" @bind-Value="@BindingValue" />
</div>

@code {

    private string _value;

    [Parameter]
    public string BindingValue
    {
        get => _value;
        set
        {
            if (_value == value ) return;
            _value = value;
            BindingValueChanged.InvokeAsync(value);
        }
    }

    [Parameter]
    public EventCallback<string> BindingValueChanged { get; set; }   

}

注意

  • 你应该通过EventCallback<string> BindingValueChanged从子组件中提升绑定更改。
  • 我选择了BindingValueBindingValueChanged作为标识符,但是你也可以只使用ValueValueChanged。然后会变成:<MyInputComponent @bind-Value="model.Name" />

在 BlazorFiddle 上尝试

(2022年编辑) 现在已经完全记录在:使用组件参数进行绑定

编辑:请参见下面的选项2以获得干净的解决方案:


将您的控件放在 EditForm 中

如果您想将您的组件放在 EditForm 内并处理验证或使用 onchange 事件执行其他操作,则应该提升 EditContext.NotifyFieldChanged。你有两种方法来做到这一点。

选项1:从 EditContext 提升

你可以从 CascadeParameter 中获取 EditContext 并手动调用 NotifyFieldChanged

    [CascadingParameter] EditContext EditContext { get; set; } = default!;
    [Parameter] public Expression<Func<string>>? ValueExpression { get; set; }
    #endregion

    #region bindedValue
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    private string _value { set; get; } = "";
    [Parameter]
    public string Value
    {
        get => _value;
        set
        {
            if (_value == value) return;
            _value = value;
            ValueChanged.InvokeAsync(value);
            var fieldIdentifier = FieldIdentifier.Create(ValueExpression);
            EditContext.NotifyFieldChanged(fieldIdentifier);

        }
    }

选项2(推荐):通过从InputBase继承

您可以从InputBase<string>继承,并仅实现TryParseValueFromStringInputBase会为您完成工作,当您从InputBase继承时,您将拥有ValueValueChangedEditContext等。

protected override bool TryParseValueFromString(string? value, out string result, [NotNullWhen(false)] out string? validationErrorMessage)
{
    result = value ?? "";
    validationErrorMessage = null;
    return true;
}

2
dani,如果您选择使用内置的InputText组件提供解决方案,那么为什么不建议继承自InputText组件本身所派生的类呢?这样更简单,需要的代码更少,并且允许您根据需要控制输入HTML标记的呈现方式,例如创建一个用于密码的自定义组件。更好的是,为什么不建议OP直接在EditForm中使用InputText组件...祝您有美好的一天。 - enet
8
@Issac,因为问题是“如何在 Blazor 组件上实现双向绑定”。 - dani herrera
1
这是正确的...但它不是一个没有上下文的普遍问题:你必须考虑用户使用的代码,例如,MyInputComponent组件使用Html输入标记。你的解决方案没有使用它。相反,你使用了内置的TextInput组件,因此我的评论建议如果你选择这种方式,那么直接使用内置组件。 - enet
1
谢谢Dani,这让我解决了绑定自定义组件的问题!@Isaac,如果你有机会发帖,我很想看到你的建议的另一种答案! - Matt Sanders
有没有办法绑定一个对象而不仅仅是一个字符串,或者所有子组件都被强制只能有一个值发送回父组件? - MC9000
@enet 我认为他并不是建议使用内置的 InputText。我认为他是建议继承自 InputBase<string> 来制作自己的 HTML <input>,同时利用所有的绑定、验证和上下文特性。请看这里 https://dev59.com/PFMH5IYBdhLWcg3w2ULi#73734223 - clamchoda

10
总的来说,接受的答案是正确的,而且运行良好。需要补充的只是代码示例使用基于事件的默认命名约定,例如:{PropertyName}Changed
但是您可以覆盖此命名约定@bind-{Property}:event="{EventCallbackName}"
<MyInputComponent @bind-BindingValue="model.Name" @bind-BindingValue:event="OnValueChanged"/>

.....

[Parameter] public EventCallback<string> OnValueChanged { get; set; }  

7

使用.NET 7的Blazor,您可以进行以下操作:

MyCustomComponent.Razor

<input type="text" @bind:get="BindingValue" @bind:set="SetAsync">

@code {


[Parameter]
public string BindingValue { get; set; }    
[Parameter]
public EventCallback<string> BindingValueChanged { get; set; }  

async Task SetAsync(string value)=> await BindingValueChanged.InvokeAsync(value);
}

那么你可以使用:

<MyCustomComponent @bind-BindingValue="whateverVariable" />                      

3

我花了一点时间才理解dani herreras 推荐的选项,所以我想为其他人提供一些清晰度。我想将所有文本输入都更改为Bootstrap 5.0 悬浮标签。继承自InputBase<string>给了我们很多可用的东西。 @CssClass 自动处理应用验证类,而@CurrentValue给出了组件的@bind-Value

InputComponent.razor

@using System.Linq.Expressions
@using Microsoft.AspNetCore.Components.Forms
@inherits InputBase<string>

<div class="form-floating mb-3">
  <input class="form-control @CssClass" id="@Id" @bind="@CurrentValue">
  <label for="@Id">@Label</label>
</div>

 <div class="form-control-validation">
    <ValidationMessage For="@ValidationFor" />
</div>

@code {

    [Parameter, EditorRequired] public Expression<Func<string>> ValidationFor { get; set; } = default!;
    [Parameter] public string? Id { get; set; }
    [Parameter] public string? Label { get; set; }

    // Note that this is only for implementing CurrentValueAsString
    protected override bool TryParseValueFromString(string? value, out string result, out string validationErrorMessage)
    {
        result = value;
        validationErrorMessage = null;
        return true;
    }
}

SomePage.razor

@using System.ComponentModel.DataAnnotations

<EditForm EditContext="@_editContext"  OnValidSubmit=@HandleValidSubmit>
    <DataAnnotationsValidator/>

    <button type="submit" class="btn btn-primary">Submit</button>
    <ValidationSummary />
    <InputComponent @bind-Value="person.Name" ValidationFor="@(()=>person.Name)" Label="Name" ></InputComponent>
    <p>Two way binded value: @person.Name</p>

</EditForm>
@code {

    private class ExamplePerson
    {
        [Required]
        public string Name { get; set; }
    }

    private ExamplePerson person { get; set; } = new ExamplePerson();

    private EditContext _editContext;

    protected override void OnInitialized()
    {
        _editContext = new(person);
        
    }
    private async void HandleValidSubmit()
    {

    }
}

此外,我们可以通过进行以下更改,使用Bootstrap 5.0类名进行验证。
protected override void OnInitialized()
{
    _editContext = new(person);
    _editContext.SetFieldCssClassProvider(new BootstrapValidationClassProvider());
}

public class BootstrapValidationClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
    {
        if (editContext == null)
            throw new ArgumentNullException(nameof(editContext));

        bool isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        if (editContext.IsModified(fieldIdentifier))
            return isValid ? "is-valid" : "is-invalid";
            
        return isValid ? string.Empty : "is-invalid";
    }
}

更新:我在回答这个问题时对Blazor还比较新。我们可以使用typeparam来实现所有类型的双向绑定,而不是继承InputBase<string>

@typeparam TItem
@inherits InputBase<TItem>

.
.
.

[Parameter, EditorRequired] public Expression<Func<TItem>> ValidationFor { get; set; } = default!;

protected override bool TryParseValueFromString(string? value, out TItem result, out string validationErrorMessage)
{
    result = (TItem)(object)value;
    validationErrorMessage = null;
    return true;
}

然后我们会像这样调用该组件

<InputComponent TItem="int" @bind-Value="person.Age" ValidationFor="@(()=>person.Age)" Label="Age" ></InputComponent>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接