3

In my WPF application the data that UI displays will be updated too frequently. I figured out that it will be great to leave the logic intact and solve this issue with an extra class that stores the most recent data and raises the update event after some delay.

So the goal is to update UI, lets say every 50 ms, and display the most recent data. But if there is no new data to show, then the UI shan't be updated.

Here is an implementation I have created so far. Is there a way to do it 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 { };
}
puretppc
  • 3,232
  • 8
  • 38
  • 65
Kuba
  • 457
  • 3
  • 13
  • Where's your data coming from? Are you ok with loosing older pieces of it, over the most recent ones? – noseratio Feb 07 '14 at 11:48
  • The data is coming from tasks (wrapped WebClients to be exact). I'm totally fine with loosing older pieces - it's just for displaying current status. – Kuba Feb 07 '14 at 11:56

3 Answers3

2

I'd suggest that you don't reinvent the wheel. The Microsoft Reactive Framework handles this situation super easily. The reactive framework allows you to turn events into linq queries.

I'm assuming that you're trying to call DownloadStringAsync and therefore need to handle the DownloadStringCompleted event.

So first you have to turn the event into an IObservable<>. That's easy:

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

This returns an object of type IObservable<EventPattern<DownloadStringCompletedEventArgs>>. It might be better to turn this into an IObservable<string>. That's easy too.

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

Now to actually get the values out, but limit them to every 50ms is also easy.

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

That's it. Super easy.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • +1, this is fresh. As a person who has only basic understanding of Rx, I've got a question. How to propagate the updates to the WPF UI thread from inside `Subscribe` lambda? Should I use `SynchronizationContextScheduler` for that, or does it happen automatically? – noseratio Feb 08 '14 at 01:47
  • You just need to add `ObserveOn(...)` with the sync context to make the lambda run on the UI thread. – Enigmativity Feb 08 '14 at 03:13
  • It's quite slick, but is it possible to drop events from the stream? 50ms is a very tight interval. In the OP's case, he's only interested in observing the the most recent data item. IIUC, your Rx solution would work similar to `Progress`, with the same concern I described in my answer. Please correct me if I'm wrong. – noseratio Feb 08 '14 at 04:17
  • 1
    @Noseratio - The `Sample` method does drop events from the stream. That's its purpose. The stream would only ever have the latest value, but I hear what you're saying - there may be more values queued on the message pump. It would be a matter of lengthening the interval to prevent that, but that's really the point of the whole question anyway. – Enigmativity Feb 08 '14 at 06:58
1

I would do it the other way around, i.e., run the UI update task on the UI thread, and request the data from there. In a nutshell:

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

This updates the status as fast as data arrives, but note await Dispatcher.Yield(DispatcherPriority.Background). It's there to keeps the UI responsive if data is arriving too fast, by giving the status update iterations a lower priority than user input events.

[UPDATE] I decided to take this a bit further and show how to handle the case when there's a background operation constantly producing the data. We might use Progress<T> pattern to post updates to the UI thread (as shown here). The problem with this would be that Progress<T> uses SynchronizationContext.Post which queues callbacks asynchronously. Thus, the currently shown data item might not have been the most recent one already when it got displayed.

To avoid that, I created Buffer<T> class, which is essentially a producer/consumer for a single data item. It exposes async Task<T> GetData() on the consumer side. I couldn't find anything similar in System.Collections.Concurrent, although it may already exist somewhere (I'd be interested if someone points that out). Below is a complete WPF app:

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

    }
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I had the same idea: https://github.com/nabuk/ProxySwarm/blob/a1efcc4f9c17523f42aacdc6e0696cda1f7303d7/src/ProxySwarm.Domain/Miscellaneous/Notifier.cs but your implementation is better - it has only 2 instance variables and handles cancellation scenario. – Kuba Feb 08 '14 at 06:28
0

Assuming that you are updating your UI through data binding (as you should in WPF), and that you are on .NET 4.5, you can simply use the delay property on your binding expression instead of all this infrastructure.

Read a nice, comprehensive article here.

---EDIT--- Our fake model class:

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

Our view model, that gets updated as many times as needed (always on the UI thread):

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

And finally our UI, that only gets updated after 50 ms, with no regards to how many times the view model property changed in the meantime:

    <TextBlock Text="{Binding Data, Delay=50}" />
Csaba Fabian
  • 846
  • 9
  • 19
  • I second the delay property! – Samuel Feb 07 '14 at 13:59
  • 1
    I don't think this answers the question, IIUC. The ViewModel is recommended to receive updates on the same main UI thread (where UI controls are bound to ViewModel). Could you elaborate on how you're going to update the ViewModel itself frequently, in the scenario described by OP (data arrives asynchronously as `Task`)? – noseratio Feb 07 '14 at 14:14
  • @TheFab and @Samuel, have you tried it? I did, and it doesn't work. Try ``. You might expect it to update the text every second, but that's not what's happening. It updates the `TextBlock` **as soon as** the model's `PropertyChanged` has been fired. [MSDN explains why](http://goo.gl/BD6Bf4): *the amount of time, in milliseconds, to wait before updating the binding source after the value on the target changes.*. This is for two-way bindings, to delay updating *the source (model)* when the control changes. Sorry for down-voting. – noseratio Feb 07 '14 at 21:28
  • @Noseratio: I generally avoid using imperative UI code like "this.TextBlock.Text = "data " + data;" in WPF and try to use XAML for UI with loose binding to view models. However, it is limited how much energy I'm willing to invest into answering this question, especially that Kuba seems to be happy with your solution. Consider my answer withdrawn. – Csaba Fabian Feb 08 '14 at 11:33
  • @TheFab, I'm sure you understand `this.TextBlock.Text = data` is used here for simplicity, as a part of my little test app. In the real-life code it would be a property of the model. Your `Delay` binding idea certainly has its own uses, it just doesn't apply to this case. OTOH, I think @Enigmativity's answer is much more elegant than mine. – noseratio Feb 08 '14 at 11:51
  • 1
    Ahhh ok, now I see! So it only delays the 'reverse' updates! Argh, so sorry! There was some code about a custom binding expression that I used with .NET 4.0 for this, I just assumed the new 'Delay' property provided the same functionality - my bad! Anyways, here is the link to the custom delay binding code, I would think it will still work in .NET 4.5: http://paulstovell.com/blog/wpf-delaybinding – Csaba Fabian Feb 08 '14 at 12:06
  • And yes, RX is very cool for complex async data-stream scenarios. It does have a learning curve, though. – Csaba Fabian Feb 08 '14 at 12:09
  • @TheFab try binding with `TwoWay` or `OneWayToSource`, then the delay should be inversed. – Samuel Feb 09 '14 at 16:32