Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor UI not updating on StateHasChanged call

I have an extremely simple example from the standard Blazor server-side template that shows that a timer function will not update the UI even after a StateHasChanged(); call has been made.

The log output shows the timmer being triggered and if I wait a few seconds and click the IncrementCount button the count value jumps to the number of times the counter has been incremented by the timer.

Very curious ... any help would be greatly appreciated

Kind regards, 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();
    }
}
like image 532
Stuart Barnaby Avatar asked Apr 23 '20 21:04

Stuart Barnaby


2 Answers

You are running Blazor Server App, right ? In that case you should call the StateHasChanged method from within the ComponentBase's InvokeAsync method as follows:

InvokeAsync(() => StateHasChanged());

I guess this occurs because the timer is executed on a different thread than the UI thread, which requires synchronization of the threads involved. On Blazor WebAssembly this behavior is not likely to happen as all code is executed on the same UI thread.

Hope this helps...

like image 170
enet Avatar answered Oct 02 '22 05:10

enet


The Timer event may execute on a background thread. And when your code is not running in a normal Lifecycle event, use

 InvokeAsync(StateHasChanged);  // no () => ()  required. 

In addition, the Timer class is IDisposable. So add:

@implements IDisposable

...

@code
{
   ...

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

Explanation:

A timer eventhandler should not call StatehasChanged() directly. A timer event is handled on a pool thread that runs on the default (null) Sync context.

When you call StatehasChanged() it will start a Render. The render operation will call Dispatcher.AssertAccess();

The code for AssertAccess() is

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

WebAssembly uses the overload

 public override bool CheckAccess() => true;

So in WebAssembly the error goes unnoticed, but it still is an error. This code might start to fail when WebAssembly gets Threads in the future.

And for Server-side we have

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

In a Server app the OP should have gotten an exception. Maybe the Logger has something to do with that not happening, I didn't check.

like image 24
Henk Holterman Avatar answered Oct 02 '22 06:10

Henk Holterman