Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cancel a Timer before it's finished

I am working on a Chat app. After the messages of a chat are loaded and the messages were visible for 5 seconds, I want to send a read confirmation to the server. This is what I've come up with so far:

public async void RefreshLocalData()
{
    // some async code to load the messages

    if (_selectedChat.countNewMessages > 0)
    {
        Device.StartTimer(TimeSpan.FromSeconds(5), SendReadConfirmation);
    }
}

When RefreshLocalData() is called, I know that either another chat was selected by the user or new messages came in for the current chat. So when RefreshLocalData() is called, I have to cancel the current timer to start a new one.

Another situation where I have to cancel the timer is when I navigate to another Page. This is no problem, because the whole ViewModel is disposed when this happens.

With the code above, if RefreshLocalData() is called again but the stated TimeSpan of 5 seconds is not over yet, the method is still executing.

Is there a way to cancel the timer (if RefreshLocalData() is called again)?

like image 741
Dennis Schröer Avatar asked Jan 11 '17 09:01

Dennis Schröer


2 Answers

I have found this answer in the Xamarin forum: https://forums.xamarin.com/discussion/comment/149877/#Comment_149877

I have changed it a little bit to meet my needs and this solution is working:

public class StoppableTimer
{
    private readonly TimeSpan timespan;
    private readonly Action callback;

    private CancellationTokenSource cancellation;

    public StoppableTimer(TimeSpan timespan, Action callback)
    {
        this.timespan = timespan;
        this.callback = callback;
        this.cancellation = new CancellationTokenSource();
    }

    public void Start()
    {
        CancellationTokenSource cts = this.cancellation; // safe copy
        Device.StartTimer(this.timespan,
            () => {
                if (cts.IsCancellationRequested) return false;
                this.callback.Invoke();
                return false; // or true for periodic behavior
        });
    }

    public void Stop()
    {
        Interlocked.Exchange(ref this.cancellation, new CancellationTokenSource()).Cancel();
    }

    public void Dispose()
    {

    }
}

And this is how I use it in the RefreshLocalData() method:

private StoppableTimer stoppableTimer;

public async void RefreshLocalData()
{
    if (stoppableTimer != null)
    {
        stoppableTimer.Stop();
    }

    // some async code to load the messages

    if (_selectedChat.countNewMessages > 0)
    {
        if (stoppableTimer == null)
        {
            stoppableTimer = new StoppableTimer(TimeSpan.FromSeconds(5), SendReadConfirmation);
            stoppableTimer.Start();
        }
        else
        {
            stoppableTimer.Start();
        }
    }
}
like image 152
Dennis Schröer Avatar answered Oct 06 '22 00:10

Dennis Schröer


You can try using this class I found, it covers some of the limits to the DeviceTimer:

public class MySystemDeviceTimer
{
    private readonly TimeSpan timespan;
    private readonly Action callback;

    private CancellationTokenSource cancellation;

    public bool running { get; private set; }

    public MySystemDeviceTimer(TimeSpan timespan, Action callback)
    {
        this.timespan = timespan;
        this.callback = callback;
        this.cancellation = new CancellationTokenSource();
    }

    public void Start()
    {
        running = true;
        start(true);
    }

    private void start(bool continuous)
    {
        CancellationTokenSource cts = this.cancellation;    // safe copy
        Device.StartTimer(this.timespan,
            () =>
            {
                if (cts.IsCancellationRequested)
                {
                    running = false;
                    return false;
                }
                this.callback.Invoke();
                return continuous;
            });
    }

    public void FireOnce()
    {
        running = true;
        start(false);
        running = false;
    }

    public void Stop()
    {
        Interlocked.Exchange(ref this.cancellation, new CancellationTokenSource()).Cancel();
    }
}

Then for your purpose:

MySystemDeviceTimer timer;

    if (timer == null)
    {
       timer = new MySystemDeviceTimer(TimeSpan.FromSeconds(5), SendReadConfirmation);
       timer.FireOnce();
    }
    else if (timer.running)
       timer.Stop();
like image 43
Richard Pike Avatar answered Oct 06 '22 01:10

Richard Pike