Blazor自定义长按按钮:解决重复触发及衍生功能问题
核心问题:长按/点击动作重复触发修复
问题根源
原代码中Task.Delay.ContinueWith使用了TaskScheduler.Default,导致ExecuteAction在后台线程执行,而_actionExecuted作为共享状态未做线程同步保护,加上NavigationManager.NavigateTo的同步特性,会导致按钮状态重置异常,进而引发动作重复触发。
修复方案
- 改用
await替代ContinueWith,确保代码在Blazor UI线程上下文执行; - 用
Interlocked保证_actionExecuted的线程安全; - 优化状态重置逻辑,导航操作后不重置按钮状态。
修复后的核心代码:
@inherits MudButton @inject IJSRuntime _jsRuntime <MudButton @onmousedown="StartTimer" @onmouseup="HandleRelease" @ontouchstart="StartTimer" @ontouchend="HandleRelease" Disabled="@(Disabled || _isExecuting)"> @ChildContent </MudButton> @code { [Parameter] public EventCallback<object> OnAction { get; set; } [Parameter] public int HoldDuration { get; set; } = 300; [Parameter] public string DataTestId { get; set; } [Parameter] public bool Disabled { get; set; } private CancellationTokenSource _cts; private bool _actionExecuted; private bool _isExecuting; private bool _isNavigating; // 标记是否为导航操作 private async Task StartTimer() { StopTimer(); _actionExecuted = false; _isExecuting = false; _cts = new CancellationTokenSource(); try { await Task.Delay(HoldDuration, _cts.Token); if (!_cts.Token.IsCancellationRequested) { await ExecuteAction(true); } } catch (TaskCanceledException) { // 定时器被取消,无需处理 } } private async Task HandleRelease() { StopTimer(); await ExecuteAction(false); // 收起软键盘 await _jsRuntime.InvokeVoidAsync("window.blurActiveElement"); } private void StopTimer() { _cts?.Cancel(); _cts?.Dispose(); _cts = null; } private async Task ExecuteAction(bool fromTimer) { string origin = fromTimer ? "timer" : "touch end"; // 线程安全的状态检查 if (Interlocked.CompareExchange(ref _actionExecuted, true, false) || _isExecuting || Disabled) { Console.WriteLine($" KioskButton - {origin} - {DataTestId} - action tried to double-fire."); return; } _isExecuting = true; try { Console.WriteLine($"a) KioskButton - {origin} - {DataTestId} - action triggered and passed check."); if (OnAction.HasDelegate) { Console.WriteLine($"b) KioskButton - {origin} - {DataTestId} - performing OnAction."); await OnAction.InvokeAsync(Parameters); } } finally { // 导航操作后不重置状态,避免重复触发 if (!_isNavigating) { _isExecuting = false; _actionExecuted = false; } } } // 暴露给外部设置导航状态的方法 public void SetNavigating(bool isNavigating) { _isNavigating = isNavigating; } }
调用导航时先标记状态:
await kioskButtonRef.SetNavigating(true); NavigationManager.NavigateTo("/target-page");
衍生问题逐一解决
1. MudDialog调度器异常修复
调用对话框关闭时引发调度器异常,通常是因为操作不在UI线程上下文或对话框已关闭。
解决方案:
- 用
InvokeAsync包裹对话框操作,确保在UI线程执行; - 先检查对话框状态,避免无效操作;
- 捕获异常防止崩溃。
示例代码:
private async Task CloseDialog() { try { if (Dialog != null && Dialog.IsOpen) { await InvokeAsync(() => Dialog.Close(DialogResult.Ok(true))); await StateHasChangedAsync(); } } catch (InvalidOperationException ex) { Console.WriteLine($"Dialog close error: {ex.Message}"); // 异常处理逻辑 } }
2. Squeekboard软键盘自动收起问题
长按按钮后软键盘未自动收起,手动聚焦按钮会导致对话框无法显示。
解决方案:
- 在
wwwroot/index.html添加全局JS方法:
<script> window.blurActiveElement = () => { const activeElement = document.activeElement; if (activeElement && activeElement.tagName !== 'BODY') { activeElement.blur(); } }; </script>
- 在按钮释放时调用该方法(已包含在核心修复代码的
HandleRelease中)。
3. 按钮禁用逻辑冲突处理
外部传入的Disabled参数与内部执行时的禁用逻辑冲突,需合并两者状态。
解决方案:
- 内部维护
_isExecuting标记按钮是否正在执行动作; - 在MudButton的
Disabled属性绑定Disabled || _isExecuting,合并外部禁用和内部执行状态; - 在
ExecuteAction中优先检查外部Disabled状态,避免无效触发。
核心绑定逻辑已包含在组件代码中:Disabled="@(Disabled || _isExecuting)"
内容的提问来源于stack exchange,提问作者Shaggydog
相关产品推荐
相关产品推荐

