Blazor Textfield Oninput 用户输入延迟

Blazor Textfield Oninput User Typing Delay

如何在 Blazor 中为事件 (OnInput) 添加延迟?
例如,如果用户正在文本字段中键入内容,而您想等到用户完成键入内容。

Blazor.Templates::3.0.0-preview8.19405.7

代码:

@page "/"
<input type="text" @bind="Data" @oninput="OnInputHandler"/>
<p>@Data</p>

@code {    
    public string Data { get; set; }   

    public void OnInputHandler(UIChangeEventArgs e)
    {
        Data = e.Value.ToString();
    }    
}

解决方案:

您的问题没有单一的解决方案。以下代码只是一种方法。看看并根据您的要求进行调整。该代码在每个 keyup 上重置一个计时器,只有最后一个计时器引发 OnUserFinish 事件。 记得通过实现 IDisposable

来处理定时器
@using System.Timers;
@implements IDisposable;

<input type="text" @bind="Data" @bind:event="oninput" 
       @onkeyup="@ResetTimer"/>
<p >UI Data: @Data
<br>Backend Data: @DataFromBackend</p>

@code {
    public string Data { get; set; } = string.Empty;
    public string DataFromBackend { get; set; }  = string.Empty;
    private Timer aTimer = default!;
    protected override void OnInitialized()
    {
        aTimer = new Timer(1000);
        aTimer.Elapsed += OnUserFinish;
        aTimer.AutoReset = false;
    }
    void ResetTimer(KeyboardEventArgs e)
    {
        aTimer.Stop();
        aTimer.Start();        
    }    
    private async void OnUserFinish(Object? source, ElapsedEventArgs e)
    {
        // 
        // Call backend
        DataFromBackend = await Task.FromResult( Data + " from backend");
        await InvokeAsync( StateHasChanged );
    }
    void IDisposable.Dispose()
        =>
        aTimer?.Dispose();    
}

用例:

此代码的一个用例示例是避免后端请求,因为在用户停止输入之前不会发送请求。

运行:

我创建了一组Blazor components. One of which is Debounced inputs with multiple input types and much more features. Blazor.Components.Debounce.Input is available on NuGet

你可以用 demo app 试试看。

注意:目前它处于预览阶段。 .NET 5 随附最终版本。发布

这个答案是之前答案之间的中间地带,即介于 and .

之间

它利用了强大的Reactive.Extensions库(a.k.a.Rx),在我看来这是正常情况下解决此类问题的唯一合理方法场景。

解决方案

安装 NuGet 包后 System.Reactive 您可以在组件中导入所需的命名空间:

@using System.Reactive.Subjects
@using System.Reactive.Linq

在组件上创建一个 Subject 字段,作为输入事件和 Observable 管道之间的粘合剂:

@code {
    private Subject<ChangeEventArgs> searchTerm = new();
    // ...
}

Subject 与您的 input 连接:

<input type="text" class="form-control" @oninput=@searchTerm.OnNext>

最后,定义Observable管道:

@code {
    // ...

    private Thing[]? things;

    protected override async Task OnInitializedAsync() {
        searchTerm
            .Throttle(TimeSpan.FromMilliseconds(200))
            .Select(e => (string?)e.Value)
            .Select(v => v?.Trim())
            .DistinctUntilChanged()
            .SelectMany(SearchThings)
            .Subscribe(ts => {
                things = ts;
                StateHasChanged();
            });
    }

    private Task<Thing[]> SearchThings(string? searchTerm = null)
        => HttpClient.GetFromJsonAsync<Thing[]>($"api/things?search={searchTerm}")
}

上面的示例管道将...

  • 给用户 200 毫秒来完成输入(a.k.a。去抖动节流 输入),
  • select 来自 ChangeEventArgs
  • 的键入值
  • trim它,
  • 跳过任何与上一个相同的值,
  • 使用到目前为止的所有值来发出 HTTP GET 请求,
  • 将响应数据存储在字段 things
  • 最后告诉组件需要重新渲染。

如果您的标记中有类似下面的内容,您将在键入时看到它正在更新:

@foreach (var thing in things) {
    <ThingDisplay Item=@thing @key=@thing.Id />
}

补充说明

别忘了清理

您应该像这样正确处理事件订阅:

@implements IDisposable // top of your component

// markup

@code {
    // ...

    private IDisposable? subscription;

    public void Dispose() => subscription?.Dispose();

    protected override async Task OnInitializedAsync() {
        subscription = searchTerm
            .Throttle(TimeSpan.FromMilliseconds(200))
            // ...
            .Subscribe(/* ... */);
    }
}

Subscribe() 实际上是 returns 一个 IDisposable,您应该将其与您的组件一起存储和处置。但是不要在上面使用using,因为这会过早地破坏订阅。

未决问题

有些事情我还没有想通:

  • 是否可以避免调用 StateHasChanged()
  • 是否可以避免调用 Subscribe() 并像在 Angular 中使用 async pipe 那样直接绑定到标记内的 Observable
  • 是否可以避免创建 Subject? Rx 支持从 C# Events 创建 Observables,但是如何获取 oninput 事件的 C# 对象?

我认为这对我来说是更好的解决方案,我用它来进行搜索。 这是我使用的代码。

    private DateTime timer {
      get;
      set;
    } = DateTime.MinValue;


    private async Task SearchFire(ChangeEventArgs Args) {
      if (timer == DateTime.MinValue) {
        timer = DateTime.UtcNow;
      } else {
        _ = StartSearch(Args);
        timer = DateTime.UtcNow;
      }
    }

    private async Task StartSearch(ChangeEventArgs Args) { //2000 = 2 seconeds you can change it 
      await Task.Delay(2000);
      var tot = TimeSpan.FromTicks((DateTime.UtcNow - timer).Ticks).TotalSeconds;
      if (tot > 2) {
        if (!string.IsNullOrEmpty(Args.Value.ToString())) { //Do anything after 2 seconds.

          //reset timer after finished writhing
          timer = DateTime.MinValue;
        } else {}
      } else {}
    }

您可以避免绑定输入。只需设置@oninput

<Input id="theinput" @oninput="OnTextInput" />
@code {
    public string SomeField { get; set; }
    public void OnTextInput(ChangeEventArgs e)
    {
        SomeField = e.Value.ToString();
    }
}

并在javascript中设置初始值(如果有的话)。

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JSRuntime.InvokeVoidAsync("setInitialValueById", "theinput", SomeField);
    }
}

setInitialValueById方法:

window.setInitialValueById = (elementId, value) => {
document.getElementById(elementId).value = value;}

这将解决 blazor 中已知的输入延迟问题。如果是这种情况,您可以设置延迟的标签值:

public async Task OnTextInput(ChangeEventArgs e)
{
    var value = e.Value.ToString();
    SomeField = value;
    await JSRuntime.InvokeVoidAsync("setLabelValue", value);
}

setLabelValue方法:

let lastInput;
window.setLabelValue = (value) => {
    lastInput = value;
    setTimeout(() => {
        let inputValue = value;
        if (inputValue === lastInput) {
            document.getElementById("theLabelId").innerHTML = inputValue;
        }
    }, 2000);
}