[WPF] 跨執行緒控制表單

摘要:[WPF] 跨執行緒控制表單

呼叫執行緒無法存取此物件

在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


期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。