Search code examples
c#.nettaskcancellation

How to keep cancelling the task until a condition is met (TaskCanceledException)


I want to call a method after some delay when an event is raised, but any subsequent events should "restart" this delay. Quick example to illustrate, the view should be updated when scrollbar position changes, but only 1 second after the user has finished scrolling.

Now I can see many ways of implementing that, but the most intuitive would be to use Task.Delay + ContinueWith + cancellation token. However, I am experiencing some issues, more precisely subsequent calls to my function cause the TaskCanceledException exception and I started to wonder how I could get rid of that. Here is my code:

private CancellationTokenSource? _cts;

private async void Update()
{
    _cts?.Cancel();
    _cts = new();
    await Task.Delay(TimeSpan.FromSeconds(1), _cts.Token)
       .ContinueWith(o => Debug.WriteLine("Update now!"), 
       TaskContinuationOptions.OnlyOnRanToCompletion);
}

I have found a workaround that works pretty nicely, but I would like to make the first idea work.

private CancellationTokenSource? _cts;
private CancellationTokenRegistration? _cancellationTokenRegistration;

private void Update()
{
    _cancellationTokenRegistration?.Unregister();
    _cts = new();
    _cancellationTokenRegistration = _cts.Token.Register(() => Debug.WriteLine("Update now!"));
    _cts.CancelAfter(1000);
}

Solution

  • You should consider using Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive and add using System.Reactive.Linq;.

    You didn't say hat UI you're using, so for Windows Forms also add System.Reactive.Windows.Forms and for WPF System.Reactive.Windows.Threading.

    Then you can do this:

    Panel panel = new Panel(); // assuming this is a scrollable control
    
    IObservable<EventPattern<ScrollEventArgs>> query =
        Observable
            .FromEventPattern<ScrollEventHandler, ScrollEventArgs>(
                h => panel.Scroll += h,
                h => panel.Scroll -= h)
            .Select(sea => Observable.Timer(TimeSpan.FromSeconds(1.0)).Select(_ => sea))
            .Switch();
        
    IDisposable subscription = query.Subscribe(sea => Console.WriteLine("Hello"));
    

    The query is firing for every Scroll event and starts a one second timer. The Switch operator watches for every Timer produces and only connects to the latest one produced, thus ignoring the previous Scroll events.

    And that's it.

    After scrolling has a 1 second pause the word "Hello" is written to the console. If you begin scrolling again then after every further 1 second pause it fires again.