DispatcherHelper
通常,WPF 應用程序從兩個線程開始:一個用於處理呈現,
一個用於管理 UI。呈現線程有效地隱藏在后台運行,而 UI 線程則接收輸入、處理事件、繪制屏幕
以及運行應用程序代碼。所以我們的大多數操作都會在UI線程中執行,同時它也處理繪制屏幕,如果我們
的一個操作相當耗時,那么它就沒有機會處理繪制屏幕,此時我們是不能夠拖動窗口的,也就是通常
說的屏幕卡住了。
Dispatcher,DispatcherObject,DependencyObject
Dispatcher即調度器,我們可以把UI線程看作CPU,我們的每個操作就是指令,指令發送到CPU處理,
CPU同時只能處理一個指令,當指令非常多時,優先級高的指令要先處理,低的可以稍后處理,Dispatcher
就是用於處理這些操作的,它將我們的操作根據優先級排隊,等待UI線程來處理。
在WPF中,所有的WPF對象都派生自DispatcherObject,
DispatcherObject暴露了Dispatcher屬性用來取得創建對象線程對應的Dispatcher。
鑒於線程親緣性,DispatcherObject對象只能被創建它的線程所訪問,
其他線程修改DispatcherObject需要取得對應的Dispatcher,
調用Invoke或者BeginInvoke來投入任務。
一個UI線程至少有一個Dispatcher來建立消息泵處理任務,一個Dispatcher只能對應一個UI線程。
WPF中的控件都是主UI線程創建的,也就是只有主UI線程可以訪問他們,如果是其他線程訪問控件
那么就會報錯
Task.Factory.StartNew(() =>
{
btn.Content = "HelloWorld";
});
解決辦法就是在主線程中訪問他們,或是
Task.Factory.StartNew(() =>
{
//耗時操作
Thread.Sleep(5000);
//最后結果通過主線程更新到控件
btn.Dispatcher.Invoke(() =>
{
btn.Content = "HelloWorld";
});
});
在WPF中,Dispatcher,DispatcherObject和DependencyObject決定了一個對象的線程親緣性,這里提供一個方便查看源代碼的網址
我們首先查看Dispatcher的源碼,發現兩個比較重要的方法和一個靜態屬性
Dispatcher
|____CurrentDispatcher
|____CheckAccess()
|____VerifyAccess()
CurrentDispatcher獲取當前線程的Dispatcher,如果當前線程沒有Dispatcher,那么就創建一個
CheckAccess()方法用於判斷,當前Dispatcher所屬的線程是不是當前
線程
VerifyAccess()其實就是調用的CheckAccess,如果Dispatcher不屬於當前線程,那么就報異常,這就是上面圖片所示的異常
我們在來看DispatcherObject,看到一個構造函數,2個方法和一個屬性
DispatcherObject
|____DispatcherObject
|____Dispatcher
|____CheckAccess()
|____VerifyAccess()
當創建一個DispatcherObject對象的時候,會給這個對象分配一個當前線程的Dispatcher,其他兩個方法也就是對Dispatcher方法的封裝
好,最后我們來看DependencyObject,看到兩個重要的方法GetValue,SetValue
DependencyObject
|____SetValue(DependencyProperty dp,object value)
|____GetValue(DependencyProperty dp)
這兩個方法中的的第一句就是this.VerifyAccess(),而DependencyObject是直接繼承自DispatcherObject的,其實就是使用的Dispatcher的VerifyAccess,到此
WPF對象的線程親緣性已經很明了了,所以,一個對象只能被它所創建的線程所訪問。
Binding的源在多線程中怎么處理
網上比較好的文章
我的理解
我說下我的理解,我們在ViewModel中更新數據都要觸發OnPropertyChanged事件,這個事件其實是被
一個叫做PropertyChangedEventManager
的家伙訂閱的,它又會去觸發相應控件的OnPropertyChanged事件
,都是事件,事件也是方法,在哪個線程促發的事件,就在哪個線程處理,照這么說,我們在非UI線程
更新Model的屬性,那么對應的會在非UI線程處理與之相綁定的控件,這里就會報如上的錯誤。我們做個
實驗來測試下是不是這樣的。
實驗
界面上顯示一個Teacher的基本數據,Name,Age,還有所管理的學生Student,我們在非UI線程做
以下操作
- 修改Teacher的屬性
- 增加學生數
- 修改最后一名學生的姓名
Teacher.cs
public class Teacher : ObservableObject
{
private string _name;
private int _age;
private ObservableCollection<Student> _students;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
RaisePropertyChanged(() => Name);
}
}
public int Age
{
get
{
return _age;
}
set
{
_age = value;
RaisePropertyChanged(() => Age);
}
}
public ObservableCollection<Student> Students
{
get
{
return _students;
}
set
{
_students = value;
RaisePropertyChanged(() => Students);
}
}
}
Student.cs
public class Student : ObservableObject
{
private string _name;
private int _age;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
RaisePropertyChanged(() => Name);
}
}
public int Age
{
get
{
return _age;
}
set
{
_age = value;
RaisePropertyChanged(() => Name);
}
}
}
MainView.xaml
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Teacher.Name}"></TextBlock>
<TextBlock Text="{Binding Teacher.Age}"></TextBlock>
<ListView ItemsSource="{Binding Teacher.Students}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name}"></GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding Age}"></GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
<StackPanel Grid.Column="1">
<Button Content="改變教師名稱" Command="{Binding ChangeTeacherNameCommand}"></Button>
<Button Content="增加學生" Command="{Binding AddStudentCommand}"></Button>
<Button Content="改變最后一名學生名稱" Command="{Binding ChangeLastStudentNameCommand}"></Button>
</StackPanel>
</Grid>
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
private Teacher _teacher;
public Teacher Teacher
{
get
{
return _teacher;
}
set
{
_teacher = value;
RaisePropertyChanged(() => Teacher);
}
}
public RelayCommand ChangeTeacherNameCommand
{
get; set;
}
public RelayCommand AddStudentCommand
{
get; set;
}
public RelayCommand ChangeLastStudentNameCommand
{
get; set;
}
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel()
{
Teacher = new Teacher()
{
Name = "LaoZhao",
Age = 30,
Students = new ObservableCollection<Student>()
{
new Student()
{
Name="LaoZhange",
Age = 18
}
}
};
InitCommand();
}
private void InitCommand()
{
ChangeTeacherNameCommand = new RelayCommand(() =>
{
Task.Factory.StartNew(() =>
{
Teacher.Name = "MaYun";
});
});
AddStudentCommand = new RelayCommand(() =>
{
Task.Factory.StartNew(() =>
{
Teacher.Students.Add(new Student()
{
Name = "LaoLi",
Age = 25
});
});
});
ChangeLastStudentNameCommand = new RelayCommand(() =>
{
Task.Factory.StartNew(() =>
{
var student = Teacher.Students.LastOrDefault();
if (student != null)
{
student.Name = "TheLast";
}
});
});
}
}
最后發現,在非UI線程更新Teacher的姓名和Student的姓名是沒有問題的,那是因為WPF在后台強制
使用了UI線程,然而向集合中增加一個學生卻報錯~(我猜測WPF針對有些控件不會幫我們切換到UI主線程),為了避免這些情況,如果要更新Model的屬性,
請在UI線程中。但是我們都是在ViewModel中,並不知道任何一個控件,自然也沒有辦法拿到Dispatcher
好了,主角終於登場了DispatcherHelper
。
DispatcherHelper
DispatcherHelper的使用首先要初始化用來保存主線程的Dispatcher,這個Dispatcher即創建界面的
線程,哪個線程創建的控件,只能由那個線程才能訪問。所以我們一般在App的構造函數中初始化DispatcherHelper
public App()
{
DispatcherHelper.Initialize();
}
DispatcherHelper主要成員以下:
- UIDispatcher 屬性:當DispatcherHelper調用Initialize方法時,使用當前線程的Dispatcher
- Initialize 方法:初始化DispatcherHelper,並保存當前線程的Dispatcher
- CheckBeginInvokeOnUI 方法:如果當前線程為非UI線程,那么在UI線程異步執行該方法,如果為UI
線程,那么立即執行 - Reset 方法:重置Dispatcher為Null
- RunAsync 方法:在UI線程上異步執行
所以,剛才增加學生出錯的代碼,我們可以修改為
AddStudentCommand = new RelayCommand(() =>
{
Task.Factory.StartNew(() =>
{
DispatcherHelper.CheckBeginInvokeOnUI(() =>
{
Teacher.Students.Add(new Student()
{
Name = "LaoLi",
Age = 25
});
});
});
});
當然,直接調用那是最好,這里只是模擬非UI線程調用的情況。