Raise an event with the latest data after a while

In my WPF application, the data displayed in the user interface will be updated too often. I realized that it would be great to leave the logic intact and solve this problem with an extra class that stores the most recent data and causes some updating after some delay.

So the goal is to update the interface, say every 50 ms, and display the most recent data. But if you do not show new data, then the user interface is not updated.

Here is the implementation I have created so far. Is there any way to do this without locking? Is my implementation correct?

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
source share
3 answers

, . :

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). , , , , .

[UPDATE] , , . Progress<T> ( ). , 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);
            }
        }

    }
}
+1

, . 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;

, , 50 , .

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

. .

+2

, ( WPF) .NET 4.5, delay .

, .

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

, , ( ):

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

And finally, our user interface, which is updated only after 50 ms, regardless of how many times the property of the view model has changed:

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

All Articles