【问题标题】:Raise an event with the most recent data after some time一段时间后使用最新数据引发事件
【发布时间】:2014-02-08 13:10:01
【问题描述】:

在我的 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 { };
}

【问题讨论】:

  • 您的数据来自哪里?您是否愿意丢失旧的部分,而不是最近的部分?
  • 数据来自任务(确切地说是包装的 WebClients)。我完全可以丢失旧件 - 这只是为了显示当前状态。

标签: c# wpf asynchronous


【解决方案1】:

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

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

所以首先你必须把事件变成IObservable&lt;&gt;。这很简单:

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

这将返回IObservable&lt;EventPattern&lt;DownloadStringCompletedEventArgs&gt;&gt; 类型的对象。将其转换为IObservable&lt;string&gt; 可能会更好。这也很容易。

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

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

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

就是这样。超级简单。

【讨论】:

  • +1,这是新鲜的。作为一个对 Rx 只有基本了解的人,我有一个问题。如何从Subscribe lambda 内部将更新传播到 WPF UI 线程?我应该为此使用SynchronizationContextScheduler,还是自动发生?
  • 您只需在同步上下文中添加ObserveOn(...) 即可使 lambda 在 UI 线程上运行。
  • 这很漂亮,但是可以从流中删除事件吗? 50ms 是一个非常紧凑的间隔。在 OP 的情况下,他只对观察最新的数据项感兴趣。 IIUC,您的 Rx 解决方案的工作方式类似于Progress&lt;T&gt;,与我在回答中描述的问题相同。如果我错了,请纠正我。
  • @Noseratio - Sample 方法确实从流中删除事件。这就是它的目的。流只会有最新的值,但我听到你在说什么 - 消息泵上可能有更多的值排队。这将是延长间隔以防止这种情况发生的问题,但这确实是整个问题的重点。
【解决方案2】:

我会反过来做,即在 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&lt;T&gt; 模式将更新发布到UI 线程(如here 所示)。这样做的问题是 Progress&lt;T&gt; 使用 SynchronizationContext.Post 异步排队回调。因此,当前显示的数据项在显示时可能还不是最新的。

为了避免这种情况,我创建了Buffer&lt;T&gt; 类,它本质上是单个数据项的生产者/消费者。它在消费者端公开async Task&lt;T&gt; 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);
            }
        }

    }
}

【讨论】:

【解决方案3】:

假设您正在通过数据绑定更新您的 UI(正如您应该在 WPF 中那样),并且您在 .NET 4.5 上,您可以简单地在您的绑定表达式上使用 delay 属性而不是所有这些基础结构。

阅读一篇不错的综合文章here

---编辑--- 我们的假模型类:

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}" />

【讨论】:

  • 我第二个延迟属性!
  • 我认为这不能回答问题,IIUC。建议 ViewModel 在同一个主 UI 线程(其中 UI 控件绑定到 ViewModel)上接收更新。您能否详细说明在 OP 描述的场景中您将如何频繁更新 ViewModel 本身(数据以Task&lt;T&gt; 异步到达)?
  • @TheFab 和@Samuel,你试过了吗?我做了,它不起作用。试试&lt;TextBlock Text="{Binding Data, Delay=1000}" /&gt;。您可能希望它每秒更新一次文本,但事实并非如此。 一旦模型的PropertyChanged 被触发,它就会更新TextBlockMSDN explains why在目标值更改后更新绑定源之前等待的时间量(以毫秒为单位)。。这是用于双向绑定,以在控件更改时延迟更新源(模型)。很抱歉投了反对票。
  • @Noseratio:我通常避免使用命令式 UI 代码,例如 "this.TextBlock.Text = "data " + data;"在 WPF 中并尝试将 XAML 用于与视图模型松散绑定的 UI。但是,我愿意投入多少精力来回答这个问题是有限的,尤其是 Kuba 似乎对您的解决方案感到满意。考虑撤回我的回答。
  • 啊,好吧,现在我明白了!所以它只会延迟“反向”更新!啊,很抱歉!有一些关于我在 .NET 4.0 中使用的自定义绑定表达式的代码,我只是假设新的“延迟”属性提供了相同的功能——我的错!无论如何,这是自定义延迟绑定代码的链接,我认为它仍然可以在 .NET 4.5 中使用:paulstovell.com/blog/wpf-delaybinding
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-25
  • 1970-01-01
  • 1970-01-01
  • 2020-01-12
相关资源
最近更新 更多