You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

bUnit测试带防抖功能的Search组件时双向绑定失效问题求解

问题根源

  1. 测试代码未模拟@bind的自动赋值逻辑
    你在测试中手动通过Add方法传入Value和ValueChanged时,仅在ValueChanged回调里给myString赋值,没有同步更新StringProperty,所以StringProperty自然不会发生变化。Blazor的@bind-Value语法本质是编译器自动生成了以下逻辑:
Value = Query,
ValueChanged = (s) => Query = s

你测试里没写给StringProperty赋值的逻辑,注释的断言必然失败。

  1. 组件存在不符合Blazor规范的隐藏问题
    你在CurrentValue的setter里直接修改了[Parameter]修饰的Value属性,Blazor规范要求子组件不能自行修改父组件传入的参数属性,所有参数变更都应该通过EventCallback通知父组件更新后,由父组件重新传入新值。你当前写法实际运行未出问题是因为防抖触发ValueChanged后,父组件传入的新值和你本地修改的值刚好一致,属于巧合。

修复方案

1. 修复测试代码

要验证双向绑定逻辑,有两种常用写法:

写法1:手动补全ValueChanged的赋值逻辑

private string StringProperty { get; set; }

[Fact]
public async Task AfterDebounce_ValueUpdates()
{
    var cut = RenderComponent<Search>(parameters => parameters
            .Add(p => p.Value, StringProperty)
            .Add(p => p.ValueChanged, (s) => StringProperty = s)
        );

    var input = cut.Find("input");
    input.Input("unit test");

    Assert.Equal("unit test", cut.Instance.Value);
    Assert.NotEqual("unit test", StringProperty);

    await Task.Delay(500); // 防抖时长为350ms,等待500ms足够覆盖
    Assert.Equal("unit test", StringProperty);
}

写法2:使用bUnit内置的Bind辅助方法,自动模拟@bind行为

[Fact]
public async Task AfterDebounce_ValueUpdates()
{
    var stringProperty = "";
    var cut = RenderComponent<Search>(parameters => parameters
            .Bind(p => p.Value, () => stringProperty, v => stringProperty = v)
        );

    var input = cut.Find("input");
    input.Input("unit test");

    Assert.Equal("unit test", cut.Instance.Value);
    Assert.NotEqual("unit test", stringProperty);

    await Task.Delay(500);
    Assert.Equal("unit test", stringProperty);
}

2. 可选:优化组件写法符合Blazor规范

不要直接修改Value参数,本地独立维护输入备份值即可:

public class Search : ComponentBase
{    
    [Parameter] public string? Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }

    [DisallowNull] public ElementReference? Element { get; protected set; }

    private System.Timers.Timer timer = null;
    // 本地维护当前输入值,不直接修改父组件传入的参数
    private string? _currentInputValue;
    protected override void OnParametersSet()
    {
        // 父组件传入新Value时同步本地值
        _currentInputValue = Value;
        base.OnParametersSet();
    }

    protected string? CurrentValue {
        get => _currentInputValue;
        set {
            var hasChanged = !EqualityComparer<string>.Default.Equals(value, _currentInputValue);
            if (hasChanged)
            {
                _currentInputValue = value;

                DisposeTimer();
                timer = new System.Timers.Timer(350);
                timer.Elapsed += TimerElapsed_TickAsync;
                timer.Enabled = true;
                timer.Start();
            }
        }
    }

    private void DisposeTimer()
    {
        if (timer != null)
        {
            timer.Enabled = false;
            timer.Elapsed -= TimerElapsed_TickAsync;
            timer.Dispose();
            timer = null;
        }
    }

    private async void TimerElapsed_TickAsync(
        object sender,
        EventArgs e)
    {
        await ValueChanged.InvokeAsync(_currentInputValue);
    }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(10, "input");
        builder.AddAttribute(20, "type", "text");
        builder.AddAttribute(60, "value", BindConverter.FormatValue(CurrentValue));
        builder.AddAttribute(70, "oninput", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValue = __value, CurrentValue));
        builder.AddElementReferenceCapture(80, __inputReference => Element = __inputReference);
        builder.CloseElement();
    }
}

内容的提问来源于stack exchange,提问作者thalacker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 06:15:03