[WPF] 跨线程控制窗体UI
呼叫线程无法存取此对象
在WPF、WinForm这些应用程序中,必需是UI线程才能控制窗体。如果像是下列的范例程序一样,使用了非UI线程来控制窗体,那就会看到内容为「呼叫线程无法存取此对象,因为此对象属于另外一个线程」的InvalidOperationException例外错误。
1 2 3 4 5 6 | < Window x:Class="WpfApplication1.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"> < TextBlock x:Name="TextBlock001" FontSize="72" /> </ Window > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | namespace WpfApplication1 { public partial class MainWindow : Window { // Fields private readonly System.Threading.Timer _timer = null ; private int _count = 0; // Constructors public MainWindow() { // Base InitializeComponent(); // Timer _timer = new System.Threading.Timer( this .Timer_Ticked, null , 0, 100); } // Handlers private void Timer_Ticked(Object stateInfo) { _count++; this .TextBlock001.Text = _count.ToString(); } } } |
使用Dispatcher对象跨线程
非UI线程如果要控制窗体,必须要将控制窗体的程序逻辑封装成为委派,再将这个委派提交给UI线程去执行,藉由这个流程非UI线程就能够跨线程控制窗体。而在WPF应用程序中,非UI线程可以透过WPF提供的Dispatcher对象来提交委派。
参考数据:
MSDN - 使用 Dispatcher 建置响应性更佳的应用程序
昏睡领域 - [Object-oriented] 线程
1 2 3 4 5 6 | < Window x:Class="WpfApplication2.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"> < TextBlock x:Name="TextBlock001" FontSize="72" /> </ Window > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | namespace WpfApplication2 { public partial class MainWindow : Window { // Fields private readonly System.Threading.Timer _timer = null ; private int _count = 0; // Constructors public MainWindow() { // Base InitializeComponent(); // Timer _timer = new System.Threading.Timer( this .Timer_Ticked, null , 0, 100); } // Handlers private void Timer_Ticked(Object stateInfo) { _count++; Action methodDelegate = delegate () { this .TextBlock001.Text = _count.ToString(); }; this .Dispatcher.BeginInvoke(methodDelegate); } } } |
使用SynchronizationContext类别跨线程
在WPF应用程序中可以透过WPF提供的Dispatcher对象来完成跨线程工作,而在WinForm应用程序中则是需要透过WinForm提供的Invoke方法、BeginInvoke方法来完成跨线程工作。以此类推能得知Silverlight、Windows Phone等等应用程序平台,也会提供对应的解决方案来让开发人员完成跨线程工作。
每个应用程序平台都提供各自的跨线程解决方案这件事,对于开发共享函式库、框架的开发人员来说,就代表了要花不少的精力才能让函式库、框架适用于各种应用程序平台。为了整合不同平台跨线程的解决方案,在.NET Framework中将这些解决方案抽象化为统一的SynchronizationContext类别,再由各个应用程序平台去提供对应的实作。自此之后开发共享函式库、共享框架的开发人员,只要透过SynchronizationContext类别,就能完成适用于不同平台的跨线程功能。
必须值得一提的是,SynchronizationContext类别设计出来之后,应用范围已经不单单适用于跨线程控制窗体,在设计软件架构线程模型之类的场合,也会发现它的身影,非常推荐有兴趣的开发人员找相关的资料学习。
参考数据:
MSDN - 不可或缺的 SynchronizationContext
1 2 3 4 5 6 | < Window x:Class="WpfApplication3.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"> < TextBlock x:Name="TextBlock001" FontSize="72" /> </ Window > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | namespace WpfApplication3 { public partial class MainWindow : Window { // Fields private readonly System.Threading.SynchronizationContext _syncContext = null ; private readonly System.Threading.Timer _timer = null ; private int _count = 0; // Constructors public MainWindow() { // Base InitializeComponent(); // SyncContext _syncContext = System.Threading.SynchronizationContext.Current; // Timer _timer = new System.Threading.Timer( this .Timer_Ticked, null , 0, 100); } // Handlers private void Timer_Ticked(Object stateInfo) { _count++; System.Threading.SendOrPostCallback methodDelegate = delegate ( object state) { this .TextBlock001.Text = _count.ToString(); }; _syncContext.Post(methodDelegate, null ); } } } |
跨线程Binding数据对象
在WPF应用程序中提供了Binding数据对象的功能,透过这个功能就能将数据对象的属性直接呈现在窗体上。而数据对象如果有实作INotifyPropertyChanged接口、INotifyCollectionChanged接口...等等更新通知接口,就可以透过事件的方式用来通知资料内容更新,例如说:INotifyPropertyChanged接口就是藉由PropertyChanged事件来通知数据内容更新。
Binding功能会去处理这些数据内容更新事件,并且在收到这些事件之后去取得数据内容来更新窗体。而也因为Binding功能会去更新窗体,所以引发这些通知事件的线程必须是UI线程,这样才能让整个Binding功能正常运作,不会产生「呼叫线程无法存取此对象,因为此对象属于另外一个线程」的InvalidOperationException例外错误。
1 2 3 4 5 6 | < Window x:Class="WpfApplication4.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"> < TextBlock x:Name="TextBlock001" FontSize="72" Text="{Binding Path=Count}" /> </ Window > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | namespace WpfApplication4 { public partial class MainWindow : Window { // Fields private readonly System.Threading.SynchronizationContext _syncContext = null ; private readonly DataObject _dataObject = null ; // Constructors public MainWindow() { // Base InitializeComponent(); // SyncContext _syncContext = System.Threading.SynchronizationContext.Current; // DataObject _dataObject = new DataObject(); _dataObject.SetSynchronizationContext(_syncContext); // DataContext this .DataContext = _dataObject; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | namespace WpfApplication4 { public class DataObject : INotifyPropertyChanged { // Fields private readonly System.Threading.Timer _timer = null ; private System.Threading.SynchronizationContext _syncContext = null ; private int _count = 0; // Constructors public DataObject() { // Timer _timer = new System.Threading.Timer( this .Timer_Ticked, null , 0, 100); } // Properties public int Count { get { return _count; } set { _count = value; this .OnPropertyChanged( "Count" ); } } // Methods public void SetSynchronizationContext(System.Threading.SynchronizationContext syncContext) { // SyncContext _syncContext = syncContext; } // Handlers private void Timer_Ticked(Object stateInfo) { this .Count++; } // Events public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged( string name) { System.Threading.SendOrPostCallback methodDelegate = delegate ( object state) { var handler = this .PropertyChanged; if (handler != null ) { handler( this , new PropertyChangedEventArgs(name)); } }; _syncContext.Post(methodDelegate, null ); } } } |
前一个跨线程Binding数据对象范例中,做为数据对象的DataObject对象,设计上很理想的在数据对象内部透过SynchronizationContext类别完成跨线程的工作。而在真实的开发环境中,数据对象常常是由另外一个系统所提供、并且无法改写(也不应该改写,因为改写代表将显示功能污染进其他系统),这时可以套用装饰者模式(Decorator Pattern)的「精神」,来完成跨线程Binding数据对象的功能。
1 2 3 4 5 6 | < Window x:Class="WpfApplication5.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"> < TextBlock x:Name="TextBlock001" FontSize="72" Text="{Binding Path=Count}" /> </ Window > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | namespace WpfApplication5 { public partial class MainWindow : Window { // Fields private readonly System.Threading.SynchronizationContext _syncContext = null ; private readonly DataObject _dataObject = null ; private readonly DataObjectDecorator _dataObjectDecorator = null ; // Constructors public MainWindow() { // Base InitializeComponent(); // SyncContext _syncContext = System.Threading.SynchronizationContext.Current; // DataObject _dataObject = new DataObject(); // DataObjectDecorator _dataObjectDecorator = new DataObjectDecorator(_dataObject); _dataObjectDecorator.SetSynchronizationContext(_syncContext); // DataContext this .DataContext = _dataObjectDecorator; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | namespace WpfApplication5 { public class DataObject : INotifyPropertyChanged { // Fields private readonly System.Threading.Timer _timer = null ; private int _count = 0; // Constructors public DataObject() { // Timer _timer = new System.Threading.Timer( this .Timer_Ticked, null , 0, 100); } // Properties public int Count { get { return _count; } set { _count = value; this .OnPropertyChanged( "Count" ); } } // Handlers private void Timer_Ticked(Object stateInfo) { this .Count++; } // Events public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged( string name) { var handler = this .PropertyChanged; if (handler != null ) { handler( this , new PropertyChangedEventArgs(name)); } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | namespace WpfApplication5 { public class DataObjectDecorator : INotifyPropertyChanged { // Fields private readonly DataObject _dataObject = null ; private System.Threading.SynchronizationContext _syncContext = null ; // Constructors public DataObjectDecorator(DataObject dataObject) { // DataObject _dataObject = dataObject; _dataObject.PropertyChanged += this .DataObject_PropertyChanged; } // Properties public int Count { get { return _dataObject.Count; } set { _dataObject.Count = value; } } // Methods public void SetSynchronizationContext(System.Threading.SynchronizationContext syncContext) { // SyncContext _syncContext = syncContext; } // Handlers private void DataObject_PropertyChanged( object sender, PropertyChangedEventArgs e) { System.Threading.SendOrPostCallback methodDelegate = delegate ( object state) { this .OnPropertyChanged(e.PropertyName); }; _syncContext.Post(methodDelegate, null ); } // Events public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged( string name) { var handler = this .PropertyChanged; if (handler != null ) { handler( this , new PropertyChangedEventArgs(name)); } } } } |
跨线程Binding数据对象(.NET 3.5之后、包含.NET3.5)
上列套用装饰者模式(Decorator Pattern)的「精神」,来完成跨线程Binding数据对象的功能,其实要加入的唯一功能,就是将INotifyPropertyChanged接口的PropertyChanged事件,由非UI线程转换为UI线程来通知数据内容更新。这样的设计方式在对象种类少、对象属性不多的情景是可行的,但当对象属性多的场合,例如说有50个对象属性,那套用装饰者模式就必须要装饰出50个对象属性,这听起来光是打字工作量就会让人崩溃,一整个是很不符合人性的设计。
最近经由老狗大大 (http://www.dotblogs.com.tw/sanctuary/)的提点,发现在.NET3.5之后、包含.NET3.5,在Binding数据对象的设计上,有了一些新的变更。其中一个变更就是在Binding数据对象的功能中,非UI线程所引发的资料内容更新事件,在背景会被转换为UI线程去执行。经由这样的特性,开发人员就不需要硬套装饰者模式来建立转换线程的数据对象,直接使用数据对象原生的线程就可以,这样能够减低程序对象的复杂度、并且大幅提升开发的效率。
但要特别说的是,Binding功能这个跨线程的特性,虽然经由下列的范例程序验证是能够正常运作的,但在网络上或是MSDN中没有看到相关的技术文件(或是我没找到@@)。开发人员在使用这个特性做为设计依据时,必须要小心斟酌的使用。
参考数据:
WPF, Data Binding & Multithreading
1 2 3 4 5 6 | < Window x:Class="WpfApplication6.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"> < TextBlock x:Name="TextBlock001" FontSize="72" Text="{Binding Path=Count}" /> </ Window > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | namespace WpfApplication6 { public partial class MainWindow : Window { // Fields private readonly DataObject _dataObject = null ; // Constructors public MainWindow() { // Base InitializeComponent(); // DataObject _dataObject = new DataObject(); // DataContext this .DataContext = _dataObject; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | namespace WpfApplication6 { public class DataObject : INotifyPropertyChanged { // Fields private readonly System.Threading.Timer _timer = null ; private int _count = 0; // Constructors public DataObject() { // Timer _timer = new System.Threading.Timer( this .Timer_Ticked, null , 0, 100); } // Properties public int Count { get { return _count; } set { _count = value; this .OnPropertyChanged( "Count" ); } } // Handlers private void Timer_Ticked(Object stateInfo) { this .Count++; } // Events public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged( string name) { var handler = this .PropertyChanged; if (handler != null ) { handler( this , new PropertyChangedEventArgs(name)); } } } } |
原始码下载
原始码下载:ThreadBindingDemo.rar
期許自己~
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?