通知ui在wpf中另一个线程上发生了什么的最好方法?

时间:2014-06-07 12:57:09

标签: c# wpf multithreading logging mvvm

我正在使用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");
        }
    }

我正在寻找可能的解决方案,最好是最好的解决方案。我是初学程序员,所以我可能无法理解最复杂的解决方案,请记住这一点。注意:我阅读有关事件,观察者模式和其他一些可能的解决方案,只是我不知道正确使用它们(当然还有如何)。提前谢谢!

3 个答案:

答案 0 :(得分:3)

我将向您介绍BlockingCollection<T>,它是一个线程安全的集合类,提供以下内容:

  • 生产者/消费者模式的实施; BlockingCollection<T>IProducerConsumerCollection<T>接口的包装器。
  • 使用Add and Take方法同时添加和删除多个线程中的项目。
  • 有限集合,当集合已满或为空时阻止“添加”和“执行”操作。
  • 使用TryAdd或TryTake方法中的CancellationToken对象取消添加或执行操作。

这是一个简单的例子

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。