2014-02-07 48 views
3

在我的WPF應用程序中,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 { }; 
} 
+0

您的數據來自哪裏?你可以放棄較舊的部分,而不是最新的部分? – Noseratio

+0

數據來自任務(精確包裝Web客戶端)。 我完全沒有損失舊件 - 它只是爲了顯示當前狀態。 – Kuba

回答

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的響應。

[UPDATE]我決定進一步說明一下如何處理背景操作不斷產生數據的情況。我們可能會使用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); 
      } 
     } 

    } 
} 
+1

我有同樣的想法:https://github.com/nabuk/ProxySwarm/blob/a1efcc4f9c17523f42aacdc6e0696cda1f7303d7/src/ProxySwarm.Domain/Miscellaneous/Notifier.cs 但你的實現是更好 - 它只有2個實例變量和手柄取消場景。 – Kuba

0

假設你通過數據更新的用戶界面結合(如你在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()); 
    } 
} 

最後我們的用戶界面只在50毫秒後得到更新,而不考慮在此期間視圖模型屬性發生了多少變化:

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

我第二個延遲屬性! – Samuel

+1

我不認爲這回答了這個問題,IIUC。建議ViewModel在相同的主UI線程(UI控件綁定到ViewModel)上接收更新。您可以詳細說明如何在OP描述的場景中頻繁更新ViewModel本身(數據以「任務'異步到達)? – Noseratio

+0

給答案增加更深的解釋... –

2

我建議你不要重新發明輪子。微軟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. 
    }); 

就是這樣。超級簡單。

+0

+1,這是新鮮的。作爲一個對Rx只有基本瞭解的人,我有一個問題。如何將更新傳播到'Subscribe' lambda中的WPF UI線程?我應該爲此使用'SynchronizationContextScheduler',還是自動發生? – Noseratio

+0

您只需在同步上下文中添加'ObserveOn(...)'以使該lambda在UI線程上運行。 – Enigmativity

+0

這很光滑,但是可以從流中刪除事件嗎? 50ms是非常緊張的時間間隔。在OP的情況下,他只對觀察最近的數據項目感興趣。 IIUC,您的Rx解決方案與「進度」類似,與我在答案中所描述的相同。如果我錯了,請糾正我。 – Noseratio

相關問題