[C#][WPF][Windows Form] 雙平台通用的跨執行緒存取UI

  • 10934
  • 0

[C#][WPF][Windows Form] 雙平台通用的跨執行緒存取UI

之前有整理一些程式碼要封入DLL檔,但是遇到一個問題,由於我碰到的程式碼事件主要是用來作UI呈現的,並沒有其他特別的目的,而程式的內容因為要處理比較久,我是採用多執行緒方式非同步處理,也就是說我的事件委派要存取UI,就一定要跨回到UI執行緒。然而WPF跟WinForm存取主執行緒的方式不一樣,一個是呼叫System.Windows.UIElement.Dispacher.Invoke/BeginInvoke,另外一個是System.Windows.Form.Control.Invoke/BeginInvoke來達成同步或非同步存取UI的擁有者執行序。問題是,我哪知道我這個DLL以後會給甚麼平台用,既然不知道我就只好在事件加入委派的時候才進行實作。但是既然只是作UI存取,這種已經非常確定的事情我幹嘛每次要用都要重新寫一次跨執行緒呢?於是我參考了一下WPF、WinForm、BackgroundWorker的作法,他們跨執行緒時似乎都是使用[SynchronizationContext類別]來存取主執行緒。

 

[完整範例程式碼下載:SynchronizationContextDemo.rar]

 

※全文歡迎轉載但請註明出處,謝謝。※

 

先示範一個一定會出錯的例子:


    {
        public Form1()
        {
            InitializeComponent();           
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            new Thread(new ThreadStart(() =>
            {
                this.Text = "Test";
            })) 
            { IsBackground = true }.Start();
        }
    }

不意外的就出現了InvalidOperationException例外狀況。

未命名 - 3

 

通常我們在這邊會改成:

 


        {
            new Thread(new ThreadStart(() =>
            {
                this.Invoke(new Action(() => { this.Text = "Test"; }));
            })) 
            { IsBackground = true }.Start();
        }

確實改成這樣會成功,但是第一段說過了,我們的目的是雙平台通用。

 

因此我先建立一個類別Adapter:


    {
        public static SynchronizationContext Dispacher { get; private set; }
        /// <summary>
        /// 請於UI執行緒呼叫此方法。
        /// </summary>
        public static void Initialize()
        {
            if (Adapter.Dispacher == null)
                Adapter.Dispacher = SynchronizationContext.Current;
        }
        /// <summary>
        /// 在 Dispatcher 關聯的執行緒上以同步方式執行指定的委派。
        /// </summary>
        public static void Invoke(SendOrPostCallback d,object state)
        {
            Dispacher.Send(d, state);
        }
        /// <summary>
        /// 在 Dispatcher 關聯的執行緒上以非同步方式執行指定的委派。
        /// </summary>
        public static void BeginInvoke(SendOrPostCallback d, object state)
        {
            Dispacher.Post(d, state);
        }
    }

會特別建立一個靜態類別是因為,UI執行緒就一條,如果每次要呼叫還要在主執行緒開個全域欄位存放SynchronizationContext執行個體不是很麻煩嗎?所以就乾脆寫個靜態類別來存放。

接著把Form1的程式碼改成:


    {
        public Form1()
        {
            InitializeComponent();
            //初始化Adapter讓他抓到UI執行緒的資料。
            Adapter.Initialize();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            new Thread(new ThreadStart(() =>
            {
                Adapter.Invoke(new SendOrPostCallback(o => { this.Text = "Test"; }), null);
            })) 
            { IsBackground = true }.Start();
        }
    }

成功了!

未命名 - 4

但是為了證明這個方法可以雙平台通用,因此多開了一個WPF視窗再用一次。


    {
        public Window_Main()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            new Thread(new ThreadStart(() =>
            {
                Adapter.Invoke(new SendOrPostCallback(
                    obj => 
                {
                    this.Title = "Test WPF";
                }), null);
            })) { IsBackground = true }.Start();
        }
    }

並且把Form1的建構函式改成:


        {
            InitializeComponent();
            //初始化Adapter讓他抓到UI執行緒的資料。
            Adapter.Initialize();
            new Window_Main().Show();
        }

F5執行成功,由於是同一個Process所以我們可以確定兩個視窗用的是同一個SynchronizationContext執行個體,並且證明WinForm抓到的UI執行緒也能給Window用,因為兩者是同一條。

未命名 - 5

分享