Reputation: 63
I'm using MVVM, and in my viewmodel I start a thread (it is a server application, so it is a connection thread), and I'm looking for an appropriate way (or ways (!)) to notify the UI about what's happening on another thread. The way I want to do it, to have a textbox of some sort for logs (lines stored in an ObservableCollection probably), and each time something happens on the connection thread, I would want a new line added to the textbox. Here's how I set the command (the method starts a thread which is listening for connections):
public ViewModel()
{
StartCommand = new RelayCommand(PacketHandler.Start);
}
PacketHandler class:
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");
}
}
I'm looking for possible solutions, preferably the best. I'm a beginner programmer, so I may not comprehend the most complex solutions, please bear in mind this, too. NOTE: I read about events, observer pattern, and a few other things as possible solutions, only I don't know which (and of course: how) to use them properly. Thanks in advance!
Upvotes: 1
Views: 2371
Reputation: 13679
I am going to introduce you to BlockingCollection<T>
which is a thread-safe collection class that provides the following:
BlockingCollection<T>
is a wrapper for the IProducerConsumerCollection<T>
interface.here is a simple example for you
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");
}
}
using this approach you can also have multiple threads adding log data without threading issues.
for more info on BlockingCollection<T>
refer http://msdn.microsoft.com/en-us/library/dd267312
Update
view model class
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));
}
}
}
and packet handler class
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");
}
}
}
this will work for your case
Upvotes: 3
Reputation: 8792
If you are using MVVM and want to create a thread to perform a given task and report back some progress to the UI without cross-threaded exceptions, you can use SOLID
principles to create a MyWorker
class that looks like this...
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;
}
}
}
It is a light-weight encapsulation of the background task. The class simply creates a Task and reports back the time every two seconds. It uses the IObservable pattern, which provides push notifications. It is documented here http://msdn.microsoft.com/en-us/library/dd990377(v=vs.110).aspx
A simple ViewModel that instantiates this class looks like this...
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
}
}
The only difference between this VM and your VM is that this one implements the IObserver pattern, which provides a mechanism for receiving push-based notifications. The docs are here http://msdn.microsoft.com/en-us/library/dd783449(v=vs.110).aspx
Because it's simple, the VM starts the thread in the constructor. In your case, you would start the thread in the Execute
delegate of your StartCommand. The VM above works with a collection of strings, hence the need for a dispatcher. Fortunately the dispatcher is provided out-of-the-box by the ListCollectionView
class. http://msdn.microsoft.com/en-us/library/system.windows.data.listcollectionview.aspx If instead, you are updating a string property then the dispatcher is not needed because the binding engine does the marshalling for you.
With these two classes, a small application can be made with this Xaml...
<Grid>
<ListBox ItemsSource="{Binding LogEntries}"/>
</Grid>
When the application is run, the ListBox will be updated every two seconds with no threading conflicts while maintaining a responsive UI.
Note: I built the application under .NET 4.5, and the MINIMUM version is .NET 4.0. It will work without Rx. If you decide to go with the full RX, you can take advantage of the ObserveOn method which gives further streamlining to multi-threaded applications. You can use the NuGet manager from within Visual Studio to install the full Reactive Extensions.
Upvotes: 3
Reputation: 3004
One more similar working example
View
<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>
View CodeBehind
using System.Windows;
namespace MultipleDataGrid
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
}
Your 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");
}
}
}
}
And finally the 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));
}
}
}
Upvotes: 2