這個做法在講解 WinForm 與 WPF 中該如何處理跨執行緒操作,並了解如何正確處理 UI 控件的執行緒。
Windows Controls 是基於 COM Single-Threaded Apartment(STA)模型,要求所有對控件的操作必須和建立控件的執行緒相同。
下面這段 WinForm 程式碼建立了一個按鈕與文字輸入框,在按鈕中的程式碼建立新的 Task
想要將修改文字的任務轉移到另一個執行緒上,
但是當你按下按鈕後會拋出錯誤訊息 System.InvalidOperationException: '跨執行緒作業無效: 存取控制項 'textBox1' 時所使用的執行緒與建立控制項的執行緒不同。
,
是因為 Task
的緣故導致當初建立控件的執行緒與現在要修改控件的執行緒不同,才會拋出這個錯誤。
private void button1_Click(object sender, EventArgs e)
{
var task = new Task(ShowText);
task.Start();
}
private void AddText(string text)
{
ModifiedText(text);
}
private void ModifiedText(string text)
{
this.textBox1.Text += text + Environment.NewLine;
}
private void ShowText()
{
while (true)
{
AddText(Thread.CurrentThread.ManagedThreadId.ToString());
Thread.Sleep(1000);
}
}
要修正這個問題需要修改 AddText
方法,並用 Invoke 的方式來呼叫修改委派,注意這裡的 InvokeRequired 是用來檢查呼叫方是否與建立控件的執行緒為同一個執行緒,
如果不同的話就代表要用 Invoke 的方式修改控件,如果相同就直接修改就好。
private void AddText(string text)
{
if (this.InvokeRequired)
this.Invoke(ModifiedText, text);
else
ModifiedText(text);
}
但是這樣每次在處理非同步更新控件的時候都要寫這段檢查程式,所以可以寫一個擴充方法來簡化程式碼。 注意到 BeginInvoke 方法能夠讓委派以非同步的形式運作在與建立控件的相同執行緒上。
public static class ControlExtensions
{
public static void InvokeIfNeeded(this Control ctl, Action doit)
{
if (ctl.IsHandleCreated == false)
doit();
else if (ctl.InvokeRequired)
ctl.Invoke(doit);
else
doit();
}
public static void InvokeIfNeeded<T>(this Control ctl, Action<T> doit, T args)
{
if (ctl.IsHandleCreated == false)
throw new InvalidOperationException("Window handle for ctl has not been created");
else if (ctl.InvokeRequired)
ctl.Invoke(doit, args);
else
doit(args);
}
public static void InvokeAsync(this Control ctl, Action doit)
{
ctl.BeginInvoke(doit);
}
public static void InvokeAsync<T>(this Control ctl, Action<T> doit, T args)
{
ctl.BeginInvoke(doit, args);
}
}
使用上就可以減少撰寫的程式碼並增加可讀性。
private void AddText(string text)
{
this.InvokeAsync(() => ModifiedText(text));
}
Summary
這個做法重點在討論 WinForm 與 WPF 在非同步的環境下更新 UI 的問題,並介紹 Invoke、InvokeRequired、BeginInvoke 它們的使用場景 與背後邏輯。