Dennis Schröer
Dennis Schröer

Reputation: 2412

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

Upvotes: 6

Views: 6719

Answers (3)

BrewMate
BrewMate

Reputation: 1020

Yes you can with Device.StartTimer() as long as you return true to have the function repeat. I typically handle this through a Boolean variable that I can control in my ViewModel. Something like below:

bool shouldRun = true;
public async void RefreshLocalData()
{
    // some async code to load the messages

    if (_selectedChat.countNewMessages > 0)
    {
        Device.StartTimer(TimeSpan.FromSeconds(5), async() => 
        {
            await SendReadConfirmationAsync()
            return shouldRun;
        });

    }
}

public async Task SendReadConfirmationAsync()
{
    //Do some stuff
    if(we want to stop call)
        shouldRun = false;
}

Upvotes: 2

Dennis Schröer
Dennis Schröer

Reputation: 2412

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

Upvotes: 11

Richard Pike
Richard Pike

Reputation: 679

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

Upvotes: 3

Related Questions