timerblazorblazor-server-side

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();
        }
    }
}

Solution

  • 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;
            }
        }
    }