C# PeriodicTimer

Karen Payne - May 26 - - Dev Community

Introduction

A periodic timer enables waiting asynchronously for timer ticks. The main goal of this timer is to be used in a loop and to support async handlers.

Learn how to use a PeriodicTimer in a code sample that fires off a PeriodicTimer every 15 seconds, retrieves a random record from a SQLite Northwind database using Dapper (EF Core would be overkill here).

PeriodicTimer was first release with NET6.

WinForms Sample project

ASP.NET Core Sample project

Task

In a Windows Form project, click a button to start the timer using the following class.

Windows form for code sample

Main timer code

Events

  • public static event OnShowTime OnShowTimeHandler sends the current time to the form. The form subscribes to this event in the form constructor
  • public static event OnShowContact OnShowContactHandler sends a random record reads from the SQLite contacts table using Dapper to the form which subscribes to the event in the form constructor.

Local method

  • IsQuarterMinute returns true is current time/seconds is a quarter of a minute.

In the code below a timer is created which triggers every second, each iteration of the while statement the time is sent to the form which displays the current time in a label.

If the current time is a quarter of a minute, read a random record and send the record to the form which is displayed in a label. This is followed by faking sending an email.

If there are any runtime exceptions they are written to a log file using SeriLog. In source project see folder SampleLogFile for samples of the error log file.

public class TimerOperations
{
    public delegate void OnShowTime(string sender);
    public static event OnShowTime OnShowTimeHandler;

    public delegate void OnShowContact(Contacts sender);
    public static event OnShowContact OnShowContactHandler;

    /// <summary>
    /// Execute a data read operation every quarter minute to retrieve a random contact.
    /// </summary>
    public static async Task Execute(CancellationToken token)
    {
        static bool IsQuarterMinute()
        {
            var seconds = Now.Second;
            return seconds is 0 or 15 or 30 or 45;
        }

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(token) && !token.IsCancellationRequested)
            {

                OnShowTimeHandler?.Invoke($"Time {Now:hh:mm:ss}");

                if (IsQuarterMinute())
                {
                    Contacts contacts = DapperOperations.Contact();
                    OnShowContactHandler?.Invoke(contacts);
                    EmailOperations.SendEmail(contacts);
                }
            }
        }
        catch (OperationCanceledException) { }
        catch (Exception exception)
        {
            Log.Error(exception,"");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Form code

Start the timer

The following starts the timer utilizing a cancellation token for providing a way to terminate the timer.

private CancellationTokenSource cts = new();
.
.
.
private async void PeriodicTimerForm_Shown(object? sender, EventArgs e)
{
    await Start();
}
private async Task Start()
{
    StartButton.Enabled = false;
    ContactNameLabel.Text = "";

    if (cts.IsCancellationRequested)
    {
        cts.Dispose();
        cts = new CancellationTokenSource();
    }

    await TimerOperations.Execute(cts.Token);
}
Enter fullscreen mode Exit fullscreen mode

Note
In the code above a check is needed to determine if the timer is currently running. If the timer is running (which it always will be since the initial firing off the timer is in form shown event), dispose of the cancellation token, create a new instance and start the processing again.

Stopping the timer

private void StopButton_Click(object sender, EventArgs e)
{
    cts.Cancel();
    StartButton.Enabled = true;
}
Enter fullscreen mode Exit fullscreen mode

Longer delay

In the above example the timer fired off every second which in some cases may be overkill. Let's change firing off every second to every 60 seconds.

This is done via TimeSpan.FromMinutes(1) and Task.Delay.

public static async Task ExecuteWait(CancellationToken token)
{

    try
    {
        // take milliseconds into account to improve start-time accuracy
        var delay = (60 - UtcNow.Second) * 1000; 
        await Task.Delay(delay, token);

        using PeriodicTimer timer = new(TimeSpan.FromMinutes(1));

        while (await timer.WaitForNextTickAsync(token))
        {
            Contacts contacts = await DapperOperations.ContactAsync();
            OnShowContactHandler?.Invoke(contacts);
            EmailOperations.SendEmail(contacts);
        }
    }
    catch (OperationCanceledException) { }
    catch (Exception exception)
    {
        Log.Error(exception, "");
    }
}
Enter fullscreen mode Exit fullscreen mode

Important

In each of the code samples above using PeriodicTimer timer = new(...). Using ensure memory is properly disposed off when the application closes.

Summary

Although code samples are done in a Windows Form the timer can be used in other type of project e.g. ASP.NET Core, Console etc.

Sample output from the web code sample.

Console window for asp.net core code sample

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .