TIPS-About UI Thread Limitation
這是一個程式"中鳥"開發Windows Form要面對的問題...
Windows Form裡的Threading有些討厭的限制。菜鳥還處於天真無邪的Single Thread打天下階段,渾然不知它的險惡;而老鳥早就吃過苦頭,熟知要如何對付它,所以也不畏懼它的刁難。而剛開始學會在Windows Form中展現Multi-threading威力的中鳥,一起步多半要先闖過這一關。
用一個最簡單的例子來說明好: 我寫了一個Windows Form,放了一個label1一個button1, 在button1_Click事件另外建立一個Thread呼叫chgLabel(text)修改label1的Text屬性:
private void button1_Click(object sender, EventArgs e)
{
Thread trd = new Thread(new ThreadStart(jobBad));
trd.Start();
}
private void jobBad()
{
chgLabel("Wrong way.");
}
private void chgLabel(string text)
{
label1.Text = text;
}
程式碼很簡單,看起來應該沒什麼問題吧?
如果你這麼想,表示你在Windows Form的世界還涉世未深...
這段程式觸犯了Windows Form裡的一條鐵律: 在非UI Thread裡更動了UI的內容!
從沒用C++, Windows SDK角度探索Windows Application Model的我,並不知道它的細節原由,但歷經多次慘痛的經驗教訓,我學到一點: 如果你另外開了一條Thread,程式碼如會變更到Windows Form上的Control內容,請記得繞道而行。在上述的例子中,我們很明確地修改了Label的內容,但有時程式碼會在你不知情的情況下更動了UI元素。最典型的例子是DataGrid Bind到一個DataTable,而我們在另一條Thread中表面上只更改了DataTable的內容,並沒有修改UI上的Control;但由於Data Binding的關係,一更動DataTable就會間接觸發了修改DataGrid顯示的事件,進而違反了在非UI Thread修改UI內容的規則。
記得在.NET 1.1時代,違反這條規則並非絕對會出錯,而是偶爾冒出Null Reference Exception讓你丈二金剛摸不著腦袋,完全不知自己犯了什麼天條遭此報應。.NET 2.0比較仁道一點,會很明確地彈出以下的訊息:
Invalid Operation Exception:
Cross-thread operation not valid: Control 'label1' accessed from a thread other than the thread it was created on.
這張罰單把違規事由寫得清清楚楚,想當初由Null Reference去追出問題出在UI Thread限制就讓我吃盡苦頭,由.NET 2.0入門的朋友算是幸福多了。
Good! 知道問題後,要怎麼修改? 官方的標準答案是宣告一個deleate, 再用Invoke或BeginInvoke來觸發它,這樣可以強迫Windows切回UI Thread來執行程式
private void button1_Click(object sender, EventArgs e)
{
Thread trd = new Thread(new ThreadStart(jobGood));
trd.Start();
}
private void chgLabel(string text)
{
label1.Text = text;
}
delegate void ChgLabelHandler(string text);
private void jobGood()
{
this.Invoke(new ChgLabelHandler(chgLabel), "Correct way.");
}
還有一些進階的課題,例如: 某些Code可能被UI Thread呼叫,也可能被非UI Thread執行,所以可以用InvokeRequired來偵測,決定要直接變更UI內容或是使用Invoke,這裡就先不要搞得太複雜,以免嚇到大家。先記住一點,當你遇到"Cross-thread operation not valid",要知道發生了什麼事以及該怎麼辦,就夠了。