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