Let's build a simple countdown timer with dotnet Blazor

Let's build a simple countdown timer with dotnet Blazor

Last weekend I was free and bored, so I decided to refresh my Blazor skills and build something small and funny. I decided to make and made a simple stupid game "find dissimilar emoji".

overall-screenshot.png

Part of the game was a countdown on top, which is a separate Blazor component I made.

In this article, I will demonstrate to you how to create an easy Blazor countdown component. A full code of component available here.

Intro

A component is a self-contained portion of the user interface (UI) with processing logic to enable dynamic behavior.

Let's deep dive into this component to understand how it works and how you could make the same countdown for your Blazor app.

The countdown component consists of several parts:

  • HTML Markup
  • C# code
  • CSS styles

Which could be isolated into separate files

  • Countdown.razor
  • Countdown.razor.cs
  • Countdown.razor.css

Or it could be combined into a single Countdown.razor file for simplicity. But for me, isolation is a better practice, especially if I plan to expand the component further.

When we isolate code into separate files with proper naming Visual Studio will combine them under one nifty tree

tree.png

HTML Markup

For markup, we have a div container and a h1 item inside of it to make our countdown big enough. We will make our container flex with CSS later to easily center our timer.

Time will be our property to hold the string value of the timer.

<div class = "clock">
    <h1>@Time</h1>
</div>

C# code

First, we need to declare the timer, field to store seconds left, and Time property for the UI part. For the timer itself dotnet provide us with several options, we will use System.Timers.Timer as the simplest one.

    private System.Timers.Timer _timer = null!;
    private int _secondsToRun = 0;

    protected string Time { get; set; } = "00:00";

To properly use it we need a Start and Stop methods to launch the timer and some kind of callback to handle time out. Note that we should manually call here StateHasChanged to notify Blazor that the component needs to redraw. This is because we call Start programmatically by code.

    [Parameter]
    public EventCallback TimerOut { get; set; }

    public void Start(int secondsToRun)
    {
        _secondsToRun = secondsToRun;

        if (_secondsToRun > 0)
        {
            Time = TimeSpan.FromSeconds(_secondsToRun).ToString(@"mm\:ss");
            StateHasChanged();
            _timer.Start();
        }
    }

    public void Stop()
    {
        _timer.Stop();
    }

The TimerOut event has an additional attribute - Parameter to make it accessible from outside of the component.

We need to set up the timer, we could make it in the OnInitialized event override

    protected override void OnInitialized()
    {
        _timer = new System.Timers.Timer(1000);
        _timer.Elapsed += OnTimedEvent;
        _timer.AutoReset = true;
    }

This timer will exist in the scope of a single user (because it's owned by a component, which is owned by a page, which exists in the scope of a single-user connection). It will tick every second, firing OnTimedEvent until stopped.

The event should decrease the number of seconds left, update the Time value and check if the timer is out to stop it and fire TimerOut event.

    private async void OnTimedEvent(object? sender, ElapsedEventArgs e)
    {
        _secondsToRun--;

        await InvokeAsync(() =>
        {
            Time = TimeSpan.FromSeconds(_secondsToRun).ToString(@"mm\:ss");
            StateHasChanged();
        });

        if (_secondsToRun <= 0)
        {
            _timer.Stop();
            await TimerOut.InvokeAsync();
        }
    }

Because the callback is invoked outside of Blazor's synchronization context, the component must wrap the logic of OnTimedEvent in ComponentBase.InvokeAsync to move it onto the renderer's (UI) synchronization context. StateHasChanged can only be called from the renderer's synchronization context and throws an exception otherwise. We do not care in what context the timer stops, so we could not wrap it.

And finally System.Timers.Timer need to be properly disposed with the component. For that, we implement IDisposable interface in our class.

public partial class Timer : ComponentBase, IDisposable
{
    ...

    public void Dispose()
    {
        _timer.Dispose();
    }
}

CSS styles

For style, we go for a simple flexbox with centered content. No additional styles are required right now. Timer.razor.css file will be bundled into {ASSEMBLY NAME}.styles.css file and included in your application.

.clock {
    color: var(--color-tone-1);
    font-family: sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-grow: 1;
    overflow: hidden;
}

Usage

To use the component we simply add it to our page

<Countdown @ref="timer" TimerOut="TimerOutCallback"/>

With @ref attribute we obtain a reference to an HTML element, which we could use to call our Start and Stop methods. For example, we could start the timer after the page is rendered and display a message after it is timed out.

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            timer.Start(10);
        }
    }

    protected Shared.Countdown timer;

    private void TimerOutCallback()
    {
        ...
    }

Conclusion

That concludes our countdown component. There is not much left to be done to improve codebase of it.

The visuals of the countdown might be improved but here is a catch: Blazor is not very suitable to work with CSS animation as JS does.

It is definitely possible but requires either a lot of "hacks" or JS functions to call from Blazor. Maybe one day I will deep dive into it.