简体   繁体   中英

Best way to notify ui what's happening on another thread in wpf?

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!

I am going to introduce you to BlockingCollection<T> which is a thread-safe collection class that provides the following:

  • An implementation of the producer/consumer pattern; BlockingCollection<T> is a wrapper for the IProducerConsumerCollection<T> interface.
  • Concurrent addition and removal of items from multiple threads with the Add and Take methods.
  • A bounded collection that blocks Add and Take operations when the collection is full or empty.
  • Cancellation of Add or Take operations by using a CancellationToken object in the TryAdd or TryTake method.

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

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM