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# Event
s 创建 Observable
s,但是如何获取 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);
}
如何在 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 随附最终版本。发布
这个答案是之前答案之间的中间地带,即介于
它利用了强大的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#Event
s 创建Observable
s,但是如何获取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);
}