Blazor界面在调用StateHasChanged后无法更新

8

我有一个非常简单的例子来自标准的Blazor服务器端模板,它展示了即使在调用了StateHasChanged()函数后计时器函数也不会更新UI。

日志输出显示计时器已被触发,如果我等待几秒钟并点击IncrementCount按钮,则计数值会跳转到计时器增加计数器次数的数字。

非常好奇...任何帮助将不胜感激。

敬礼,Stuart

@page "/counter"
@using System.Timers;

@using Microsoft.Extensions.Logging
@inject ILogger<Counter> Logger

<h1>Counter</h1>

<p>Current count: @(currentCount.ToString())</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }

    public System.Timers.Timer timer;
    protected override async Task OnInitializedAsync()
    {
        timer = new Timer(1000);
        timer.Elapsed += this.OnTimedEvent;
        timer.AutoReset = true;
        timer.Enabled = true;
        timer.Start();
    }

    public void OnTimedEvent(Object source, ElapsedEventArgs e)
    {
        Logger.LogInformation("Timer triggered");
        IncrementCount();
        StateHasChanged();
    }
}

没有问题。你的代码在我的电脑上按预期工作。 - H H
但是你的文本、标签和结果存在冲突。在 Blazor/Wasm 上可以工作,在 Blazor/Server 上应该会抛出异常。 - H H
我正在使用服务端 Blazor。将 StateHasChanged(); 更改为 InvokeAsync(() => StateHasChanged()); 可以使其工作。您能否指引我到相关文档的方向?我找不到任何参考资料。为什么Blazor / Server上会失败?非常感谢您的帮助。 - Stuart Barnaby
@Stuart Barnaby,我已经解释了你代码的问题所在。然而,当你按原样执行代码时不应该引发任何异常...如果出现异常,那与所讨论的问题无关。请注意:问题的根源实际上与Blazor无关。它属于多线程环境和单线程环境的领域。即使在Blazor中使用的SynchronizationContext来缓解你面临的问题,也不是专门针对Blazor的东西。 - enet
@Stuart Barnaby:请查看以下链接:https://learn.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1#invoke-component-methods-externally-to-update-state 和 https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext?view=netframework-4.8,以及 https://hamidmosalla.com/2018/06/24/what-is-synchronizationcontext/。 - enet
显示剩余2条评论
2个回答

19

你正在运行Blazor Server应用程序,对吗?在这种情况下,你应该从ComponentBase的InvokeAsync方法中调用StateHasChanged方法,如下所示:

InvokeAsync(() => StateHasChanged());
我猜这是因为计时器在不同于UI线程的线程上执行,需要对涉及的线程进行同步。在 Blazor WebAssembly 上,由于所有代码都在同一 UI 线程上执行,因此不太可能发生这种行为。
希望这可以帮到你...

谢谢,是的我正在使用服务端 Blazor。将 StateHasChanged(); 更改为 InvokeAsync(() => StateHasChanged()); 可以使它工作。 - Stuart Barnaby

2
计时器事件可能在后台线程上执行。当您的代码未在正常的生命周期事件中运行时,请使用。
 InvokeAsync(StateHasChanged);  // no () => ()  required. 

此外,Timer类是可释放的。因此,请添加以下内容:
@implements IDisposable

...

@code
{
   ...

   public void Dispose()
   {
      timer?.Dispose();
   }
}

解释:

计时器事件处理程序不应直接调用StatehasChanged()。计时器事件在运行在默认(null)同步上下文中的池线程上处理。

当调用 StatehasChanged() 时,它会启动一个渲染操作。渲染操作将调用Dispatcher.AssertAccess();

AssertAccess() 的代码为

 if (!CheckAccess()) throw new InvalidOperationException(...);

WebAssembly使用重载

 public override bool CheckAccess() => true;

在WebAssembly中,错误可能不会被注意到,但它仍然是一个错误。当WebAssembly在未来引入线程时,此代码可能开始失败。

而对于服务器端,我们有

 public override bool CheckAccess() => SynchronizationContext.Current == _context;

在一个服务器应用程序中,操作员应该已经收到了异常。也许记录器与此有关,我没有进行检查。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接