Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

I want to run a method every minute that's started from my OnAppearing(). Do I need to run this as a task?

Here's the code that I have come up with. It seems to work but I am concerned that it may not be good way to do what I want to do. What I need is to run a method every minute as soon as the OnAppearing happens and have it stop with the OnDisappearing();

protected async override void OnAppearing()
{
   base.OnAppearing();
   BindingContext = vm;

   cts = new CancellationTokenSource();
   if (Settings.mode == MO.Practice)
   {
      if (!App.stopWatch.IsRunning) { App.stopWatch.Start(); }
            Device.StartTimer(new TimeSpan(0, 0, 5), () =>
      {
          if (App.stopWatch.IsRunning && App.stopWatch.Elapsed.Seconds >= 60)
          {
             // Here's the method I want to run. After it's finished
             // I call BeginInvoke .. to update info on the screen
             if (App.DB.ReducePoints() == true)
                Device.BeginInvokeOnMainThread(() =>
                {
                   vm.PifInfo = GetPifInfo();
                });
                App.stopWatch.Restart();
             }
             return true;
            });
        }
        await GetCards(cts.Token);
    }
}

protected override void OnDisappearing()
{
    Unsubscribe();
    cts.Cancel();
    if (App.stopWatch.IsRunning) { App.stopWatch.Stop(); }
    base.OnDisappearing();
}

Not part of the question but I would welcome any comments on the code also.

like image 734
Alan2 Avatar asked Dec 10 '22 04:12

Alan2


2 Answers

You can do this simpler by returning the correct value from Device.StartTimer, to repeat true, to not repeat false and not use a StopWatch. (source states that While the callback returns true, the timer will keep recurring. And as you see from the source, the method doesn't need a Func<Task<bool>> it only needs a Func<bool> callback so there is no need to use a Task.)

in the class

volatile bool run;

in OnAppearing

run = true;
Device.StartTimer(new TimeSpan(0, 1, 0), () => {
if (run) { /*do what you want;*/ return true; }
else { return false; }
});

in OnDisappearing

run = false;

EDIT - as requested by OP

Here is the code. I am leaving my original answer to help anyone else who needs this.

volatile bool run;

protected async override void OnAppearing()
{
   base.OnAppearing();
   BindingContext = vm;    
   cts = new CancellationTokenSource();
   if (Settings.mode == MO.Practice)
   {
        run = true;
        Device.StartTimer(new TimeSpan(0, 1, 0), () => 
        {
            if (run) 
            { 
                 if (App.DB.ReducePoints() == true)
                    Device.BeginInvokeOnMainThread(() =>
                    {
                       vm.PifInfo = GetPifInfo();
                    });
                 return true; 
            }
            else { return false; }
        });
        await GetCards(cts.Token);
    }
}

protected override void OnDisappearing()
{
    run = false;
    Unsubscribe();
    cts.Cancel();
    base.OnDisappearing();
}
like image 97
123 Avatar answered May 13 '23 17:05

123


You can refactor the code to make proper use of the Timer in combination with a CancellationToken.

Also note the use of the async event handler to avoid the fire and forget call to async void OnAppearing that won't allow thrown exceptions to be caught and can cause crashes.

CancellationTokenSource source;

protected override void OnAppearing() {
    base.OnAppearing();
    BindingContext = vm;
    timerStarted += onTimerStarted;
    timerStarted(this, EventArgs.Empty);
}

private event EventHandler timerStarted = delegate { };

private async void onTimerStarted(object sender, EventArgs args) {
    timerStarted -= onTimerStarted;
    cts = new CancellationTokenSource();
    if (Settings.mode == MO.Practice) {
        source = new CancellationTokenSource();
        StartTimer(source.Token);
        await GetCards(cts.Token);
    }
}

private void StartTimer(CancellationToken token) {        
    var interval = TimeSpan.FromMinutes(1);
    Func<bool> callback = () => {
        //check if to stop timer
        if(token.IsCancellationRequested) return false;
        //Code to be repeated
        checkPoints();            
        //While the callback returns true, the timer will keep recurring.
        return true;
    };
    //repeat this function every minute
    Device.StartTimer(interval, callback);
}

private void checkPoints() {
    // Here's the method I want to run. After it's finished
    // I call BeginInvoke .. to update info on the screen
    if (App.DB.ReducePoints() == true) {
        Device.BeginInvokeOnMainThread(() => {
            vm.PifInfo = GetPifInfo();
        });
    }
}

protected override void OnDisappearing() {        
    source.Cancel();//Timer will short-circuit on next interval
    Unsubscribe();
    cts.Cancel();        
    base.OnDisappearing();
}

The cancellation token will be used to force the timer to return false and stop recurring when the token is cancelled in OnDisappearing().

If the function to be repeated needs to be asynchronous add another async event handler to manage that.

like image 36
Nkosi Avatar answered May 13 '23 18:05

Nkosi