Display a timer in Blazor

11,491

Solution 1

rmunn got me on the right track, but my logic was a bit off as I did not fully understand what the timer class was actually doing. I had to account for not only the start time but the interval as well to get everything in sync, and the code now works as intended. Here is the updated code:

F#:

let private setTimer countDownSec intervalSec timeEvent =

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

    time <- countDownSec

    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 internal setTimerAsync countDownSec intervalSec timeEvent = async{
    setTimer countDownSec intervalSec timeEvent
    do! Async.Sleep (countDownSec * 1000)
}

type Timer (countDown) =

    member val CountDown : int = countDown with get,set

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

C#/Blazor:

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

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

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

    protected override async Task OnInitializedAsync()
    {
        time = startCount;

        // 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(startCount).SetTimeAsTask(func,1);

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

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

    }

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

            if(time <= 0)
            {
                time = startCount;
            }

            StateHasChanged();
        });
    }

}

Solution 2

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
)
Share:
11,491
user1206480
Author by

user1206480

Updated on June 04, 2022

Comments

  • user1206480
    user1206480 almost 2 years

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