More Effective C# 39.了解 XAML 環境中的跨執行緒呼叫 More Effective C# 39.了解 XAML 環境中的跨執行緒呼叫(Understand Cross-Thread Calls in XAML environments)

這個做法在講解 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 它們的使用場景 與背後邏輯。