Blazor/.NET8任务执行时阻止OnValidSubmit重复触发
Blazor/.NET8 注册组件重复提交问题解决
问题描述
我开发了一个Blazor/.NET8 Web应用,在用户注册组件中遇到两个核心问题:
- 无法在任务运行时阻止
OnValidSubmit重复触发 - 无法禁用提交按钮防止重复点击
尝试过的方案及异常表现
首先尝试用私有bool变量控制任务执行,但提交操作会将该bool重置为false;之后创建单例StateService持久化状态,结果依旧。
StateService代码
namespace TripSuite.Service { public interface IStateService { bool IsSubmitting { get; set; } } public class StateService : IStateService { public bool IsSubmitting { get; set; } = false; } }
服务注册(program.cs)
builder.Services.AddSingleton<IStateService, StateService>();
注册组件代码
<EditForm Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="HandleSubmit" FormName="register"> <DataAnnotationsValidator /> <hr /> <ValidationSummary class="text-danger" role="alert" /> <div class="form-floating mb-3"> <InputText @bind-Value="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" /> <label for="email">Email</label> <ValidationMessage For="() => Input.Email" class="text-danger" /> </div> <div class="form-floating mb-3"> <InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" /> <label for="password">Password</label> <ValidationMessage For="() => Input.Password" class="text-danger" /> </div> <div class="form-floating mb-3"> <InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" /> <label for="confirm-password">Confirm Password</label> <ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" /> </div> <MudButton ButtonType="ButtonType.Submit" Disabled="@stateService.IsSubmitting">Register</MudButton> </EditForm>
组件后台代码
private async void HandleSubmit() { if (!stateService.IsSubmitting) { stateService.IsSubmitting = true; StateHasChanged(); await RegisterUser(); } }
异常细节
注释掉await RegisterUser();后,UI表现正常,提交按钮会被禁用;但保留该调用时,重复提交问题依旧。RegisterUser方法用于创建用户并设置默认值:
public async Task RegisterUser() { var user = CreateUser(); await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); var emailStore = GetEmailStore(); await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); var result = await UserManager.CreateAsync(user, Input.Password); if (!result.Succeeded) { identityErrors = result.Errors; return; } var userId = await UserManager.GetUserIdAsync(user); var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = NavigationManager.GetUriWithQueryParameters( NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); var usersCount = await UserManager.Users.CountAsync(); if (usersCount == 1) // 第一个用户刚创建 { var adminRoleExists = await RoleManager.RoleExistsAsync("Admin"); if (!adminRoleExists) { await RoleManager.CreateAsync(new IdentityRole("Admin")); } await UserManager.AddToRoleAsync(user, "Admin"); await UserManager.AddClaimAsync(user, new Claim("Permission", "CanAccessEverything")); } var newUserSettings = new UserSettings { UserID = userId, SelectedTheme = "Dark" }; var newEntity = new Entities { Email = user.Email, UserID = userId, EntityType = Enums.EntityTypes.WebUser }; DB.Entities.Add(newEntity); DB.UserSettings.Add(newUserSettings); await DB.SaveChangesAsync(); if (UserManager.Options.SignIn.RequireConfirmedAccount) { RedirectManager.RedirectTo( "Account/RegisterConfirmation", new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); } await SignInManager.SignInAsync(user, isPersistent: false); RedirectManager.RedirectTo(ReturnUrl); }
补充测试
新建服务器端项目,用Identity脚手架生成的注册组件测试,依然能重复触发OnValidSubmit:
<div class="col-md-4"> <StatusMessage Message="@Message" /> <EditForm Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="RegisterUser" FormName="register"> <button type="submit" class="w-100 btn btn-lg btn-primary" disabled="@_isSubmitting">Register</button> </EditForm> </div>
private bool _isSubmitting = false; public async Task RegisterUser(EditContext editContext) { if (!_isSubmitting) { _isSubmitting = true; await Task.Delay(100); // 注册逻辑 _isSubmitting = false; } else { return; } }
问题原因及修复方案
核心原因
async void误用:HandleSubmit使用async void导致Blazor无法跟踪异步操作状态,引发UI更新异常- 状态未全局重置:
RegisterUser执行失败时未重置提交状态,重定向操作会触发组件重新渲染,私有变量被重置 - 原生表单提交行为:
EditForm默认触发浏览器表单提交,导致页面刷新重置组件状态
修复步骤
1. 替换async void为async Task
Blazor需要通过Task跟踪异步操作,修改HandleSubmit:
private async Task HandleSubmit() { if (stateService.IsSubmitting) return; stateService.IsSubmitting = true; try { await RegisterUser(); } finally { // 无论成功失败都重置状态 stateService.IsSubmitting = false; StateHasChanged(); } }
2. 阻止原生表单提交
在EditForm中添加原生事件阻止默认提交,避免页面刷新:
<EditForm Model="Input" asp-route-returnUrl="@ReturnUrl" OnValidSubmit="HandleSubmit" FormName="register" @onsubmit="e => e.PreventDefault()">
3. 修复单例状态共享问题
单例StateService会导致多用户共享状态,改用组件私有变量更合理:
private bool _isSubmitting = false; private async Task HandleSubmit() { if (_isSubmitting) return; _isSubmitting = true; try { await RegisterUser(); } finally { _isSubmitting = false; StateHasChanged(); } }
按钮绑定改为:
<MudButton ButtonType="ButtonType.Submit" Disabled="@_isSubmitting">Register</MudButton>
4. 确保RegisterUser全分支重置状态
修改RegisterUser,在所有退出路径重置状态:
public async Task RegisterUser() { try { // 原有注册逻辑... } finally { _isSubmitting = false; StateHasChanged(); } }
5. 测试项目的修复
对测试项目的RegisterUser做同样修改,并添加表单阻止默认提交:
private bool _isSubmitting = false; public async Task RegisterUser(EditContext editContext) { if (_isSubmitting) return; _isSubmitting = true; try { await Task.Delay(100); // 注册逻辑 } finally { _isSubmitting = false; StateHasChanged(); } }
<EditForm Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="RegisterUser" FormName="register" @onsubmit="e => e.PreventDefault()">
内容的提问来源于stack exchange,提问作者Gary Tripodi
相关产品推荐
相关产品推荐

