15

I am setting up a timer in a Blazor server-side page. The goal is to call an API every x seconds and based on the return value, update the UI.

I got this code:

private string Time { get; set; }

protected override void OnInitialized()
{
    var timer = new System.Threading.Timer((_) =>
    {
        Time = DateTime.Now.ToString();
        InvokeAsync(() =>
        {
            StateHasChanged();
        });
    }, null, 0, 1000);
    base.OnInitialized();
}

This works beautifully. The UI was updated every second with the new time value. However, I can't figure out how to call an async task to get the value. I would like to replace the line:

Time = DateTime.Now.ToString();

with a line that calls the following function:

private async Task<string> GetValue()
{
    var result = await _api.GetAsync<StringDto>("/api/GetValue");
    return result.Text;
}

I've tried this line:

Time = GetValue().Result;

But I received this following error:

The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.

What do I need to do to call the async method?

Thanks a bunch!

Franky
  • 1,262
  • 2
  • 16
  • 31

3 Answers3

19

You probably don't want to Invoke() the GetValue(), that would be rather pointless. You can implement the timer like this:

System.Threading.Timer timer;
protected override void OnInitialized()
{
    timer = new System.Threading.Timer(async _ =>  // async void
    {
        Time = await GetValue();
        // we need StateHasChanged() because this is an async void handler
        // we need to Invoke it because we could be on the wrong Thread          
        await InvokeAsync(StateHasChanged);
    }, null, 0, 1000);
}

I used a field to store the Timer because you should dispose it, add this to the Razor section:

@implements IDisposable

and this to the code:

public void Dispose()
{
    timer?.Dispose();
}
H H
  • 263,252
  • 30
  • 330
  • 514
  • I like the general solution in your answer but did you intend to omit the call to base.OnInitialized()? – camelCase Jul 25 '20 at 13:57
  • 1
    Yes, it is not needed in a normal page. You can put it in out of 'general principle' , that's a matter of taste. Look at the WeatherForecast page for a reference. – H H Jul 25 '20 at 14:04
  • 2
    Yes, I can confirm that, if you check https://source.dot.net/#Microsoft.AspNetCore.Components/ComponentBase.cs,64e82abf33cdffa6 you'll see there are no statements in `OnInitialized`. – Felipe Costa Gualberto Jul 30 '21 at 19:39
14

In .NET 6 You can use PeriodicTimer class as follows

private string Time { get; set; }

     protected override async void OnAfterRender(bool firstRender)
{
    if (firstRender)
    {
        using var periodicTimer = new PeriodicTimer(TimeSpan.FromMinutes(10));
        while (await periodicTimer.WaitForNextTickAsync())
        {
          Time=   await GetValue();

            await InvokeAsync(StateHasChanged);
        }
    }
}
     

Running it without tieing up a LifeCycle method forever:

@implements IDisposable
PeriodicTimer periodicTimer = new (TimeSpan.FromMinutes(10));

protected override void OnInitialized()
{
    RunTimer();  // fire-and-forget
}

async void RunTimer()
{
    while (await periodicTimer.WaitForNextTickAsync()) { .... }    
}

public void Dispose()
{
   periodicTimer?.Dispose();
}
hamishmcn
  • 7,843
  • 10
  • 41
  • 46
  • Yes, interesting. Looks like a better fit here. But it is IDisposable, you still need to Dispose() it. So no `var timer` but `_timer`. – H H Apr 20 '22 at 08:01
  • The periodicTimer is created to be async in nature, according to David Fowler, "API only makes sense for timers that fire repeatedly", and the timer stops during the execution of the repeated task. Also you can wrap it around using statement to make sure correct usage of IDisposable. You can also use Cancelation Token to stop it. – Ibrahim Mohammed Apr 20 '22 at 11:25
  • 1
    I wouldn't block OnInitialized() like this. It might prevent the (first) OnParametersSet(). This might be a case for an `async void` method to house the `using {}`. – H H Apr 21 '22 at 06:00
  • You right I have modified the code to reflect what you just pointed out. Thanks. – Ibrahim Mohammed Apr 21 '22 at 08:19
  • If the essence of the code is to refresh the Blazor UI then the timer function must be within OnAfterRender(), hence we have – Ibrahim Mohammed Apr 22 '22 at 05:24
  • Still not ideal - I don't know how much you block by not returning from OnInitialized or OnAfterRender { Async} . I would write a separate `async void` method and launch that from one of the lifecycle methods (wouldn't matter much which one). – H H Apr 22 '22 at 05:40
  • 1
    I'll edit that in here, roll back if you don't like it. – H H Apr 22 '22 at 05:43
6

Try this code:

  private string Time { get; set; }

protected override void OnInitialized()
{
    base.OnInitialized();
    var timer = new System.Threading.Timer((_) =>
    {

        InvokeAsync( async ()  =>
        {
            Time = await GetValue();
            StateHasChanged();
        });
    }, null, 0, 1000);

}

Your GetValue method should be:

private async Task<string> GetValue()
{
    return await _api.GetAsync<StringDto>("/api/GetValue");
  
}
H H
  • 263,252
  • 30
  • 330
  • 514
enet
  • 41,195
  • 5
  • 76
  • 113