2

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();
        }
    }
}
Lucian Bumb
  • 2,821
  • 5
  • 26
  • 39
  • What version of Blazor do you use? I tried to mock this up with Task.Delay() but it won't even start (6.0). I think i did see it work in earlier versions. – H H Jun 22 '22 at 09:00
  • @HenkHolterman project is `net6.0` – Lucian Bumb Jun 22 '22 at 09:39
  • I'll have to try again later then. Maybe Task.Delay() has changed. – H H Jun 22 '22 at 09:54
  • Some criticism: You keep OnInitialized running , blocking parameters. And it just looks wrong. The Dispose() is overdone, you don't need SuppressFinalize or bool disposing. – H H Jun 22 '22 at 09:57
  • I would go for Systems.Threading.Timer, with an async void event. – H H Jun 22 '22 at 09:58
  • See this anser: https://stackoverflow.com/a/63062755/60761 – H H Jun 22 '22 at 10:00

1 Answers1

2

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;
        }
    }
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Would be a better answer with just one option. And without the disposing/SuppressFinalize stuff. – H H Jun 22 '22 at 18:34
  • I tend to agree. I did simplify `Dispose`. I was trying to demonstrate the two options. I'm not sure I like the `RunTimer` option anyway. I haven't dug into the code, but I suspect PeriodicTimer is just another wrapper for `System.Threading.Timer`. – MrC aka Shaun Curtis Jun 22 '22 at 20:55
  • The component where I used the sample code was not the application Index(first page), actually, all my components with routing are named index. Indeed, I could use a simple timer to refresh my component ViewModel, but I found the PeriodicTIme as an easy read/write implementation. I end up using the while loop in the `OnAfterRenderAsync(bool firstRender)` when is firstRender. Till now I saw no problem in the production environment. – Lucian Bumb Jun 23 '22 at 05:49