又是一個看起來很簡單的問題。像下面這樣在Closing裡彈出個MessageBox確認一下不就行了?
public static void OnWindowClosing(object sender, CancelEventArgs e) { if (MessageBox.Show(string.Format("Are you sure to close the {0}?", (sender as Window).Title), "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No) { e.Cancel = true; } }
代碼簡單得不能再簡單了,而且試了一下可以達到目的,似乎是沒有什麼問題啊。但是很多代碼需要大量的測試才能發現問題。這個代碼就是其中之一。
在說明BUG之前,先給幾個信息,大家可以猜猜如何重現這個問題。
1.只有非模態的子窗體有這個BUG。主窗口是沒有問題的。
2.彈出MessageBox的時候,UI消息處理線程會被阻塞。
3.拋出的異常,是InvalidOperationException。
4.子窗體與主窗體有從屬關系,主窗體最小化時,子窗體也跟著最小化。
5.最後一個信息,已經是把BUG告訴大家了。就是Error Message,如下圖所示。
圖1. Exception信息
看了Error Message,應該都明白了。主窗體沒有問題,因為關主窗體時彈出消息框之後,根本沒有什麼UI操作可以最小化主窗體。(自己寫另一個程序去最小化這個窗體不在考慮范圍之內。)所以不會有這個BUG,但是為什麼子窗體有呢。
下面描述一個這個Bug的產生過程。
1.主窗體和子窗體都顯示出來。
2.點擊關閉按鈕關閉子窗體,此時會彈出消息框問要不要關。不去理這個消息框。
3.點擊任務欄上的主窗體,使主窗體最小化。這裡子窗體也最小化了。
4.再點任務欄上的主窗體,使主窗體還原。異常拋出。而且,即使handle了這個異常,這個子窗體也會變黑的。
可以發現,其實罪魁禍首是上面的第4條信息。操作系統在主窗體最小化時,自動最小化其子窗體,結果幫了倒忙。這種自動做事幫倒忙的事情應該還有不少。還發現過的一例就是在WPF Bug清單裡的RadioButton無法綁定的BUG,也是系統自動做事造成的。但是邏輯上來講,這麼做也的確是對的。
最後想辦法解決問題的,只能是我們自己。Exception的Message上說在窗體Closing的時候,不能做這做那,但是我們又要做,那怎麼辦呢?其實是我們的MessageBox阻塞了窗體Close的過程才有這個異常。那麼解決方案就出來,讓MessageBox不阻塞UI線程不就得了。代碼如下 :
public static void OnWindowClosingAdv(object sender, CancelEventArgs e) { Prevent Recursion#region Prevent Recursion if (new StackTrace().GetFrames().Any((frame) => { return frame.GetMethod().Name == "ConfirmClose"; })) return; #endregion e.Cancel = true; //Cancel every close at once. Window window = sender as Window; window.Dispatcher.BeginInvoke(new Action<Window>(ConfirmClose), window); } private static void ConfirmClose(Window window) { if (MessageBox.Show(string.Format("Are you sure to close the {0}?", window.Title), "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.Yes) { window.Close(); } }
注:其中的Prevent Recursion代碼段是為了防止遞歸調用的。完全可以用一個臨時變量代碼代替,而且不會出現硬編碼的字符串。分析調用棧只是為了突出代碼相對於原版的丑惡。^_^
這次還好,為了Fix系統的邏輯BUG,並沒有多寫多少代碼。