Blazor自定义组件上的双向绑定

21

我正在创建一个 Blazor 服务器端应用程序,并且在两个自定义组件之间绑定值时遇到问题。

我查看了多种示例,了解了 @bind 或绑定的工作方式,但是我无法弄清楚这方面的最新信息。

给定一个模型类 User:

public class User
    {
        [Mapping(ColumnName = "short_id")]
        public string ShortId { get; set; }

        public User()
        {

        }
    }

我想建立一个表单,显示该用户的所有属性,并将它们显示在输入框中,以便进行编辑,并最终保存到数据库中。

我的表单(父组件)长这样:

<div class="edit-user-form">
    <AnimatedUserInput TbText="@User.ShortId" Placeholder="MHTEE Id"/>
    <button class="btn btn-secondary" @onclick="Hide" style="{display:inline-block;}">Back</button>
    <button class="btn btn-primary" @onclick="SaveUser" style="{display:inline-block;}">Save</button>
</div>
@code {
    [Parameter] public vUser User { get; set; }

    [Parameter] public Action Hide { get; set; }

    bool _error = false;

    void SaveUser()
    {
        ValidateUserData();
    }

    void ValidateUserData()
    {
        _error = string.IsNullOrWhiteSpace(User.ShortId);
    }
}

其中AnimatedUserInput是一个自定义组件,长得像这样:

<div class="edit-area @FocusClass @WiggleClass">
    <input type="text" @bind="@TbText" />
    <span data-placeholder="@Placeholder"></span>
</div>
@code {
    [Parameter]
    public string TbText { get; set; }

    [Parameter]
    public string Placeholder { get; set; }
}

现在在输入文本框中,我可以正确地看到父组件中User对象的ShortId

但是,如果我更改输入框中的文本并单击Save按钮,该按钮会触发ValidateUserData方法,并允许我查看当前User对象,我会发现实际的User.ShortId属性没有发生任何更改,只有在输入框中发生了更改。

有没有办法将它绑定起来,以便输入框中的更改会自动应用于绑定的属性?

我的一些属性需要在表单中显示,这就是为什么我不想钩接每个属性的自定义OnChanged事件的原因。

5个回答

34

好的,对于任何偶然发现这篇文章的人。我尝试了一些方法并找到了解决方案。 所以我在我的自定义输入组件AnimatedUserInput中添加了一个事件回调,每当输入框中的值更新时,我就调用它:

@code {

    [Parameter]
    public string TbText
    {
        get => _tbText;
        set
        {
            if (_tbText == value) return;

            _tbText = value;
            TbTextChanged.InvokeAsync(value);
        }
    }

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

    [Parameter]
    public string Placeholder { get; set; }

    private string _tbText;
}

我的父组件上的绑定代码如下:

<div class="edit-user-form">
    <AnimatedUserInput @bind-TbText="@User.ShortId" Placeholder="MHTEE Id"/>
    <AnimatedUserInput @bind-TbText="@User.FirstName" Placeholder="First name" />
    <AnimatedUserInput @bind-TbText="@User.LastName" Placeholder="Last name" />
    <AnimatedUserInput @bind-TbText="@User.UserName" Placeholder="Username" />
    <AnimatedUserInput @bind-TbText="@User.StaffType" Placeholder="Staff type" />
    <AnimatedUserInput @bind-TbText="@User.Token" Placeholder="Token" />
    <button class="btn btn-secondary" @onclick="Hide" style="{display:inline-block;}">Back</button>
    <button class="btn btn-primary" @onclick="SaveUser" style="{display:inline-block;}">Save</button>
</div>

@code {
    [Parameter] public vUser User { get; set; }
}
这样,Blazor就可以正确地将值绑定在一起,并以我期望的方式从两侧更新它们。

2
这让我疯了,谢谢你的提示!我在我的字符串值上缺少本地设置以调用更改。这也是一个很难搜索的问题。 - chrisbyte

13

有关Blazor的一些数据绑定资源未提及,当您执行@bind-Value时,实际上会自动映射到3个参数:

绑定调用

<CustomComponent TValue="DateTime?" @bind-Value="order.PurchaseDate"></CustomComponent>

自定义组件

@using System.Linq.Expressions;
@typeparam TValue
//...
@code {
[Parameter]
public virtual TValue Value { get; set; }
[Parameter]
public EventCallback<TValue> ValueChanged { get; set; }
[Parameter]
public Expression<Func<TValue>> ValueExpression { get; set; }
//...
}

如果您想要简化用户界面的组件,那么了解这一点非常重要,因为您可能需要该值表达式进行表单验证...

<ValidationMessage For="ValueExpression" ></ValidationMessage>

类似地,我使用这种ValueExpression访问方法来运行我的标签,因为你可能想要一个模型数据注释,就像你可能想知道字段是否具有最大长度或是否必填...

@( DisplayName.For(ValueExpression) )

代码显示名称:

public static class StringHelpers
{
    public static string CamelCaseToPhrase(this string self)
    {
        return Regex.Replace(self, "([A-Z])", " $1").Trim();
    }
}

public static class DisplayName
{
    public static string For(string input)
    {
        return input.CamelCaseToPhrase();
    }

    public static string For<T>(Expression<Func<T>> accessor)
    {
        var expression = (MemberExpression)accessor.Body;
        var value = expression.Member.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute;
        return value?.Name ?? expression.Member.Name?.CamelCaseToPhrase() ?? "";
    }

}

我在这里分享这个内容,因为当我搜索时,Google会显示这个页面,而其他大多数资源并没有完整地给出信息。希望这可以对其他人有所帮助,特别是如果您想访问数据注释,则该模式也已包括在内。


3
谢谢你们在这里发布的示例。我使用它们,基于W3schools' How TO-Toggle Switch中的Toggle Switch示例,做了一个自定义的输入复选框双向绑定组件,就像这样:

enter image description here

它在状态改变时触发事件。

该组件

<div class="@Class">
    <label class="switch">
        <input id="@Id" type="checkbox" @bind="IsOn" />
        <span class="slider round"></span>
    </label>
</div>

@code {
    [Parameter] public EventCallback<bool> OnToggle { get; set; }
    [Parameter] public string Id { get; set; }
    [Parameter] public string Class { get; set; }
    [Parameter] public bool SetAsChecked { get; set; }

    private bool _isSettingUp = true;
    private bool _isOn;

    protected override Task OnInitializedAsync()
    {
        IsOn = SetAsChecked;
        return base.OnInitializedAsync();
    }

    private bool IsOn
    {
        get => _isOn;
        set
        {
            _isOn = value;
            if (!_isSettingUp)
            {
                OnToggle.InvokeAsync(value);
            }
            _isSettingUp = false;
        }
    }
}

“_isSettingUp”标记是我发现的一种方法,可以在首次设置检查时不触发EventCallback。也许有人会找到更好的处理方式。

父级

(这里是您使用组件的地方)
<ToggleSwitch Id="mySwitch" OnToggle="((isChecked)=>DoSomething(isChecked))" SetAsChecked="isChecked" />

@code {
    private bool isChecked;

    protected override void OnInitialized()
    {
        isChecked = true;
    }

    protected void DoSomething(bool isChecked)
    {
        Console.WriteLine($">>>> {isChecked} <<<<");
    }
}

CSS样式表

CSS样式表是从W3Scholl的示例这里进行了改编。 如果您想利用Blazor CSS隔离,则应将其放置在一个名为组件名称的.razor.css文件中(位于同一文件夹中)。然后,您可以使用组件的Class参数来适应不同的需求。

.switch {
    position: relative;
    display: inline-block;
    width: 60px;
    margin-top: 7px;
}

    .switch input {
        opacity: 0;
        width: 0;
        height: 0;
    }

.slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    padding-top: 2px;
    background-color: #737272;
    -webkit-transition: .7s;
    transition: .7s;
    width: 62px;
    height: 29px;
}

    .slider:before {
        position: absolute;
        content: "\2716";
        text-align: center;
        color: dimgrey;
        height: 25px;
        width: 25px;
        left: 2px;
        bottom: 2px;
        padding-top: 1px;
        background-color: white;
        -webkit-transition: .8s;
        transition: .8s;
    }

input:checked + .slider {
    background-color: limegreen;
}

    input:checked + .slider:before {
        -webkit-transform: translateX(33px);
        -ms-transform: translateX(33px);
        transform: translateX(33px);
        content: "\2714";
        text-align: center;
        color: dodgerblue;
    }

.slider.round {
    border-radius: 20px;
}

    .slider.round:before {
        border-radius: 50%;
    }


顺便说一下,您可以在开关中放置任何HTML符号。我从这个很酷的页面选择了这些符号,该页面具有搜索功能和CSS代码,如果我们在CSS内容中添加符号,那么这就是我们需要的。
希望对某人有用。

1

这是我实现双向绑定的方法

父组件

<LabelledField Class=""
   Label="Label value"
   DateValue="@Model.Value"
   OnDateChange="@((e) => Model.Value = e )"
   Type="InputType.date" />

子组件

<div class="@Class">
<div class="ft-07 mb-03 field-label @LabelClass">@Label</div>
@switch (Type)
{
    case InputType.constant:
        <div class="label-field-value @ValueClass">@Value</div>
        break;
    case InputType.text:
        <input class="form-control input @ValueClass"
               type="text"
               @bind="Value" />

        break;
    case InputType.date:
        <input class="form-control input @ValueClass"
               type="date"
               @bind="DateValue" />

        break;
    default:
        break;
}

@code{
private string _tbText;
private DateTime? _tbDate;

[Parameter]
public string Class { get; set; }

[Parameter]
public string Label { get; set; }

[Parameter]
public string LabelClass { get; set; }

[Parameter]
public string Value
{
    get => _tbText;
    set
    {
        if (_tbText == value) return;

        _tbText = value;
        OnTextChange.InvokeAsync(value);
    }
}

[Parameter]
public DateTime? DateValue
{
    get => _tbDate;
    set
    {
        if (_tbDate == value) return;

        _tbDate = value;
        OnDateChange.InvokeAsync(_tbDate);
    }
}

[Parameter]
public string ValueClass { get; set; }

[Parameter]
public InputType Type { get; set; } = InputType.constant;

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

[Parameter]
public EventCallback<DateTime?> OnDateChange { get; set; }
}

0
可能对某些人有用。 使用 InputBase 类 示例 组件 EditText.razor
@inherits InputText

<div class="@cssClass">    
@if (viewEdit)
{
    <div @onclick="EditInput">
        @Value
    </div> 
    <label class="hover">

    </label>
    <label class="@view">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16">
            <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
        </svg>
    </label>
}
else
{
    <input @ref="textInput" @bind:event="oninput" 
@bind="CurrentValueAsString" @onkeydown="ClickKey" @onfocusout="FocusOut">
}
</div>
@code {

[Parameter]
public string cssClass { get; set; }

public bool viewEdit { get; set; } = true;
ElementReference textInput;
// If you want to run JS
 //Guid guid { get; set; }
//protected override void OnInitialized()
//{
//    guid = Guid.NewGuid();
//}
//protected override async Task OnAfterRenderAsync(bool firstRender)
//{
//    if (firstRender)
//    {
//        await JSRuntime.InvokeVoidAsync("customMask",guid.ToString(), AdditionalAttributes["data-mask"].ToString(), AdditionalAttributes["data-placeholder"].ToString());
//    }
//}
async Task EditInput()
{
    viewEdit = !viewEdit;
    if (!viewEdit)
    {
        await Task.Run(async () => await textInput.FocusAsync());            
    }
}

async Task FocusOut()
{
    viewEdit = true;
    viewIcon();
}

async Task ClickKey(KeyboardEventArgs e)
{
    if (e.Code == "Enter")
    {
        viewEdit = true;
        viewIcon();
    }        
}

bool viewClass = false;

public string view => viewClass ? "active" : "hidden";

async Task viewIcon()
{
    viewClass = true;        
    await Task.Delay(2000);        
    viewClass = false;
    StateHasChanged();
}

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

}

使用方法
// With JS
<EditText data-mask="Date" data-placeholder="_" @bind-Value="myhtml" cssClass="form" />

<EditText cssClass="form" @bind-Value="@myhtml"></EditText>

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