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();
}
}
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...
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With