Display a timer in Blazor

user1206480 picture user1206480 · Sep 29, 2019 · Viewed 8.6k times · Source

I am attempting to display a countdown timer in a server-side Blazor app. My code is in both F# and C#. The code somewhat works, but the timer never stops as intended, and the timer display sporadically does not render all of the numbers. This is my first attempt at a Blazor server-side app. I am not sure if the problem is an async issue, timer issue, or rendering issue.

Here's my code:

F#

let private setTimer countDown timeEvent =

    let timer = new Timer(float countDown * float 1000)
    let mutable time = 0

    time <- countDown

    timer.Elapsed.Add(fun arg ->
        time <- time - 1
        if time = 0
        then 
            timer.Stop()
            timer.Dispose()
        else
            ()
        timeEvent arg
    )

    timer.AutoReset <- true
    timer.Start()

let setTimerAsync countDown timeEvent = async{
    setTimer countDown timeEvent
    do! Async.Sleep (countDown * 1000)
}

type Timer (countDown) =

    member val CountDown : int = countDown with get,set

    member this.SetTimeAsTask (timeEvent) =
        setTimerAsync countDown timeEvent |> Async.StartAsTask

C# / Blazor

@page "/CountDown"
@using System.Timers
@using ClientTImer
@using Microsoft.FSharp.Core

<h3>Count Down</h3>
<p>
    Task: @task <br />
    Status: @status
</p>
<p>
    Timer: @time
</p>

@code {
    string task = "";
    string status = "";
    int time = 5;

    protected override async Task OnInitializedAsync()
    {

        // Initial task and status
        task = "First Task";
        status = "Status One";

        Action<System.Timers.ElapsedEventArgs> timeEvent =
            t =>
            {
                UpdateTime().Wait();
            };

        var func = FuncConvert.ToFSharpFunc(timeEvent);

        await new ClientTImer.Timer(time).SetTimeAsTask(func);

        // Update task and status
        task = "Second Task";
        status = "Status Two";
        await new ClientTImer.Timer(time).SetTimeAsTask(func);

        // Update task and status
        task = "Third Task";
        status = "Status Three";

    }

    public async Task UpdateTime()
    {
        await InvokeAsync(() =>
        {
            time--;
            StateHasChanged();
        });
    }

}

Answer

rmunn picture rmunn · Sep 29, 2019

Inside your F# Timer.Elapsed event handler, your final line is timeEvent (with no parameters), and I see from the rest of your code that timeEvent is an Action that's been converted to an F# function. Since you have not written any parameters after timeEvent, what that line is doing is specifying the value of timeEvent as the return value of the event handler, i.e. your event handler is returning a function. Or it would return a function if event handlers returned something other than void (or unit in F# terms). Since they don't I suspect that you've got a warning on that timeEvent line that says something about the value of timeEvent being inherently ignored.

Also, your timer.Elapsed.Add line in F# looks wrong to me. The Add method on events takes a parameter of type 'T -> unit, where 'T is whatever type of data the event gives you: in the case of the Elapsed event on timers, that would be an ElapsedEventArgs instance. What you should be passing to Add is a fun elapsedEventArgs -> .... And then you'd change your timeEvent line to actually pass it a parameter (those same elapsedEventArgs) so that it gets called and actually does something.

Also, whenever you're decrementing a number and comparing it to 0, I always like to do the comparison as <= rather than =, just on the off chance that I change my code later in a way that could cause the decrement to happen twice. If my comparison is = 0 and a double decrement takes the number from 1 to -1, the if x = 0 branch won't trigger. But if I was comparing to <= 0, then it will trigger even if I make a mistake elsewhere. So I'd suggest writing if time <= 0 rather than if time = 0.

In other words, I think your timer.Elapsed event handler needs to look like this:

timer.Elapsed.Add(fun evtArgs ->
    time <- time - 1
    if time <= 0
    then 
        timer.Stop()
        timer.Dispose()
    else
        ()
    timeEvent evtArgs
)