Я пытаюсь создать простой элемент управления поисковым вводом с функцией устранения дребезга на основе таймера.
Search.razor
:
<input type = "search" placeholder = "Search" @oninput=@((e) => SearchChanged(e)) />
@code {
[Parameter]
public EventCallback<string> OnSearchChanged { get; set; }
private System.Timers.Timer? _debounceTimer;
string Text = string.Empty;
public void SearchChanged(ChangeEventArgs e)
{
Text = e.Value == null ? string.Empty : e.Value.ToString()!;
// Debounce search
_debounceTimer?.Stop();
_debounceTimer?.Dispose();
_debounceTimer = new System.Timers.Timer(200);
_debounceTimer.Elapsed += DebounceTimerElapsed;
_debounceTimer.Enabled = true;
_debounceTimer.Start();
}
private void DebounceTimerElapsed(object? sender, EventArgs e)
{
_debounceTimer?.Dispose();
OnSearchChanged.InvokeAsync(Text); // <<< Dispatcher exception
}
}
Я думаю, что имеет смысл включить в этот компонент функцию Debounce, чтобы мне не приходилось повторять ее для каждого родителя. Но я не могу использовать EventCallback, чтобы сообщить родительскому компоненту, что таймер истек. Это вызывает печально известное исключение The current thread is not associated with the Dispatcher
.
На этой странице Университета Блазор объясняется, что события, не связанные с пользовательским интерфейсом, такие как Timer.Elapsed
, появляются в другом потоке. Таймер действительно виноват, потому что, очевидно, он работает, если я звоню OnSearchChanged.InvokeAsync()
прямо из SearchChanged
. Но в этом случае InvokeAsync()
, похоже, не синхронизируется с потоком пользовательского интерфейса. Тем не менее, я думаю, что должен быть простой способ заставить эту работу работать.
Есть ли (простой) способ сообщить родительскому компоненту, что таймер в дочернем компоненте истек?
Спасибо, @MrCakaShaunCurtis. На самом деле я уже нашел первую статью в блоге Джереми Ликнесса.
Проблема в отделении средства устранения дребезга от логики поиска, которая, вероятно, является асинхронной и представляет собой своего рода удаленный вызов, заключается в том, что вы все равно можете получить состояние гонки, когда поиск выполняется медленнее, чем период устранения дребезга. Если вы их плотно свяжете, новый поиск будет выполняться только после завершения предыдущего.
Сообщение об ошибке, с которым вы сталкиваетесь, часто встречается при попытке обновить пользовательский интерфейс из потока, не связанного с пользовательским интерфейсом, в Blazor. Такая ситуация часто возникает в обработчиках событий асинхронных операций или таймерах, как в вашем случае с System.Timers.Timer
. Поскольку событие Elapsed таймера выполняется в потоке пула потоков, а не в потоке Blazor Dispatcher, попытка изменить состояние компонента напрямую приводит к этой ошибке.
Чтобы это исправить, необходимо убедиться, что логика обновления пользовательского интерфейса выполняется в потоке диспетчера Blazor. Этого можно добиться, используя метод InvokeAsync для обратного вызова в контекст компонента Blazor. Вот как вы можете изменить свой метод поиска для использования InvokeAsync
:
(Предполагая, что вы будете повторно использовать этот компонент и захотите @bind
использовать его.
DebouncedSearch.razor
@using System.Timers
<input @bind = "SearchText"
@bind:event = "oninput"
autocomplete = "off"
type = "text" />
@code {
[Parameter] public string? Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private Timer _debounceTimer = new();
private string _searchText = string.Empty;
private string SearchText
{
get => _searchText;
set
{
_searchText = value;
_debounceTimer.Stop();
_debounceTimer.Start();
}
}
protected override void OnInitialized()
{
_debounceTimer.Interval = 200;
_debounceTimer.AutoReset = false;
_debounceTimer.Elapsed += Search;
}
private async void Search(object? source, ElapsedEventArgs e)
{
await InvokeAsync(async () =>
{
await ValueChanged.InvokeAsync(_searchText);
});
}
}
Index.razor или любой другой родительский элемент
<DebouncedSearch @bind-Value = "@searchText" />
<label>Text: @searchText</label>
@code {
private string? searchText;
}
Для тех, кто читает это: ответ на ОП заключается в том, как вызывается InvokeAsync; await InvokeAsync(async () = { await ValueChanged.InvokeAsync(_searchText); });
Привязка события oninput с использованием метода получения/установки, как в этом ответе, или с использованием пустоты, как в исходном вопросе, является вопросом личных предпочтений.
Ваш входной код не скомпилируется — например, input @bind-value = "SearchText"
должно быть input @bind = "SearchText"
. То же самое с @bind-value:event = "oninput"
Совершенно никаких проблем.
Вот немного другая версия ответа @Verbe:
bind:after
для управления дебаунсером и передачи обновлений родителю.System.Threading.Timer
.@using System.Threading
<input @bind = "_searchText"
@bind:event = "oninput"
@bind:after = "Debounce"
autocomplete = "off"
type = "text" />
@code {
[Parameter] public string? Value { get; set; }
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
private System.Threading.Timer? _timer;
private string? _searchText;
private int _debounceMilliseconds = 300;
protected override void OnParametersSet()
{
this._searchText = this.Value;
}
private void Debounce()
{
_timer?.Dispose();
_timer = new Timer(this.OnTimerExpired, null, _debounceMilliseconds, -1);
}
private void OnTimerExpired(object? state)
{
_timer?.Dispose();
_timer = null;
// invoke OnValueChanged on the Synchronisation Context
this.InvokeAsync(OnValueChanged);
}
private Task OnValueChanged()
=> this.ValueChanged.InvokeAsync(_searchText);
}
Есть проблема в том, чтобы сделать это таким образом. Вы отделяете дебаунсер от логики поиска. Если эта логика представляет собой асинхронный вызов удаленной службы, вы все равно можете получить состояние гонки, когда поиск будет медленнее, чем ваш период устранения дребезга.
К вашему сведению — см. эти реализации отскока codeproject.com/Articles/5308567/An-Easier-Blazor-Debounce и shauncurtis.github.io/Building-Blazor-Applications/…