Search code examples
xamarintimerxamarin.forms

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)?


Solution

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