最近在用MVVM模式寫應用的時候和線程打交道很多,也遇到一些問題,引起一些思考,這里和大家分享一下。
MVVM設計模式
**Model-View-ViewModel (MVVM) **在Windows平臺開發(fā)可以使用MVVM Light Toolkit進行,簡化開發(fā)過程,構建松耦合的應用程序。
MVVM Light通過:
- 依賴注入(DI)/控制反轉(IOC)的方式創(chuàng)建ViewModel
- 使用
Messenger
實現(xiàn)消息通知 - 使用
RelayCommand
替代事件處理程序
實現(xiàn)了View和ViewModel的分離關系。
客戶端上的多線程和調度
如今在移動客戶端上處理多線程以及線程間的通信顯得非常重要。在.Net平臺上構建應用程序和框架,多線程也是長談的話題,每個完整的應用都要啟動多個后臺線程并對他們進行管理,對于計算能力較低,資源受限的平臺,例如移動端設備和嵌入式設備,在這些平臺上處理好線程對UX的流暢顯得尤為重要。
Windows 10 Mobile平臺最早的版本W(wǎng)indows Phone7上,對于長列表的滾動流暢非常困難,但在其后的版本中,專門使用后臺線程處理,不在影響主線程,實現(xiàn)了滾動的流暢。本文中,我會回顧基于XAML編寫界面的程序中處理線程的一般方式。
線程
線程(Thread),在操作系統(tǒng)中被視為最小的執(zhí)行單位。每個程序至少都有一個主線程,新的線程可以在代碼中顯式啟動,多數(shù)情況下,新線程的啟動主要是為了執(zhí)行或者等待某個操作,也不會導致程序的其他部分被阻塞。耗時操作主要是計算密集型操作、磁盤I/O和網(wǎng)絡傳輸。由于這些操作在現(xiàn)代應用程序中非常常見,應用也日益多線程化。
同步和異步
在為Windows 10 UWP編寫的應用中,異步操作已經(jīng)是家常便飯了。例如,UWP中對文件的操作都是以異步的方式進行的。
下面是在WPF中同步讀取文件方式:
public string ReadFile(FileInfo file)
{
using (var reader = new StreamReader(file.FullName))
{
return reader.ReadToEnd();
}
}
而在UWP中,和上面效果等同的異步操作:
public async Task<string> ReadFile(IStorageFile file)
{
var content = await FileIO.ReadTextAsync(file);
return content;
}
我們注意到,這里多了兩個關鍵字await
和async
,這里要感謝微軟爸爸對C#的不斷完善,這兩個關鍵字使新的程序代碼的可讀性提高,方便了程序員編寫異步方法。
同步方式和異步方式的主要區(qū)別在于,同步方法如果正在讀取的文件較大,可能會阻塞主線程,導致UX變得糟糕。而異步的方法將在后臺線程處理。
線程間通信
當一個線程要和另一個線程通行時,例如訪問另一個線程的對象,我們需要采取一些防范措施。看下面的代碼:
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Change();
}
private async Task Change()
{
await Task.Run(() =>
{
this.Count.Text=DateTime.Now.ToLocalTime().ToString();
});
}
使用Task.Run()
方法新開一個線程,訪問一個XAML中的文本框控件的Text
屬性。
如果真的用這段代碼運行程序,點擊按鈕時,應用程序會崩潰退出。
我們來分析一下。在創(chuàng)建對象時,這個操作發(fā)生在調用構造函數(shù)的線程上。對于UI控件,創(chuàng)建控件對象的操作發(fā)生在主線程上,也可以說是UI線程。因此,所有UI控件都是屬于主線程的,當我們新開一個線程去訪問UI線程擁有的對象時就會發(fā)生跨線程訪問問題。這個操作會引發(fā)一個異常:
注意到,異常信息提示“應用程序調用一個已為另一線程整理的接口”。
那么問題來了,難道我們真的不能在非UI線程去更新UI控件么?
線程調度
要使代碼按照我們設想的運行,我們需要讓新線程通過聯(lián)系主線程,通過主線程的調度程序更新UI控件。將上面的代碼修改為:
await Task.Run(async () =>
{
await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
this.Count.Text = DateTime.Now.ToLocalTime().ToString();
});
});
注意到這里用到了Dispatcher
屬性
Dispatcher屬性摘要
獲取此對象所關聯(lián)的 CoreDispatcher。 CoreDispatcher 表示即便代碼由非 UI 線程發(fā)起也可訪問 UI 線程上的 DependencyObject 的設備。
這個屬性提供對其所有者調度程序的訪問。所以所有擁有這個屬性的對象,理論上都能提供跨線程訪問的服務。
這個屬性源自Windows.UI.Xaml.DependencyObject
對象,也就是說,所有有依賴屬性都繼承自這個對象,那么所有的UI控件都是可以通過Dispatcher
實現(xiàn)跨線程訪問的。
MVVM中的跨線程調度
當我們在ViewModel中執(zhí)行后臺線程操作時,情況有所不同。通常,VM不會去繼承前面提到的DependencyObject
,也就沒有Dispatcher
屬性。
當我們把一個UI控件的屬性綁定到VM中的一個屬性,我們的程序通過更改VM的屬性來改變UI控件屬性。注意到,我們在使用MVVM Light的時候,每個VM都要繼承一個叫做ViewModelBase
的對象,這個對象實現(xiàn)了INotifyPropertyChanged
接口,這個接口的方法引發(fā)PropertyChanged
事件,在這里實現(xiàn)通知UI屬性改變的效果。
當我們按先前的方法,新開一個線程,用這個線程去更改VM中綁定到UI控件屬性的屬性時,你會發(fā)現(xiàn)也引發(fā)了跨線程訪問異常,程序異常退出。
我們可以知道,即使我們通過數(shù)據(jù)綁定來實現(xiàn)這樣的目的也是不能實現(xiàn)的。
因此,我們需要一種方式來訪問主線程。在MVVM Light中,為我們實現(xiàn)了一種方式。當你的項目使用了MVVM Light之后,你嘗試在VM的類中輸入DispatcherHelper
,再用一下智能感知,你會發(fā)現(xiàn)在MVVM Light中存在這個對象。這個類所做的就是將主線程的調度程序保存在靜態(tài)屬性中,公開了讓我們跨線程訪問等一些實用方法。為了正常使用這些功能,我們需要在主線程初始化這個類,最好是在應用程序初始化時進行。
我們能夠想到,App.xaml.cs
這個文件的OnLaunched
方法是在程序開始運行是執(zhí)行的,我們要把DispatcherHelper.Initialize();
放在這個方法的最后以實現(xiàn)初始化,之后就能在VM中使用了。
DispatcherHelper.CheckBeginInvokeOnUI(() =>
{
//訪問VM的屬性
});
這里推薦使用他的CheckBeginInvokeOnUI
方法去執(zhí)行而不是他的UIDispatcher屬性。這個方法首先執(zhí)行檢查,檢查調用方是否已經(jīng)在主線程上,如果在就直接執(zhí)行后面的委托,如果不是就去執(zhí)行調度。
那這篇文章就到這里啦,希望大家在MVVM的使用中一切順利~
本文參考MVVM : Multithreading and Dispatching in MVVM Applications