[WPF] 跨線程式控制制表單UI

來源:互聯網
上載者:User

呼叫線程無法存取此對象

在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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.