在一段时间后使用最新数据提升事件

时间:2014-02-07 11:11:24

标签: c# wpf asynchronous

在我的WPF应用程序中,UI显示的数据将过于频繁地更新。 我发现将逻辑完好无损并用一个存储最新数据的额外类来解决这个问题会很好,并在一段延迟后引发更新事件。

因此,目标是更新UI,每50毫秒说一次,并显示最新数据。但是,如果没有要显示的新数据,则不应更新UI。

这是我到目前为止创建的一个实现。有没有锁定方法可以做到这一点?我的实施是否正确?

class Publisher<T>
{
    private readonly TimeSpan delay;
    private readonly CancellationToken cancellationToken;
    private readonly Task cancellationTask;

    private T data;

    private bool published = true;
    private readonly object publishLock = new object();

    private async void PublishMethod()
    {
        await Task.WhenAny(Task.Delay(this.delay), this.cancellationTask);
        this.cancellationToken.ThrowIfCancellationRequested();

        T dataToPublish;
        lock (this.publishLock)
        {
            this.published = true;
            dataToPublish = this.data;
        }
        this.NewDataAvailable(dataToPublish);
    }

    internal Publisher(TimeSpan delay, CancellationToken cancellationToken)
    {
        this.delay = delay;
        this.cancellationToken = cancellationToken;
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false);
        this.cancellationTask = tcs.Task;
    }

    internal void Publish(T data)
    {
        var runNewTask = false;

        lock (this.publishLock)
        {
            this.data = data;
            if (this.published)
            {
                this.published = false;
                runNewTask = true;
            }
        }

        if (runNewTask)
            Task.Run((Action)this.PublishMethod);
    }

    internal event Action<T> NewDataAvailable = delegate { };
}

3 个答案:

答案 0 :(得分:2)

我建议你不要重新发明轮子。 Microsoft Reactive Framework非常容易处理这种情况。反应框架允许您将事件转换为linq查询。

我假设您正在尝试拨打DownloadStringAsync,因此需要处理DownloadStringCompleted事件。

首先,您必须将事件转换为IObservable<>。这很简单:

var source = Observable
    .FromEventPattern<
        DownloadStringCompletedEventHandler,
        DownloadStringCompletedEventArgs>(
        h => wc.DownloadStringCompleted += h,
        h => wc.DownloadStringCompleted -= h);

返回IObservable<EventPattern<DownloadStringCompletedEventArgs>>类型的对象。将其转换为IObservable<string>可能更好。这也很容易。

var sources2 =
    from ep in sources
    select ep.EventArgs.Result;

现在要实际获取值,但将它们限制为每50ms也很容易。

sources2
    .Sample(TimeSpan.FromMilliseconds(50))
    .Subscribe(t =>
    {
        // Do something with the text returned.
    });

就是这样。超级容易。

答案 1 :(得分:1)

我会反过来这样做,即在UI线程上运行UI更新任务,并从那里请求数据。简而言之:

async Task UpdateUIAsync(CancellationToken token)
{
    while (true)
    {
        token.ThrowIfCancellationRequested();

        await Dispatcher.Yield(DispatcherPriority.Background);

        var data = await GetDataAsync(token);

        // do the UI update (or ViewModel update)
        this.TextBlock.Text = "data " + data;
    }
}

async Task<int> GetDataAsync(CancellationToken token)
{
    // simulate async data arrival
    await Task.Delay(10, token).ConfigureAwait(false);
    return new Random(Environment.TickCount).Next(1, 100);
}

这会在数据到达时尽快更新状态,但请注意await Dispatcher.Yield(DispatcherPriority.Background)。通过使状态更新迭代的优先级低于用户输入事件,可以在数据到达过快时保持UI响应。

[更新] 我决定更进一步,并展示当后台操作不断产生数据时如何处理这种情况。我们可能会使用Progress<T>模式将更新发布到UI线程(如图here所示)。这个问题是Progress<T>使用SynchronizationContext.Post异步排队回调。因此,当前显示的数据项可能不是最近显示的数据项。

为了避免这种情况,我创建了Buffer<T>类,它实际上是单个数据项的生产者/消费者。它暴露了消费者方面的async Task<T> GetData()。我在System.Collections.Concurrent中找不到类似的东西,虽然它可能已经存在于某个地方(如果有人指出这一点,我会感兴趣)。以下是完整的WPF应用程序:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace Wpf_21626242
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.Content = new TextBox();

            this.Loaded += MainWindow_Loaded;
        }

        async void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                // cancel in 10s
                var cts = new CancellationTokenSource(10000);
                var token = cts.Token;
                var buffer = new Buffer<int>();

                // background worker task
                var workerTask = Task.Run(() =>
                {
                    var start = Environment.TickCount;
                    while (true)
                    {
                        token.ThrowIfCancellationRequested();
                        Thread.Sleep(50);
                        buffer.PutData(Environment.TickCount - start);
                    }
                });

                // the UI thread task
                while (true)
                {
                    // yield to keep the UI responsive
                    await Dispatcher.Yield(DispatcherPriority.Background);

                    // get the current data item
                    var result = await buffer.GetData(token);

                    // update the UI (or ViewModel)
                    ((TextBox)this.Content).Text = result.ToString();
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        /// <summary>Consumer/producer async buffer for single data item</summary>
        public class Buffer<T>
        {
            volatile TaskCompletionSource<T> _tcs = new TaskCompletionSource<T>();
            object _lock = new Object();  // protect _tcs

            // consumer
            public async Task<T> GetData(CancellationToken token)
            {
                Task<T> task = null;

                lock (_lock)
                    task = _tcs.Task;

                try
                {
                    // observe cancellation
                    var cancellationTcs = new TaskCompletionSource<bool>();
                    using (token.Register(() => cancellationTcs.SetCanceled(),
                        useSynchronizationContext: false))
                    {
                        await Task.WhenAny(task, cancellationTcs.Task).ConfigureAwait(false);
                    }

                    token.ThrowIfCancellationRequested();

                    // return the data item
                    return await task.ConfigureAwait(false);
                }
                finally
                {
                    // get ready for the next data item
                    lock (_lock)
                        if (_tcs.Task == task && task.IsCompleted)
                            _tcs = new TaskCompletionSource<T>();
                }
            }

            // producer
            public void PutData(T data)
            {
                TaskCompletionSource<T> tcs;
                lock (_lock)
                {
                    if (_tcs.Task.IsCompleted)
                        _tcs = new TaskCompletionSource<T>();
                    tcs = _tcs;
                }
                tcs.SetResult(data);
            }
        }

    }
}

答案 2 :(得分:0)

假设您正在通过数据绑定更新UI(正如您在WPF中所做的那样),并且您使用的是.NET 4.5,您只需在绑定表达式上使用delay属性而不是所有此基础结构

阅读一篇精彩,全面的文章here

--- --- EDIT 我们的假模型类:

public class Model
{
    public async Task<int> GetDataAsync()
    {
        // Simulate work done on the web service
        await Task.Delay(1000);
        return new Random(Environment.TickCount).Next(1, 100);
    }
}

我们的视图模型,根据需要多次更新(始终在UI线程上):

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    private readonly Model _model = new Model();
    private int _data;

    public int Data
    {
        get { return _data; }
        set
        {
            // NotifyPropertyChanged boilerplate
            if (_data != value)
            {
                _data = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Data"));
            }
        }
    }

    /// <summary>
    /// Some sort of trigger that starts querying the model; for simplicity, we assume this to come from the UI thread.
    /// If that's not the case, save the UI scheduler in the constructor, or pass it in through the constructor.
    /// </summary>
    internal void UpdateData()
    {
        _model.GetDataAsync().ContinueWith(t => Data = t.Result, TaskScheduler.FromCurrentSynchronizationContext());
    }
}

最后我们的UI,只在50毫秒后更新,而不考虑视图模型属性在此期间改变了多少次:

    <TextBlock Text="{Binding Data, Delay=50}" />