1. 程式人生 > >WPF&MVVM執行緒問題(progressbar為例)

WPF&MVVM執行緒問題(progressbar為例)

WPF&MVVM執行緒問題

別讓能力撐不起野心

後臺,UI執行緒

專業解釋我就不貼,說說自己的個人愚見,執行緒有後臺,UI(前臺)之分,UI元素所使用的執行緒為UI執行緒,其他的可以理解為後臺執行緒。
區別:程式要關閉,必須等待UI執行緒終止,而不用等待後臺執行緒終止。(這個也是為什麼有時候我們的介面會卡死,但也關閉不了的原因)

舉個例子:
介面上做個按鈕ProgressBar,和Button,按鈕click設定點選事件:

 private void Button_Click(object sender, RoutedEventArgs e)
        {
            for
(int j = 0; j < 100; j++) { this.progressbar.Value = j; System.Threading.Thread.Sleep(100); } }

按鈕點選後,我們會發現視窗卡住了,今天我們就是要處理這樣的問題。

一般執行緒互動

有了上面拋的磚,下面我們繼續撿磚(常規WPF的執行緒互動)。

先說說所使用的技術:
1.Dispatcher(UI),執行緒排程器;
2.Task.Factory,(執行緒工廠,底層是執行緒池);

a.首次修改(引入Dispatcher)

直接上程式碼了

 private void Button_Click(object sender, RoutedEventArgs e)
        {
                for (int j = 0; j < 100; j++)
                {
this.progressbar.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, (Action)delegate () { 
                            this.progressbar
.Value = j; }); System.Threading.Thread.Sleep(100); } }

這裡通過this.progressbar.Dispatcher來獲取UI執行緒的排程器,然後使用非同步方法更新UI,
關於Dispatcher的用法可以參考這裡Dispatcher

實際上是,這裡並沒有起任何作用,那麼就有疑問:為什麼我呼叫了UI執行緒更新UI卻不起作用。
原因在於,我們這個事件的擁有者是sender,而這裡的sender就是Button,UI元素,其實這裡本身就已經是UI執行緒,也是主執行緒。

b.再次修改(引入Task)

老規矩,先上程式碼:

private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task.Factory.StartNew(() =>
            {
                for (int j = 0; j < 100; j++)
                {
                    this.progressbar.Value = j;
                    System.Threading.Thread.Sleep(100);
                }
            });
        }

這裡加入了Task.Factory的用法,關於Task.Factory的用法可以參考這裡Task.Factory

編譯通過,可是執行時報錯,在 this.progressbar.Value = j 時報
“呼叫執行緒無法訪問此物件,因為另一個執行緒擁有該物件。”

我們startnew了一個新執行緒,但是在這個執行緒中操作UI物件(不是UI執行緒,這裡要注意),物件j傳遞出錯。

c.最後修改

聰明的人應該已經知道接下來就是兩者一起運用了:

private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task.Factory.StartNew(() =>
            {
                for (int j = 0; j < 100; j++)
                {                    this.progressbar.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, (Action)delegate () { this.progressbar.Value = j; });
                    System.Threading.Thread.Sleep(100);
                }
            });
        }

這裡結合使用,具體的意思是:在新執行緒(後臺執行緒)中進行執行運算(sleep(100)模擬運算),在UI執行緒進行小更新(progressbar),各司其職,合理分配。

MVVM執行緒互動

關於mvvm的使用,已經不是新鮮事了,但是系統的mvvm的教程還是不多,而且一般都是邊開發邊學習得來的知識,包括自己,難免會缺三補四。
關於MVVM模式的說明和使用,個人推薦一下msdn說明devexpress應用&程式碼
吐槽的話少說,上硬菜。

1.mvvm的濫用:
為什麼這麼說,自己之前的一個愚見,通過在ViewModel中設定相關的屬性變數,命令command,事件event,然後讓View的控制元件進行繫結,通過後臺修改ViewModel變數的屬性,實現更新UI。這樣一來,介面增加控制元件,ViewModel增加相應的變數,命令,事件,而不用再View的cs後臺進行修改。
初期,這是個還可以接受的做法,到了後期,會發現,這個ViewModel十分的龐大,維繫成本增加,要是程式碼轉接,這也是一個需要學習瞭解的過程。(關於這點,還沒有學習到相關的資料,只是自己想象減輕工作的思路,自己也是這麼做的,就是介面控制元件化,介面拆分成細緻的控制元件,讓控制元件獨立繫結ViewModel,今天問題不在這裡)

2.ViewModel不方便的地方:
ViewModel可以實現絕大部分的介面互動,不過,總會有些奇奇怪怪的功能讓你懊惱。比如:按鈕實現窗體退出功能,子窗體跳轉功能,部分控制元件的部分屬性修改。
這些功能,對於winform老手,沒有多少選擇,直接在View後臺幾行程式碼就實現了,而選擇了使用MVVM,讓窗體的closed屬性bignding,這個還真沒有試過吧(其實窗體還沒有closed屬性,只有closed事件)。

3.MVVM執行緒互動:
以上兩點純屬是自己開發過程中的一些牢騷總結,回來正式話題。
ViewModel中的Command實際上是後臺執行緒,而非UI執行緒,與click事件之類的事件不同,而bingding的屬性都會有一個PropertyChanged的方法通知View,它控制元件bingding的這個屬性改變了,讓控制元件重新整理資料,這裡實質上是UI執行緒的互動,相關理論知識還得檢視INotifyPropertyChanged介面描述。
現在的問題來了,如果我一個控制元件,沒有實現bingding,但我想操作它的屬性,該怎麼辦,這個就是本章的主要問題,如果你正在做相關的工作,從這裡直接看就可以了。
所使用的技術點:
1.EventHandler;
2.Dispatcher(同上描述);

ViewModel的程式碼:

        public event EventHandler CloseProgressBarEvent;
        public event EventHandler CloseWndAndShowGameView;
        public ICommand PlusCount { get; set; }
        …………
        public MainViewModel()
        {
            …………
            this.PlusCount = new DelegateCommand(this.plusCount);
            …………
        }

        private void plusCount()
        {
            …………
            var dispatcher = App.Current.MainWindow.Dispatcher;
            dispatcher.BeginInvoke((Action)delegate () { CloseProgressBarEvent?.Invoke(this, tmpArgs); });
            Task.Factory.StartNew(() =>
            {
                ShowValue = 0;
                System.Threading.Thread.Sleep(2000);
                while (ShowValue < 100)
                {
                    ShowValue += 1;
                    System.Threading.Thread.Sleep(100);
                    tmpArgs._message = $"the program precent is {ShowValue.ToString()}";
                    dispatcher.BeginInvoke((Action)delegate () { CloseProgressBarEvent?.Invoke(this, tmpArgs); });
                }
                dispatcher.Invoke((Action)delegate () { CloseWndAndShowGameView?.Invoke(this, new EventArgs()); });
            });
        }

這裡關鍵的程式碼就是var dispatcher = App.Current.MainWindow.Dispatcher獲取當前視窗執行緒,讓窗體執行緒觸發EventHandler的操作,下面來看看EventHandler 的程式碼:

        public MainWindow()
        {
            InitializeComponent();
            var vm = new MainViewModel();
            vm.CloseProgressBarEvent += Vm_CloseProgressBarEvent;
            vm.CloseWndAndShowGameView += Vm_CloseWndAndShowGameView; ;
            this.DataContext = vm;
        }
        private void Vm_CloseWndAndShowGameView(object sender, System.EventArgs e)
        {
            GameView newWnd = new GameView();
            newWnd.Show();
            this.Close();
        }
        private void Vm_CloseProgressBarEvent(object sender, System.EventArgs e)
        {
            MyEventArgs tmp = e as MyEventArgs;
            this.textBlock.Text = tmp._message;
        }

大家先別吐槽,這event是有點粗暴,但是,能讓你一眼就看懂的程式碼,也就這樣了,對,就是這麼簡單粗暴。直接的使用this來操作UI介面。
這裡也使用到了Task.Factory,Dispatcher的非同步,同步方法,這就是我們一般執行緒互動提到的方法。

原始碼下載

最後,原始碼在這裡Dispatcher
最近發現CSDN資源已經不能免費了,在這裡補充百度雲的連結:Dispatcher

本文最後修改時間:2017年8月12日16:10:11。