2014年9月14日 星期日

C# 跨執行緒作業無效: 存取控制項時所使用的執行緒與建立控制項的執行緒不同

最近寫程式時遇到一個問題,如題,但造成的原因是什麼呢?

以下是我寫的遇到問題的程式的程式碼片段Form1.cs的內容
完整的遇到問題之專案請到這裡下載


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Timers;
namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        MyLabelController Controller = new  MyLabelController();
        public Form1()
        {
            InitializeComponent();
        }
        public void label1_Hello()
        {
            label1.Text = "Hello";
        }
        public void label1_World()
        {
            label1.Text = "World";
        }
        private void button1_Click(object sender, EventArgs e)
        {
            Controller.HelloWorld(this);
        }
    }

    public class MyLabelController
    {
        Form1 Parent;
        public void HelloWorld(Form1 target)
        {
            Parent=target;
            Parent.label1_Hello();
            System.Timers.Timer t = new System.Timers.Timer(3000);
            t.Elapsed += this.Next;
            t.Enabled = true;
        }
        private void Next(Object obj, ElapsedEventArgs e)
        {
            Parent.label1_World();
        }
    }
}


這隻程式主要是透過按下按鈕,然後把label的值改成Hello,並在三秒後把值改成World,但是在這裡並不是由Form1對label的值進行修改,而是由Form1的成員Controller來對label的值進行修改,並且啟動Controller中的Timer來計時,並觸發二度改寫文字的事件,成員Controller的類別是屬於寫在同一個Namespace WindowsFormsApplication1的類別MyLabelController,至於為何我要這樣用? 只是單純想測試可不可以寫一個控制用成員,負責管理其他UI類的成員,只需要操控此控制用成員,此控制用成員能幫你去管理你的所有UI類成員,也就是當有數百個label類的UI成員的時候,成員Controller能夠依照你對它的操控,幫你去管理其他的label類的UI成員。

程式執行畫面大概像這樣:

而按下按鈕,之後會順利的顯示Hello,但是過了三秒後再跳出World的時候,就發生錯誤了

以上就是遇到的問題,或許大家會覺得我這種寫法脫褲子放屁,遇到問題活該,不過由於我剛學C#沒有很久,之前都是學Gnu的C與C++,所以目前還是在學習階段,所以我當遇到問題是學習,既然都遇到問題了,那至少要先把這個問題解決,再以別的方法實踐相同功能,作為C#的學習。

於是後來查了資料,此種問題,是因為我在class MyLabelController中宣告的Timers事件,實際上已經與Form1始於不同的執行續, 但是在Form1呼叫Controller的方法HelloWorld時,是以Form1的執行續互叫來執行的,所以顯示Hello時沒遇到錯誤,直到Controller的Timers事件Elapsed時,此時由於是由MyLabelController的事件處理觸發的事件,故執行續與Form1的執行續不相同,而為了安全起見,兩個不同執行續A、B,A無法控制位於B中的UI物件(控制項),B也無法無法控制位於A中的UI物件(控制項),所以發生了System.InvalidOperationException,造成錯誤。至於要如何修改呢?  在這裡我們必須用到委派(dalegate)的方式來處理

委派如同字面意思,在這裡就是讓Controller委託 Form1去做label1_World()這件事情,由於 Form1本身就是label1UI物件(控制項)得擁有者,由Controller委託 Form1來做,自然就不會遇到A無法控制位於B中的UI物件(控制項)的問題,至於要怎麼改呢?  程式碼如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Timers;
namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        MyLabelController Controller = new  MyLabelController();
        public delegate void mydalegate();
        public mydalegate md;

        public Form1()
        {
            InitializeComponent();
            md = new mydalegate(label1_World);
        }
        public void label1_Hello()
        {
            label1.Text = "Hello";
        }
        public void label1_World()
        {
            label1.Text = "World";
        }
        private void button1_Click(object sender, EventArgs e)
        {
            Controller.HelloWorld(this);
        }
    }

    public class MyLabelController
    {
        Form1 Parent;
        public void HelloWorld(Form1 target)
        {
            Parent=target;
            Parent.label1_Hello();
            System.Timers.Timer t = new System.Timers.Timer(3000);
            t.Elapsed += this.Next;
            t.Enabled = true;
        }
        private void Next(Object obj, ElapsedEventArgs e)
        {
            //Parent.label1_World();
            Parent.Invoke(Parent.md);
        }
    }
}
解釋一下下列的程式碼:
public delegate void mydalegate(); 由於我們要委託的對象方法為
public void label1_World(); 是屬於void,並且沒有參數傳入,所以這裡要把 mydalegate 定義成這樣:public delegate void mydalegate();
接下來,建立委派
public mydalegate md; 並且在Form1的建構子,明確指定此委派的對象為方法為 label1_World;

        public Form1()
        {
            InitializeComponent();
            md = new mydalegate(label1_World);
        }
最後,只要把Next中的程式,修改為

        private void Next(Object obj, ElapsedEventArgs e)
        {
            //Parent.label1_World();
            Parent.Invoke(Parent.md);    //委派Form1去執行md這個委派

        }
就能順利執行了,以下為程式執行結果


改寫後的程式碼,請點這裡下載