You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>();

关键特性说明

  1. 线程安全状态管理:使用ConcurrentDictionary确保多用户并发操作时的状态一致性。
  2. 可靠事件通知:遍历每个订阅者单独处理,单个订阅者的异常不会中断其他用户的通知,同时记录异常避免“即发即弃”。
  3. UI线程安全:通过InvokeAsync确保UI更新在Blazor同步上下文执行,避免跨线程异常。
  4. 内存泄漏防护:使用实例事件替代静态事件,组件销毁时取消订阅,避免组件实例被长期引用。

内容的提问来源于stack exchange,提问作者Marvin Klein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 11:55:22