利刃 MVVMLight 8:DispatchHelper在多線程和調度中的使用
在應用程序中,線程可以被看做是應用程序的一個較小的執行單位。每個應用程序都至少擁有一個線程,我們稱為主線程,這是在啟動時調用應用程序的主方法時由操作系統分配啟動的線程。
當調用和操作主線程的時候,該操作將動作添加到一個隊列中。每個操作均按照將它們添加到隊列中的順序連續執行,但是可以通過為這些動作指定優先級來影響執行順序,而負責管理此隊列的對象稱之為線程調度程序。
在很多情況下,我們啟動新的線程主目的是執行操作(或等待某個操作的結果),而不會導致應用程序的其余部分被阻塞。密集型計算操作、高並發I/O操作等都是這種情況,所以現在的復雜應用程序日益多線程化了。
當我們啟動一個應用程序並創建對象時,就會調用構造函數方法所在的線程,對於 UI 元素,在加載 XAML 文檔時,XAML 分析器會創建基於這些UI元素的對象。所以所有的對象(包括UI元素)的創建都歸屬於當前的主線程,當然也只有主線程可以訪問他們。
但在實際情況中,有很多情況是要假手其他線程來處理的。
比如在一個長交互中,我們可能需要而外的線程來處理復雜的執行過程,以免造成線程阻塞,給用戶界面卡死的錯覺。
比如下面這個例子,我們使用委托的方式模擬用戶執行數據創建的操作:
調用CreateUserInfoHelper幫助類 和 執行 CreateProcess方法 的代碼如下:
1 UserParam up = new UserParam() { UserAdd = txtUserAdd.Text, UserName = txtUserName.Text, UserPhone = txtUserPhone.Text, UserSex = txtUserSex.Text };2 CreateUserInfoHelper creatUser = new CreateUserInfoHelper(up); 3 creatUser.CreateProcess += new EventHandler<CreateUserInfoHelper.CreateArgs>(CreateProcess); //註冊事件 4 creatUser.Create(); 5 processPanel.Visibility = Visibility.Visible;
1 private void CreateProcess(objectsender, CreateUserInfoHelper.CreateArgs args)//響應時間執行 2 { 3 processBar.Value = args.process; 4 processInfo.Text = String.Format("創建進度:{0}/100",args.process); 5 if (args.isFinish) 6 { 7 if (args.userInfo != null) 8 { 9 ObservableCollection<UserParam> data = (ObservableCollection<UserParam>)dg.DataContext; 10 data.Add(args.userInfo); 11 dg.DataContext = data; 12 } 13 processPanel.Visibility = Visibility.Hidden; 14 ClearForm(); 15 } 16 }
CreateUserInfoHelper幫助類代碼如下:
1 public class CreateUserInfoHelper 2 { 3 //執行進度事件(響應註冊的事件) 4 public event EventHandler<CreateArgs> CreateProcess; 5 6 //待創建信息 7 public UserParam up { get; set; } 8 9 public CreateUserInfoHelper(UserParam _up) 10 { 11 up = _up; 12 } 13 14 public void Create() 15 { 16 Thread t = new Thread(Start);//拋出一個行線程 17 t.Start(); 18 } 19 20 private void Start() 21 { 22 try 23 { 24 //ToDo:編寫創建用戶的DataAccess代碼 25 for (Int32 idx = 1; idx <= 10; idx++) 26 { 27 CreateProcess(this, new CreateArgs() 28 { 29 isFinish = ((idx == 10) ? true : false), 30 process = idx * 10, 31 userInfo =null 32 }); 33 Thread.Sleep(1000); 34 } 35 36 CreateProcess(this, new CreateArgs() 37 { 38 isFinish = true, 39 process = 100, 40 userInfo =up 41 }); 42 } 43 catch (Exception ex) 44 { 45 CreateProcess(this, new CreateArgs() 46 { 47 isFinish = true, 48 process = 100, 49 userInfo = null 50 }); 51 } 52 } 53 54 /// <summary> 55 /// 創建步驟反饋參數 56 /// </summary> 57 public class CreateArgs : EventArgs 58 { 59 /// <summary> 60 /// 是否創建結束 61 /// </summary> 62 public Boolean isFinish { get; set; } 63 /// <summary> 64 /// 進度 65 /// </summary> 66 public Int32 process { get; set; } 67 /// <summary> 68 /// 處理後的用戶信息 69 /// </summary> 70 public UserParam userInfo { get; set; } 71 } 72 }
目的很簡單:就是在創建用戶信息的時候,使用另外一個線程執行創建工作,最後將結果呈現在試圖列表上,而在這個創建過程中會相應的呈現進度條。
來看下效果:
立馬報錯了,原因很簡單,在創建對象時,該操作發生在調用CreateUserInfoHelper幫助類方法所在的線程中。
對於 UI 元素,在加載 XAML 文檔時,XAML 分析器會創建對象。所有這一切都在主線程上進行。因此,所有這些 UI 元素都屬於主線程,這也通常稱為 UI 線程。
當先前代碼中的後臺線程嘗試修改 UI主線程的元素 屬性時,則會導致非法的跨線程訪問。因此會引發異常。
解決辦法就是去通知主線程來處理UI, 通過向主線程的Dispatcher隊列註冊工作項,來通知UI線程更新結果。
Dispatcher提供兩個註冊工作項的方法:Invoke 和 BeginInvoke。
這兩個方法均調度一個委托來執行。Invoke 是同步調用,也就是說,直到 UI 線程實際執行完該委托它才返回。BeginInvoke是異步的,將立即返回。
所以我們修改上面的代碼如下:
1 private void CreateProcess(object sender, CreateUserInfoHelper.CreateArgs args) 2 { 3 this.Dispatcher.BeginInvoke((Action)delegate() 4 { 5 processBar.Value = args.process; 6 processInfo.Text = String.Format("創建進度:{0}/100",args.process); 7 if (args.isFinish) 8 { 9 if (args.userInfo != null) 10 { 11 ObservableCollection<UserParam> data = (ObservableCollection<UserParam>)dg.DataContext; 12 data.Add(args.userInfo); 13 dg.DataContext = data; 14 } 15 processPanel.Visibility = Visibility.Hidden; 16 ClearForm(); 17 } 18 }); 19 }
結果如下:
實現異步執行的結果。
MVVM 應用程序中的調度
當從 ViewModel 執行後臺操作時,情況略有不同。通常,ViewModel 不從 DispatcherObject 繼承。它們是執行 INotifyPropertyChanged 接口的 Plain Old CLR Objects (POCO)。
因為 ViewModel 是一個 POCO,它不能訪問 Dispatcher 屬性,因此我需要通過另一種方式來訪問主線程,以將操作加入隊列中。這是 MVVM Light DispatcherHelper 組件的作用。
實際上,該類所做的是將主線程的調度程序保存在靜態屬性中,並公開一些實用工具方法,以便通過便捷且一致的方式訪問。為了實現正常功能,需要在主線程上初始化該類。
最好應在應用程序生命周期的初期進行此操作,使應用程序一開始便能夠訪問這些功能。通常,在 MVVM Light 應用程序中,DispatcherHelper 在 App.xaml.cs 中進行初始化,App.xaml.cs 是定義應用程序啟動類的文件。在 Windows Phone 中,在應用程序的主框架剛剛創建之後,在 InitializePhoneApplication 方法中調用 DispatcherHelper.Initialize。在 WPF 中,該類是在 App 構造函數中進行初始化的。在 Windows 8 中,在窗口激活之後便立刻在 OnLaunched 中調用 Initialize 方法。
完成了對 DispatcherHelper.Initialize 方法的調用後,DispatcherHelper 類的 UIDispatcher 屬性包含對主線程的調度程序的引用。相對而言很少直接使用該屬性,但如果需要可以這樣做。但最好使用 CheckBeginInvokeOnUi 方法。此方法將委托視為參數。
所以將上述代碼改裝程:
View代碼(學過Bind和Command之後應該很好理解下面這段代碼,沒什麽特別的):
1 <Grid> 2 <Grid.Resources> 3 <Style TargetType="{x:Type Border}" x:Key="ProcessBarBorder"> 4 <Setter Property="BorderBrush" Value="LightGray" ></Setter> 5 <Setter Property="BorderThickness" Value="1" ></Setter> 6 <Setter Property="Background" Value="White" ></Setter> 7 </Style> 8 </Grid.Resources> 9 10 <!-- 延遲框 --> 11 <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" > 12 <Border Style="{StaticResource ProcessBarBorder}" Padding="5" Visibility="{Binding IsWaitingDisplay,Converter={StaticResource boolToVisibility}}" Panel.ZIndex="999" HorizontalAlignment="Center" VerticalAlignment="Center" Height="50"> 13 <StackPanel Orientation="Vertical" VerticalAlignment="Center" > 14 <ProgressBar Value="{Binding ProcessRange}" Maximum="100" Width="400" Height="5" ></ProgressBar> 15 <TextBlock Text="{Binding ProcessRange,StringFormat=‘執行進度:\{0\}/100‘}" Margin="0,10,0,0" ></TextBlock> 16 </StackPanel> 17 </Border> 18 </Grid> 19 20 <StackPanel Orientation="Vertical" IsEnabled="{Binding IsEnableForm}" > 21 <StackPanel> 22 <DataGrid ItemsSource="{Binding UserList}" AutoGenerateColumns="False" CanUserAddRows="False" 23 CanUserSortColumns="False" Margin="10" AllowDrop="True" IsReadOnly="True" > 24 <DataGrid.Columns> 25 <DataGridTextColumn Header="學生姓名" Binding="{Binding UserName}" Width="100" /> 26 <DataGridTextColumn Header="學生家庭地址" Binding="{Binding UserAdd}" Width="425" > 27 <DataGridTextColumn.ElementStyle> 28 <Style TargetType="{x:Type TextBlock}"> 29 <Setter Property="TextWrapping" Value="Wrap"/> 30 <Setter Property="Height" Value="auto"/> 31 </Style> 32 </DataGridTextColumn.ElementStyle> 33 </DataGridTextColumn> 34 <DataGridTextColumn Header="電話" Binding="{Binding UserPhone}" Width="100" /> 35 <DataGridTextColumn Header="性別" Binding="{Binding UserSex}" Width="100" /> 36 </DataGrid.Columns> 37 </DataGrid> 38 </StackPanel> 39 40 <StackPanel Orientation="Horizontal" Margin="10,10,10,10"> 41 <StackPanel Orientation="Vertical" Margin="0,0,10,0" > 42 <StackPanel Orientation="Horizontal" Margin="0,0,0,5" > 43 <TextBlock Text="學生姓名" Width="80" ></TextBlock> 44 <TextBox Text="{Binding User.UserName}" Width="200" /> 45 </StackPanel> 46 <StackPanel Orientation="Horizontal" Margin="0,0,0,5"> 47 <TextBlock Text="學生電話" Width="80" ></TextBlock> 48 <TextBox Text="{Binding User.UserPhone}" Width="200" /> 49 </StackPanel> 50 <StackPanel Orientation="Horizontal" Margin="0,0,0,5"> 51 <TextBlock Text="學生家庭地址" Width="80"></TextBlock> 52 <TextBox Text="{Binding User.UserAdd}" Width="200"/> 53 </StackPanel> 54 <StackPanel Orientation="Horizontal" Margin="0,0,0,5" > 55 <TextBlock Text="學生性別" Width="80" ></TextBlock> 56 <TextBox Text="{Binding User.UserSex}" Width="200" /> 57 </StackPanel> 58 <StackPanel> 59 <Button Content="提交" Width="100" Command="{Binding AddRecordCmd}" ></Button> 60 </StackPanel> 61 </StackPanel> 62 </StackPanel> 63 64 </StackPanel> 65 </Grid>
ViewModel代碼:
(先初始化 DispatcherHelper,再調用 CheckBeginInvokeOnUI 方法來實現對UI線程的調度)
1 public class DispatcherHelperViewModel:ViewModelBase 2 { 3 /// <summary> 4 /// 構造行數 5 /// </summary> 6 public DispatcherHelperViewModel() 7 { 8 InitData(); 9 DispatcherHelper.Initialize(); 10 } 11 12 13 #region 全局屬性 14 15 private ObservableCollection<UserParam> userList; 16 /// <summary> 17 /// 數據列表 18 /// </summary> 19 public ObservableCollection<UserParam> UserList 20 { 21 get { return userList; } 22 set { userList = value; RaisePropertyChanged(() => UserList); } 23 } 24 25 private UserParam user; 26 /// <summary> 27 /// 當前用戶信息 28 /// </summary> 29 public UserParam User 30 { 31 get { return user; } 32 set { user = value; RaisePropertyChanged(()=>User); } 33 } 34 35 36 private Boolean isEnableForm; 37 /// <summary> 38 /// 是否表單可用 39 /// </summary> 40 public bool IsEnableForm 41 { 42 get { return isEnableForm; } 43 set { isEnableForm = value; RaisePropertyChanged(()=>IsEnableForm); } 44 } 45 46 private Boolean isWaitingDisplay; 47 /// <summary> 48 /// 是都顯示延遲旋轉框 49 /// </summary> 50 public bool IsWaitingDisplay 51 { 52 get{ return isWaitingDisplay; } 53 set{ isWaitingDisplay = value; RaisePropertyChanged(()=>IsWaitingDisplay);} 54 } 55 56 private Int32 processRange; 57 /// <summary> 58 /// 進度比例 59 /// </summary> 60 public int ProcessRange 61 { 62 get { return processRange; } 63 set { processRange = value; RaisePropertyChanged(()=>ProcessRange);} 64 } 65 66 #endregion 67 68 69 #region 全局命令 70 private RelayCommand addRecordCmd; 71 /// <summary> 72 /// 添加資源 73 /// </summary> 74 public RelayCommand AddRecordCmd 75 { 76 get 77 { 78 if (addRecordCmd == null) addRecordCmd = new RelayCommand(()=>ExcuteAddRecordCmd()); 79 return addRecordCmd; 80 } 81 set 82 { 83 addRecordCmd = value; 84 } 85 } 86 #endregion 87 88 89 #region 輔助方法 90 /// <summary> 91 /// 初始化數據 92 /// </summary> 93 private void InitData() 94 { 95 UserList = new ObservableCollection<UserParam>() 96 { 97 new UserParam(){ UserName="周傑倫", UserAdd="周傑倫地址", UserPhone ="88888888", UserSex="男" }, 98 new UserParam(){ UserName="劉德華", UserAdd="劉德華地址", UserPhone ="88888888", UserSex="男" }, 99 new UserParam(){ UserName="劉若英", UserAdd="劉若英地址", UserPhone ="88888888", UserSex="女" } 100 }; 101 User = new UserParam(); 102 IsEnableForm = true; 103 IsWaitingDisplay = false; 104 } 105 106 /// <summary> 107 /// 執行命令 108 /// </summary> 109 private void ExcuteAddRecordCmd() 110 { 111 UserParam up = new UserParam { UserAdd = User.UserAdd, UserName = User.UserName, UserPhone = User.UserPhone, UserSex = User.UserSex }; 112 CreateUserInfoHelper creatUser = new CreateUserInfoHelper(up); 113 creatUser.CreateProcess += new EventHandler<CreateUserInfoHelper.CreateArgs>(CreateProcess); 114 creatUser.Create(); 115 IsEnableForm = false; 116 IsWaitingDisplay = true; 117 } 118 119 /// <summary> 120 /// 創建進度 121 /// </summary> 122 /// <param name="sender"></param> 123 /// <param name="args"></param> 124 private void CreateProcess(object sender, CreateUserInfoHelper.CreateArgs args) 125 { 126 DispatcherHelper.CheckBeginInvokeOnUI(() => 127 { 128 if (args.isFinish) 129 { 130 if (args.userInfo != null) 131 { 132 UserList.Add(args.userInfo); 133 } 134 135 IsEnableForm = true; 136 IsWaitingDisplay = false; 137 } 138 else 139 { 140 ProcessRange = args.process; 141 } 142 }); 143 } 144 #endregion 145 146 }
結果如下:
示例代碼下載
轉載請註明出處,謝謝
利刃 MVVMLight 8:DispatchHelper在多線程和調度中的使用