呼叫線程無法存取此對象
在WPF、WinForm這些應用程序中,必需是UI線程才能控制窗體。如果像是下列的范例程序一樣,使用了非UI線程來控制窗體,那就會看到內容為「呼叫線程無法存取此對象,因為此對象屬於另外一個線程」的InvalidOperationException例外錯誤。
<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>
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] 線程
<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>
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
<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>
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例外錯誤。
<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>
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; } } }
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數據對象的功能。
<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>
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; } } }
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)); } } } }
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
<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>
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; } } }
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