Lucian Bumb
Lucian Bumb

Reputation: 2881

What is the best way to use PeriodicTimer for refresh component in blazor server

I have a component that needs to display fresh data every 1 minute.

I tried the new timer (Periodic Timer) and it works fine.

Now, my questions are,

Where is the best place to put the while loop?

Is something else required for a proper dispose of the PeriodicTimer?

public partial class Index:IDisposable
{
    private readonly PeriodicTimer _periodicTimer = new(TimeSpan.FromMinutes(1));

    protected override async Task OnInitializedAsync()
    {
        await Service.Init();
        while (await _periodicTimer.WaitForNextTickAsync())
        {
            await Service.GetViewModels();
            await InvokeAsync(StateHasChanged);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
           _periodicTimer.Dispose();
        }
    }
}

Upvotes: 3

Views: 4446

Answers (2)

Pawel
Pawel

Reputation: 1297

Based on the answer of @mrc-aka-shaun-curtis this is what worked for me best. Previously I was hounted with System.InvalidOperationException: Operation is not valid due to the current state of the object. at System.Threading.PeriodicTimer.State.WaitForNextTickAsync(PeriodicTimer owner, CancellationToken cancellationToken):

@implements IDisposable

// Markup

private PeriodicTimer? timer;
private CancellationTokenSource? cts;

protected override async Task OnParametersSetAsync()
{
    cts = new CancellationTokenSource();
    timer = new PeriodicTimer(TimeSpan.FromSeconds(1));

    // Start the timer task, do not await
    RunTimerAsync(cts.Token);
}

private async Task RunTimerAsync(CancellationToken cancellationToken)
{
    try
    {
        while (timer is not null && await timer.WaitForNextTickAsync(cancellationToken))
        {
            // Do stuff here
            await InvokeAsync(StateHasChanged);
        }
    }
    catch (OperationCanceledException)
    {
        // Timer cancelled, exit the loop
    }
    finally
    {
        timer?.Dispose();
    }
}

public void Dispose()
{
    cts?.Cancel();
    cts?.Dispose();
}

Upvotes: 1

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30001

If I put the code in Index, i.e. the statup page, it never completes loading.

If I put the code in another page, it looks like it running fine but it isn't.

The problem is that OnInitializedAsync never completes. It's always awaiting in the while loop. Therefore the rest of the initial component lifecycle doesn't complete either.

As a startup page, it gets locked in the initial server load.

To solve the problem you need to "fire and forget" the timer loop.

Here's a demo component that works, and how to use the "older" timer with an event handler (which I personally would stick with).

@page "/Test"
@implements IDisposable

<h1>Hello</h1>
<div class="m-2">
    Message: @message
</div>
<div class="m-2">
   Timer Message: @timerMessage
</div>
<div class="m-2">
    @state
</div>
@code {
    private System.Timers.Timer timer = new System.Timers.Timer(2000);
    private readonly PeriodicTimer _periodicTimer = new(TimeSpan.FromSeconds(5));
    private string message = "Starting";
    private string timerMessage = "Starting";
    private string state = "On Init Running";

    // This is broken code 
    //protected override async Task OnInitializedAsync()
    //{
    //    while (await _periodicTimer.WaitForNextTickAsync())
    //    {
    //        message = $"Updated at {DateTime.Now.ToLongTimeString()}";
    //        await InvokeAsync(StateHasChanged);
    //    }
    //}

    protected override void OnInitialized()
    {
        RunTimer();
        state = "On Init complete";

        // Uses System.Timers.Timer
        timer.Elapsed += TimeElapsed;
        timer.AutoReset = true;
        timer.Enabled = true;

    }

    private async void TimeElapsed(object? sender, System.Timers.ElapsedEventArgs e)
    {
        // emulate an async data get
        await Task.Delay(100);
        timerMessage = $"Updated at {DateTime.Now.ToLongTimeString()}";
        await InvokeAsync(StateHasChanged);
    } 

    protected async void RunTimer()
    {
        while (await _periodicTimer.WaitForNextTickAsync())
        {
            // emulate an async data get
            await Task.Delay(100);
            message = $"Updated at {DateTime.Now.ToLongTimeString()}";
            await InvokeAsync(StateHasChanged);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _periodicTimer.Dispose();
            timer.Elapsed -= TimeElapsed;
        }
    }
}

Upvotes: 3

Related Questions