1

我有一个后台任务,我按以下方式运行。

这是文本框文本更改事件的附加行为。

我想要的是如果文本被更改然后再次更改,在第二次更改时检查上一个任务是否仍在运行,如果是,则停止它并继续执行最新的任务。

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

[编辑]我还没有测试过这个,只是一个可能的模型。

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

在此处输入图像描述

4

2 回答 2

4

我有点担心您建议在非 UI 线程上通过“DataGrid 中的对象属性”进行拖网搜索——这可能会很好,因为您没有从后台线程设置任何值,但有一点味道它。

暂时忽略这一点,让我提出以下解决方案:

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

编辑

由于我现在知道您的目标是async-aware 框架,因此可以简化和增强上述解决方案。

我不得不对如何获取“网格属性”做出许多假设,并尝试将该进程(在我看来应该在调度程序线程上运行)与实际搜索(我在线程池上调度)分离)。

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;
    }
}
于 2014-06-24T23:50:45.217 回答
1

如果您将取消令牌从 CancellationTokenSource 传递给任务,并通过增加变量来记录您更改文本的次数,您将能够检查此值并通过调用 token.Cancel 取消任务(),并调用引发 OperationCancelled 异常的 ThrowIfCancellationRequested。

延续必须通过 TaskContinuationOptions.OnlyOnCanceled 以便仅在引发取消时执行,否则将在其他任务完成时引发。(代码示例如下)

于 2014-06-24T08:15:21.560 回答