Search code examples
c#taskfactory

How to check if previous task is still running and stop/cancel it?


I have a background task that I run in the following way.

This is in an attached behavior on a textbox text changed event.

What I would like is if the text is changed and then changed again, on the second change check if the previous task is still running, if so, stop it and continue with the latest one.

public class FindTextChangedBehavior : Behavior<TextBox>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.TextChanged += OnTextChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.TextChanged -= OnTextChanged;
        base.OnDetaching();
    }

    private void OnTextChanged(object sender, TextChangedEventArgs args)
    {
        var textBox = (sender as TextBox);
        if (textBox != null)
        {
            Task.Factory.StartNew(() =>
            {
                //Do text search on object properties within a DataGrid
                //and populate temporary ObservableCollection with items.
                ClassPropTextSearch.init(itemType, columnBoundProperties);

                if (itemsSource != null)
                {
                    foreach (object o in itemsSource)
                    {
                        if (ClassPropTextSearch.Match(o, searchValue))
                        {
                            tempItems.Add(o);
                        }
                    }
                }

                //Copy temporary collection to UI bound ObservableCollection 
                //on UI thread
                Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
            });
        }
    }

[EDIT] I have not tested this yet just a mockup of what might be.

CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();

private void OnTextChanged(object sender, TextChangedEventArgs args)
{
    var newCts = new CancellationTokenSource();
    var oldCts = Interlocked.Exchange(ref this.CancellationTokenSource, newCts);

    if (oldCts != null)
    {
        oldCts.Cancel();
    }

    var cancellationToken = newCts.Token;

    var textBox = (sender as TextBox);
    if (textBox != null)
    {
        ObservableCollection<Object> tempItems = new ObservableCollection<Object>();
        var ui = TaskScheduler.FromCurrentSynchronizationContext();

        var search = Task.Factory.StartNew(() =>
        {
            ClassPropTextSearch.init(itemType, columnBoundProperties);

            if (itemsSource != null)
            {
                foreach (object o in itemsSource)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    if (ClassPropTextSearch.Match(o, searchValue))
                    {
                        tempItems.Add(o);
                    }
                }
            }
        }, cancellationToken);

        //Still to be considered.
        //If it gets to here and it is still updating the UI then 
        //what to do, upon SearchMarkers being set below do I cancel
        //or wait until it is done and continue to update again???
        var displaySearchResults = search.ContinueWith(resultTask =>
                     MyClass.Instance.SearchMarkers = tempItems,
                     CancellationToken.None,
                     TaskContinuationOptions.OnlyOnRanToCompletion,
                     ui);
    }
}

enter image description here


Solution

  • I am a bit worried about you proposing trawling through "object properties within a DataGrid" on a non-UI thread - this may very well work as you're not setting any values from the background thread, but has a bit of a smell to it.

    Ignoring that for now, let me propose the following solution:

    private readonly SemaphoreSlim Mutex = new SemaphoreSlim(1, 1);
    private CancellationTokenSource CancellationTokenSource;
    
    private void OnTextChanged(object sender, TextChangedEventArgs args)
    {
        var newCts = new CancellationTokenSource();
        var oldCts = Interlocked.Exchange(ref this.CancellationTokenSource, newCts);
    
        if (oldCts != null)
        {
            oldCts.Cancel();
        }
    
        var cancellationToken = newCts.Token;
    
        var textBox = (sender as TextBox);
        if (textBox != null)
        {
            // Personally I would be capturing
            // TaskScheduler.FromCurrentSynchronizationContext()
            // here and then scheduling a continuation using that (UI) scheduler.
            Task.Factory.StartNew(() =>
            {
                // Ensure that only one thread can execute
                // the try body at any given time.
                this.Mutex.Wait(cancellationToken);
    
                try
                {
                    cancellationToken.ThrowIfCancellationRequested();
    
                    RunSearch(cancellationToken);
    
                    cancellationToken.ThrowIfCancellationRequested();
    
                    //Copy temporary collection to UI bound ObservableCollection 
                    //on UI thread
                    Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
                }
                finally
                {
                    this.Mutex.Release();
                }
            }, cancellationToken);
        }
    }
    

    EDIT

    Since I know now that you're targeting an async-aware framework, the above solution can be both simplified and enhanced.

    I had to make numerous assumptions as to how "grid properties" are harvested and made an attempt to decouple that process (which in my mind should be running on the dispatcher thread) from the actual search (which I'm scheduling on the thread pool).

    public class FindTextChangedBehavior : Behavior<TextBox>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.TextChanged += OnTextChanged;
        }
    
        protected override void OnDetaching()
        {
            AssociatedObject.TextChanged -= OnTextChanged;
            base.OnDetaching();
        }
    
        private CancellationTokenSource CancellationTokenSource;
    
        // We're a UI handler, hence async void.
        private async void OnTextChanged(object sender, TextChangedEventArgs args)
        {
            // Assume that this always runs on the UI thread:
            // no thread safety when exchanging the CTS.
            if (this.CancellationTokenSource != null)
            {
                this.CancellationTokenSource.Cancel();
            }
    
            this.CancellationTokenSource = new CancellationTokenSource();
    
            var cancellationToken = this.CancellationTokenSource.Token;
    
            var textBox = (sender as TextBox);
            if (textBox != null)
            {
                try
                {
                    // If your async work completes too quickly,
                    // the dispatcher will be flooded with UI
                    // update requests producing a laggy user
                    // experience. We'll get around that by
                    // introducing a slight delay (throttling)
                    // before going ahead and performing any work.
                    await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
    
                    // Reduce TaskCanceledExceptions.
                    // This is async void, so we'll just
                    // exit the method instead of throwing.
    
                    // IMPORTANT: in order to guarantee that async
                    // requests are executed in correct order
                    // and respond to cancellation appropriately,
                    // you need to perform this check after every await.
                    // THIS is the reason we no longer need the Semaphore.
                    if (cancellationToken.IsCancellationRequested) return;
    
                    // Harvest the object properties within the DataGrid.
                    // We're still on the UI thread, so this is the
                    // right place to do so.
                    IEnumerable<GridProperty> interestingProperties = this
                        .GetInterestingProperties()
                        .ToArray(); // Redundant if GetInterestingProperties returns a
                                    // list, array or similar materialised IEnumerable.
    
                    // This appears to be CPU-bound, so Task.Run is appropriate.
                    ObservableCollection<object> tempItems = await Task.Run(
                        () => this.ResolveSearchMarkers(interestingProperties, cancellationToken)
                    );
    
                    // Do not forget this.
                    if (cancellationToken.IsCancellationRequested) return;
    
                    // We've run to completion meaning that
                    // OnTextChanged has not been called again.
                    // Time to update the UI.
                    MyClass.Instance.SearchMarkers = tempItems;
                }
                catch (OperationCanceledException)
                {
                    // Expected.
                    // Can still be thrown by Task.Delay for example.
                }
                catch (Exception ex)
                {
                    // This is a really, really unexpected exception.
                    // Do what makes sense: log it, invalidate some
                    // state, tear things down if necessary.
                }
            }
        }
    
        private IEnumerable<GridProperty> GetInterestingProperties()
        {
            // Be sure to return a materialised IEnumerable,
            // i.e. array, list, collection.
            throw new NotImplementedException();
        }
    
        private ObservableCollection<object> ResolveSearchMarkersAsync(
            IEnumerable<GridProperty> interestingProperties, CancellationToken cancellationToken)
        {
            var tempItems = new ObservableCollection<object>();
    
            //Do text search on object properties within a DataGrid
            //and populate temporary ObservableCollection with items.
            foreach (var o in interestingProperties)
            {
                cancellationToken.ThrowIfCancellationRequested();
    
                if (ClassPropTextSearch.Match(o, searchValue))
                {
                    tempItems.Add(o);
                }
            }
    
            return tempItems;
        }
    }