Blazor Server应用中异步事件触发及多用户页面同步验证方案咨询
Blazor Server 跨用户实时校验实现方案
核心改造思路
针对多用户同页面的实时校验需求,需要从共享状态管理、可靠事件通知、异常安全处理三个维度重构代码,解决静态事件的内存泄漏和异常传播问题。
1. 重构全局共享服务(Singleton)
将服务改为单例模式实现跨用户状态共享,用线程安全集合存储已选中元素,同时实现可靠的事件通知机制:
// 自定义事件参数:传递更新的元素信息 public class ElementUpdatedEventArgs : EventArgs { public IEnumerable<string> AddedElementIds { get; set; } = Enumerable.Empty<string>(); } // 全局共享服务 public class ElementSelectionService { // 线程安全存储:元素ID -> 占用用户ID private readonly ConcurrentDictionary<string, string> _selectedElements = new(); // 实例事件(替代静态事件,避免内存泄漏) public event AsyncEventHandler<ElementUpdatedEventArgs>? OnElementsUpdated; /// <summary> /// 保存选中元素(先校验再更新) /// </summary> /// <param name="elementIds">要选中的元素ID</param> /// <param name="currentUserId">当前操作用户ID</param> /// <returns>是否保存成功</returns> /// <exception cref="InvalidOperationException">元素已被占用时抛出</exception> public async Task<bool> SaveSelectedElementsAsync(IEnumerable<string> elementIds, string currentUserId) { // 1. 前置校验:检查元素是否已被其他用户选中 var occupiedElements = elementIds.Where(id => _selectedElements.ContainsKey(id) && _selectedElements[id] != currentUserId); if (occupiedElements.Any()) { throw new InvalidOperationException($"元素 [{string.Join(",", occupiedElements)}] 已被其他用户选中"); } // 2. 原子性更新共享状态 foreach (var id in elementIds) { _selectedElements.TryAdd(id, currentUserId); } // 3. 触发全局通知:通知所有在线用户更新状态 await NotifyElementsUpdatedAsync(new ElementUpdatedEventArgs { AddedElementIds = elementIds }); return true; } /// <summary> /// 获取当前可用元素列表 /// </summary> public IEnumerable<string> GetAvailableElementIds(IEnumerable<string> allElementIds, string currentUserId) { // 排除已被其他用户选中的元素 return allElementIds.Where(id => !_selectedElements.ContainsKey(id) || _selectedElements[id] == currentUserId); } /// <summary> /// 可靠触发事件:逐个处理订阅者,避免单个异常中断全局通知 /// </summary> private async Task NotifyElementsUpdatedAsync(ElementUpdatedEventArgs args) { var handlers = OnElementsUpdated; if (handlers == null) return; // 遍历每个订阅者,单独处理异常(不影响其他用户) foreach (var handler in handlers.GetInvocationList().Cast<AsyncEventHandler<ElementUpdatedEventArgs>>()) { try { await handler.Invoke(this, args); } catch (Exception ex) { // 记录异常(可注入ILogger),禁止吞掉异常 Console.WriteLine($"通知处理异常: {ex.Message}"); } } } }
2. 页面组件集成
在页面中注册事件、处理通知并更新UI,同时确保组件销毁时取消订阅:
@page "/element-selection" @inject ElementSelectionService SelectionService @implements IAsyncDisposable <!-- UI示例:显示可用元素和选中元素 --> <div> @if (!string.IsNullOrEmpty(ErrorMsg)) { <div class="alert alert-danger">@ErrorMsg</div> } <h3>可用元素</h3> @foreach (var id in AvailableElements) { <button class="btn btn-primary m-1" @onclick="() => SelectElement(id)" disabled="!IsElementAvailable(id)">@id</button> } <h3>我的选中</h3> @foreach (var id in UserSelectedElements) { <span class="badge bg-secondary m-1">@id <button @onclick="() => DeselectElement(id)">×</button></span> } <button class="btn btn-success mt-3" @onclick="SaveSelections">保存选中</button> </div> @code { private List<string> AllElements { get; set; } = new() { "A", "B", "C", "D", "E" }; private List<string> UserSelectedElements { get; set; } = new(); private List<string> AvailableElements { get; set; } = new(); private string? ErrorMsg { get; set; } // 模拟当前用户ID(实际可从认证系统获取) private string CurrentUserId = Guid.NewGuid().ToString(); protected override async Task OnInitializedAsync() { // 初始化可用元素列表 RefreshAvailableElements(); // 订阅全局事件 SelectionService.OnElementsUpdated += HandleElementsUpdated; await base.OnInitializedAsync(); } /// <summary> /// 处理全局元素更新通知 /// </summary> private async Task HandleElementsUpdated(object? sender, ElementUpdatedEventArgs e) { try { // 在Blazor同步上下文执行UI更新(避免线程异常) await InvokeAsync(() => { RefreshAvailableElements(); // 校验当前用户的选中项:移除已被其他用户占用的元素 var invalidSelections = UserSelectedElements.Intersect(e.AddedElementIds).ToList(); if (invalidSelections.Any()) { UserSelectedElements.RemoveAll(id => invalidSelections.Contains(id)); ErrorMsg = $"以下元素已被其他用户选中:{string.Join(",", invalidSelections)}"; } // 强制更新UI StateHasChanged(); }); } catch (Exception ex) { await InvokeAsync(() => { ErrorMsg = $"更新状态失败:{ex.Message}"; StateHasChanged(); }); } } /// <summary> /// 刷新可用元素列表 /// </summary> private void RefreshAvailableElements() { AvailableElements = SelectionService.GetAvailableElementIds(AllElements, CurrentUserId).ToList(); } // 选中/取消选中元素的逻辑 private void SelectElement(string id) { if (!UserSelectedElements.Contains(id)) { UserSelectedElements.Add(id); ErrorMsg = null; } } private void DeselectElement(string id) { UserSelectedElements.Remove(id); ErrorMsg = null; } /// <summary> /// 保存选中元素 /// </summary> private async Task SaveSelections() { if (!UserSelectedElements.Any()) { ErrorMsg = "请选择至少一个元素"; return; } try { await SelectionService.SaveSelectedElementsAsync(UserSelectedElements, CurrentUserId); ErrorMsg = "保存成功"; } catch (InvalidOperationException ex) { ErrorMsg = ex.Message; } catch (Exception ex) { ErrorMsg = $"保存失败:{ex.Message}"; } } /// <summary> /// 组件销毁时取消事件订阅(避免内存泄漏) /// </summary> public async ValueTask DisposeAsync() { SelectionService.OnElementsUpdated -= HandleElementsUpdated; await ValueTask.CompletedTask; } private bool IsElementAvailable(string id) => AvailableElements.Contains(id); }
3. 服务注册
在Program.cs中将服务注册为单例:
builder.Services.AddSingleton<ElementSelectionService>();
关键特性说明
- 线程安全状态管理:使用
ConcurrentDictionary确保多用户并发操作时的状态一致性。 - 可靠事件通知:遍历每个订阅者单独处理,单个订阅者的异常不会中断其他用户的通知,同时记录异常避免“即发即弃”。
- UI线程安全:通过
InvokeAsync确保UI更新在Blazor同步上下文执行,避免跨线程异常。 - 内存泄漏防护:使用实例事件替代静态事件,组件销毁时取消订阅,避免组件实例被长期引用。
内容的提问来源于stack exchange,提问作者Marvin Klein
相关产品推荐
相关产品推荐

