[英]What's the best way to update an ObservableCollection from another thread?
[英]Best way to notify ui what's happening on another thread in wpf?
我正在使用MVVM,並且在我的視圖模型中啟動了一個線程(它是一個服務器應用程序,所以它是一個連接線程),並且我正在尋找一種適當的方式(或方式(!))來通知UI另一個線程發生了什么。 我想要這樣做的方法是,為日志添加某種文本框(可能將行存儲在ObservableCollection中),並且每次連接線程上發生任何事情時,我都希望向該文本框添加新行。 這是我設置命令的方式(該方法啟動一個正在監聽連接的線程):
public ViewModel()
{
StartCommand = new RelayCommand(PacketHandler.Start);
}
PacketHandler類:
public static void Start()
{
var connectionThread = new Thread(StartListening);
connectionThread.IsBackground = true;
connectionThread.Start();
}
private static void StartListening()
{
if (!isInitialized) Initialize();
try
{
listener.Start();
while (true)
{
client = listener.AcceptTcpClient();
// some kind of logging here which reaches the ui immediately
var protocol = new Protocol(client);
var thread = new Thread(protocol.StartCommunicating) { IsBackground = true };
thread.Start();
connectedThreads.Add(thread);
}
}
catch (Exception)
{
// temp
MessageBox.Show("Error in PacketHandler class");
}
}
我正在尋找可能的解決方案,最好是最好的。 我是一名初學者程序員,所以我可能不理解最復雜的解決方案,也請記住這一點。 注意:我了解了事件,觀察者模式以及其他一些可能的解決方案,只是我不知道正確地使用它們(當然是如何使用)。 提前致謝!
我將向您介紹BlockingCollection<T>
,它是一個線程安全的集合類,提供以下內容:
BlockingCollection<T>
是IProducerConsumerCollection<T>
接口的包裝。 這是一個簡單的例子給你
public static void Start()
{
var connectionThread = new Thread(StartListening);
connectionThread.IsBackground = true;
connectionThread.Start();
ThreadPool.QueueUserWorkItem(Logger); //start logger thread
}
//thread safe data collection, can be modified from multiple threads without threading issues
static BlockingCollection<string> logData = new BlockingCollection<string>();
public ObservableCollection<string> Logs { get; set; } // to bind to the UI
private void Logger(object state)
{
//collect everything from the logData, this loop will not terminate until `logData.CompleteAdding()` is called
foreach (string item in logData.GetConsumingEnumerable())
{
//add the item to the UI bound ObservableCollection<string>
Dispatcher.Invoke(() => Logs.Add(item));
}
}
private static void StartListening()
{
if (!isInitialized) Initialize();
try
{
listener.Start();
while (true)
{
client = listener.AcceptTcpClient();
// some kind of logging here which reaches the ui immediately
logData.TryAdd("log"); //adding a log entry to the logData, completely thread safe
var protocol = new Protocol(client);
var thread = new Thread(protocol.StartCommunicating) { IsBackground = true };
thread.Start();
connectedThreads.Add(thread);
}
}
catch (Exception)
{
// temp
MessageBox.Show("Error in PacketHandler class");
}
}
使用這種方法,您還可以讓多個線程添加日志數據而不會出現線程問題。
有關BlockingCollection<T>
更多信息,請參見http://msdn.microsoft.com/zh-cn/library/dd267312
更新資料
查看模型類
public class ViewModel
{
private Dispatcher Dispatcher;
public ViewModel()
{
StartCommand = new RelayCommand(PacketHandler.Start);
// dispatcher is required for UI updates
// remove this line and the variable if there is one
// also assuming this constructor will be called from UI (main) thread
Dispatcher = Dispatcher.CurrentDispatcher;
ThreadPool.QueueUserWorkItem(Logger); //start logger thread
}
public ObservableCollection<string> Logs { get; set; } // to bind to the UI
private void Logger(object state)
{
//collect everything from the LogData, this loop will not terminate until `CompleteAdding()` is called on LogData
foreach (string item in PacketHandler.LogData.GetConsumingEnumerable())
{
//add the item to the UI bound ObservableCollection<string>
Dispatcher.Invoke(() => Logs.Add(item));
}
}
}
和數據包處理程序類
public class PacketHandler
{
public static BlockingCollection<string> LogData = new BlockingCollection<string>();
private static void StartListening()
{
if (!isInitialized) Initialize();
try
{
listener.Start();
while (true)
{
client = listener.AcceptTcpClient();
// some kind of logging here which reaches the ui immediately
LogData.TryAdd("log"); //adding a log entry to the logData, completely thread safe
var protocol = new Protocol(client);
var thread = new Thread(protocol.StartCommunicating) { IsBackground = true };
thread.Start();
connectedThreads.Add(thread);
}
}
catch (Exception)
{
// temp
MessageBox.Show("Error in PacketHandler class");
}
}
}
這將適合您的情況
一個類似的工作示例
視圖
<Window x:Class="MultipleDataGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ScrollViewer MaxHeight="100">
<ItemsControl ItemsSource="{Binding ServerLog}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Window>
查看代碼背后
using System.Windows;
namespace MultipleDataGrid
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
}
您的ServerThread
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Windows;
using System.Windows.Data;
namespace MultipleDataGrid
{
public class Something
{
public static void Start()
{
var connectionThread = new Thread(StartListening);
Log = new ObservableCollection<string>();
BindingOperations.EnableCollectionSynchronization(Log, _lock);//For Thread Safety
connectionThread.IsBackground = true;
connectionThread.Start();
}
public static ObservableCollection<string> Log { get; private set; }
private static readonly object _lock = new object();
private static void StartListening()
{
try
{
int i = 0;
while (i <= 100)
{
Log.Add("Something happened " + i);
Thread.Sleep(1000);
i++;
}
}
catch (Exception)
{
// temp
MessageBox.Show("Error in PacketHandler class");
}
}
}
}
最后是ViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MultipleDataGrid
{
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> ServerLog { get; private set; }
public ViewModel()
{
Something.Start();
Something.Log.CollectionChanged += (s, e) =>
{
ServerLog = Something.Log;
RaisePropertyChanged("ServerLog");
};
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propName)
{
var pc = PropertyChanged;
if (pc != null)
pc(this, new PropertyChangedEventArgs(propName));
}
}
}
如果您使用的是MVVM,並且想要創建一個線程來執行給定的任務並向UI報告一些進度而沒有跨線程異常,則可以使用SOLID
原理來創建如下所示的MyWorker
類。
public class MyWorker : IObservable<string>, IDisposable
{
private Task _task;
private IObserver<string> _observer;
public IDisposable Subscribe(IObserver<string> observer)
{
_observer = observer;
return this;
}
public void StartWork()
{
_task = new Task(() =>
{
while (true)
{
// background work goes here
Thread.Sleep(2000);
if (_observer != null)
{
string status = DateTime.Now.ToString("G");
_observer.OnNext(status);
}
}
});
_task.ContinueWith(r =>
{
if (_observer != null)
{
_observer.OnCompleted();
}
});
_task.Start();
}
public void Dispose()
{
if (_task != null)
{
_task.Dispose();
_task = null;
}
}
}
它是后台任務的輕量級封裝。 該類僅創建一個Task並每兩秒報告一次時間。 它使用IObservable模式,該模式提供推送通知。 它記錄在這里http://msdn.microsoft.com/zh-cn/library/dd990377(v=vs.110).aspx
實例化此類的一個簡單ViewModel看起來像這樣...
public class ViewModel : INotifyPropertyChanged, IObserver<string>
{
readonly ListCollectionView _listCollectionView;
public ViewModel()
{
LogEntries = new ObservableCollection<string>();
_listCollectionView = CollectionViewSource.GetDefaultView(LogEntries) as ListCollectionView;
if (_listCollectionView != null)
{
MyWorker worker = new MyWorker();
worker.Subscribe(this);
worker.StartWork();
}
}
public ObservableCollection<string> LogEntries { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public void OnNext(string logEntry)
{
_listCollectionView.Dispatcher.InvokeAsync(() => LogEntries.Add(logEntry));
}
public void OnCompleted()
{
// clean up goes here
}
public void OnError(Exception error)
{
// error handling goes here
}
}
此虛擬機與您的虛擬機之間的唯一區別是,該虛擬機實現了IObserver模式,該模式提供了一種用於接收基於推式通知的機制。 這些文檔在這里http://msdn.microsoft.com/zh-cn/library/dd783449(v=vs.110).aspx
因為它很簡單,所以VM在構造函數中啟動線程。 對於您的情況,您可以在StartCommand的Execute
委托中啟動線程。 上面的VM使用字符串集合,因此需要調度程序。 幸運的是, ListCollectionView
類提供了開箱即用的調度程序。 http://msdn.microsoft.com/zh-cn/library/system.windows.data.listcollectionview.aspx如果相反,您正在更新字符串屬性,則不需要調度程序,因為綁定引擎會為您進行編組。
使用這兩個類,可以使用此Xaml創建一個小型應用程序。
<Grid>
<ListBox ItemsSource="{Binding LogEntries}"/>
</Grid>
當應用程序運行時,ListBox將每兩秒鍾更新一次,而不會出現線程沖突,同時保持響應式UI。
注意:我在.NET 4.5下構建了該應用程序,而最低版本是.NET 4.0。 沒有 Rx即可使用。 如果您決定使用完整的RX,則可以利用ObserveOn方法,該方法進一步簡化了多線程應用程序。 您可以在Visual Studio中使用NuGet管理器來安裝完整的Reactive Extensions。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.