8

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();
    }
}
H H
  • 263,252
  • 30
  • 330
  • 514
  • No repro. Your code works as intended on my PC. – H H Apr 23 '20 at 21:53
  • But your text, tags and results are in conflict. On Blazor/Wasm this works. On Blazor/Server this should throw an exception. – H H Apr 23 '20 at 21:56
  • I am using server-side Blazor. Changing StateHasChanged(); to InvokeAsync(() => StateHasChanged()); makes this work. Can you point me in the direction to the documentation on this, I can't seem to find any reference to it? Why should this fail on Blazor/Server? Many thanks for your help with this. – Stuart Barnaby Apr 24 '20 at 10:23
  • It ought to throw an exception (on Server). And you tagged client-side (fixed). There is no configuration where it would just 'do nothing'. So you must be doing something else that was not posted. – H H Apr 24 '20 at 10:36
  • And why should it throw an exception ( on the server ). This is a baseless assertion which leads you to a false conclusion that the OP "must be doing something else that was not posted." – enet Apr 24 '20 at 13:22
  • @Stuart Barnaby, I've already explained what is the issue with your code. However, no exception should be raised when you execute your code as is... And if an exception is raised, it has got nothing to do with the issue in question. Note: The source of the issue is not really related to Blazor. It is in the domain of muti-threading environment, and single threading environment. Even the SynchronizationContext used in Blazor to mitigate the issue you faced is not exclusively a blazor thing – enet Apr 24 '20 at 13:37
  • @Stuart Barnaby: see these: https://learn.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1#invoke-component-methods-externally-to-update-state and this: https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext?view=netframework-4.8 And this: https://hamidmosalla.com/2018/06/24/what-is-synchronizationcontext/ – enet Apr 24 '20 at 13:37

2 Answers2

19

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...

enet
  • 41,195
  • 5
  • 76
  • 113
  • Thank you, yes I am using server-side Blazor. Changing StateHasChanged(); to InvokeAsync(() => StateHasChanged()); makes this work. – Stuart Barnaby Apr 24 '20 at 10:13
2

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.

H H
  • 263,252
  • 30
  • 330
  • 514