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
)
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();
});
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With