我正在使用MVVM,在我的viewmodel中我启动了一个线程(它是一个服务器应用程序,所以它是一个连接线程),我正在寻找一种适当的方式(或方式(!))来通知关于在另一个线程上发生的事情的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");
}
}
我正在寻找可能的解决方案,最好是最好的解决方案。我是初学程序员,所以我可能无法理解最复杂的解决方案,请记住这一点。注意:我阅读有关事件,观察者模式和其他一些可能的解决方案,只是我不知道正确使用它们(当然还有如何)。提前谢谢!
答案 0 :(得分:3)
我将向您介绍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/en-us/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");
}
}
}
这适用于您的案例
答案 1 :(得分:2)
另一个类似的工作示例
查看
<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>
查看CodeBehind
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));
}
}
}
答案 2 :(得分:2)
如果您正在使用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/en-us/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
}
}
此VM与您的VM之间的唯一区别是,此VM实现了IObserver模式,该模式提供了一种接收基于推送的通知的机制。文档在这里http://msdn.microsoft.com/en-us/library/dd783449(v=vs.110).aspx
因为它很简单,VM在构造函数中启动线程。在您的情况下,您将在StartCommand的Execute
委托中启动该线程。上面的VM使用一组字符串,因此需要一个调度程序。幸运的是,调度程序由ListCollectionView
类开箱即用。 http://msdn.microsoft.com/en-us/library/system.windows.data.listcollectionview.aspx如果您正在更新字符串属性,则不需要调度程序,因为绑定引擎会为您执行编组。
使用这两个类,可以使用此Xaml创建一个小应用程序......
<Grid>
<ListBox ItemsSource="{Binding LogEntries}"/>
</Grid>
运行应用程序时,ListBox将每两秒更新一次,在保持响应式UI时不会发生线程冲突。
注意:我在.NET 4.5下构建了应用程序,MINIMUM版本是.NET 4.0。它可以没有 Rx。如果您决定使用完整的RX,您可以利用ObserveOn方法进一步简化多线程应用程序。您可以在Visual Studio中使用NuGet管理器来安装完整的Reactive Extensions。