簡體   English   中英

通知ui的最佳方法是在wpf中另一個線程上發生了什么?

[英]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>接口的包裝。
  • 使用Add和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/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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM