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

Blazor WASM中StateHasChanged仅触发一次问题求助

问题描述

我开发了一个基于Blazor WASM的应用,多个独立组件需要在特定事件发生时互相通信,于是创建了消息服务(MessageService)实现组件间的消息发送与订阅,多数场景下只需通知其他组件更新后重新渲染。

当前场景:

  • 主页面承载多个组件
  • 模态对话框组件允许用户更新Adventure对象
  • 模态框完成更新后触发事件,订阅者调用StateHasChanged()更新UI

核心问题:StateHasChanged()仅首次调用有效,后续调用无法更新UI。事件每次都会触发,StateHasChanged()也每次都执行,但UI只在第一次更新。

已尝试过设置组件属性、用async替代void等方法,均无效。


消息服务代码

public interface IMessageService
{
    event Action<string> OnMessage;
    event Func<string, Task> OnMessageAsync;
    void SendMessage(string message);
    void ClearMessages();
}

public class MessageService : IMessageService
{
    public event Action<string> OnMessage;
    public event Func<string, Task> OnMessageAsync;

    public void SendMessage(string message)
    {
        OnMessage?.Invoke(message);
    }

    public void ClearMessages()
    {
        OnMessage?.Invoke(null);
    }
}

模态对话框代码

@code {
    [Parameter] public entities.Adventure Adventure { get; set; }
    private bool isBusy = false;
    private DateTime startDate;
    private DateTime endDate;
    private int numberOfDays;

    protected override async Task OnInitializedAsync()
    {
        // 记录原始起止日期,用于检测是否被修改
        startDate = Adventure.StartDate;
        endDate = Adventure.EndDate;
        numberOfDays = (endDate - startDate).Days;
    }

    private async Task OnSubmit()
    {
        try
        {
            isBusy = true;
            bool isDateChange = false;

            if (startDate != Adventure.StartDate || endDate != Adventure.EndDate)
            {
                if ((Adventure.EndDate - Adventure.StartDate).Days != numberOfDays)
                {
                    await DialogService.Alert("你的行程天数与原计划不一致,修改后会丢失部分行程项(任务、活动等)。请选择与原行程天数相同的日期范围,后续可单独删除天数。", "提示", new AlertOptions() { OkButtonText = "确定" });
                    isBusy = false;

                    return;
                }
                else
                {
                    isDateChange = true;
                }
            }

            var updatedAdventure = await AdventureClient.UpdateAdventureDetails(Adventure, isDateChange);

            MessageService.SendMessage("AdventureUpdated");

            DialogService.Close(updatedAdventure);
        }
        catch (Exception ex)
        {
            await DialogService.Alert($"保存行程时出错:{ex.Message}", "错误", new AlertOptions() { OkButtonText = "关闭" });
        }
        finally
        {
            isBusy = false;
        }
    }
}

需要重新渲染的宿主页面代码

@code {
    [Parameter] public Guid Id { get; set; }

    private entities.Adventure? adventure;
    
    protected override async Task OnInitializedAsync()
    {
        MessageService.OnMessage += MessageHandler;
        await GetAdventure();
    }

    async Task GetAdventure()
    {
        try
        {
            adventure = await AdventureClient.GetAdventure(Id);
        }
        catch (Exception ex)
        {
            await DialogService.Alert($"获取行程时出错:{ex.Message}", "错误", new AlertOptions() { OkButtonText = "关闭" });
        }
    }

    private void MessageHandler(string message)
    {
        if (message != null && message == "AdventureUpdated")
        {
            // 问题:仅第一次模态框更新时生效,后续更新时事件触发但UI不更新
            StateHasChanged();
        }
    }

    public void Dispose()
    {
        MessageService.OnMessage -= MessageHandler;
    }
}

解决方案

1. 核心修复:更新数据源而非仅触发渲染

当前MessageHandler只调用StateHasChanged(),但页面绑定的adventure仍是旧数据,Blazor变更检测会判定数据未变化,因此UI不更新。必须同步更新数据源:

修改宿主页面的MessageHandler为异步方法,重新获取最新数据:

private async Task MessageHandler(string message)
{
    if (message != null && message == "AdventureUpdated")
    {
        await GetAdventure();
        // 显式触发渲染,确保数据更新后UI同步
        await InvokeAsync(StateHasChanged);
    }
}

2. 适配异步事件处理

原消息服务的同步事件不适合异步逻辑,修改MessageService添加异步发送方法:

public async Task SendMessageAsync(string message)
{
    OnMessage?.Invoke(message);
    if (OnMessageAsync != null)
    {
        await OnMessageAsync.Invoke(message);
    }
}

同时更新模态对话框的调用代码:

await MessageService.SendMessageAsync("AdventureUpdated");

3. 修正事件订阅逻辑

在宿主页面切换为订阅异步事件,避免上下文丢失:

protected override async Task OnInitializedAsync()
{
    MessageService.OnMessageAsync += MessageHandler;
    await GetAdventure();
}

public void Dispose()
{
    MessageService.OnMessageAsync -= MessageHandler;
}

4. 优化方案:直接使用模态框返回数据

模态框关闭时已返回更新后的updatedAdventure,可直接用此数据更新页面,省去API调用:

// 宿主页面中打开模态框的逻辑
private async Task OpenEditModal()
{
    var dialogResult = await DialogService.Show<EditAdventureDialog>(adventure).Result;
    if (!dialogResult.Cancelled)
    {
        adventure = (entities.Adventure)dialogResult.Data;
        await InvokeAsync(StateHasChanged);
    }
}

内容的提问来源于stack exchange,提问作者Don Fitz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 12:32:44