I. 緣起
最近我的一位客戶跟我抱怨說,自從安裝了我做的軟體,他們本來應該半夜2點自動關機的伺服器,會偶爾在第二天早晨發現系統仍然開著。他們懷疑是我的軟體的Bug,希望我能檢查一下。
沒錯,無法自動關機是一個很煩人的事情:對於個人來說這意味著費電,電腦的磨損,心情的不爽等等;對企業來說這有可能意味著巨大的經濟損失。
可是,我的程式只是一個簡單的資料處理程式,它怎麼可能罪惡到阻撓關機呢?——我剛剛聽到客戶的抱怨時,我也這麼想;可是當我做了一個非常簡單的程式實驗之後,我得到非常令人沮喪的結果:
(注意,這是在.Net Framework1.1下的結果。在2.0已經修正了這個問題)
1,開啟Vistual Studio.Net,建立一個C#(VB.Net也可以,以下代碼以C#為例)的Windows Form。
2,假設自動產生的Form叫做Form1,我們再添加一個Windows Form叫做Form2。
3,在Form1上添加一個按鈕,在其Click事件處理函數中寫下:
Form2 f = new Form2();
f.ShowDialog();
沒錯,就是這麼簡單的一個程式——2個Form,在點擊第一個Form的按鈕時,把第二個Form作為Modal Dialog(強制回應對話方塊?)顯示。
好,現在編譯,運行這個程式(不要在調試環境下),然後按下那個按鈕,使Form2顯示出來。
然後關機(或者重新啟動,或者Log off也可以)。
你會發現,Form2關閉了,可是Form1還留在那裡;而且無論你等多久,還沒有出現你期待的關機(或者重啟,Logoff)。
如果是我們人類遇到這種情況,很簡單——我們可以反覆多試幾次;經驗豐富的高手還可以開啟工作管理員,尋找有可能是原因的進程,將其Kill之;最後實在無奈了還可以按下電源開關……可是很遺憾,我們的客戶不可能安排一個工作人員每天半夜2點爬起來一一驗證他們的Server關掉了沒有。
所以,我必須找到原因。
II. 發現
對於這個問題,我承認最開始我的確疏忽了——我當時覺得,.Net的Modal Dialog每天有成千上萬的人在使用;如果顯示了Modal Dialog就無法關機,肯定早就被人注意到了;也就是說,網上肯定有數不清的文章講解這個問題的原因和解決方案,並且,如果MS認為這算是一個BUG,會在MSDN中說明,給出解決方案;如果他就是這麼設計的,那麼他會給出理由的。——總之,這一定是一個Well-known的現象。
可是我找了一個小時卻一無所獲(也許是我用的Keyword選得不好?),所以我只好承認我的孤陋寡聞。可是無論我是多麼無知,我卻處於必須為客戶解決問題的尷尬位置上,所以我至少硬著頭皮嘗試解決這個問題。
作為分析這個問題的前提,有些粗淺的基礎知識是必須知道的;為了照顧和我一樣的菜鳥,我先把它們寫在這裡:
Windows在結束一次會話(指關機,重新啟動或者Logoff,以下稱“關閉”)時,會向所有視窗發送WM_QUERYENDSESSION通知(Int值是0x11,10進位的17)來詢問應用程式的意見,在收到“同意”的回答之後才會繼續關閉。所謂“同意”指的是對於這條訊息返回TRUE(或Int的1)。反之,如果一個程式在收到WM_QUERYENDSESSION之後返回了FALSE(或Int的0),那麼系統就會中斷關閉的動作並停止向其他程式發送關閉的通知,系統也就不會關閉了。
我們看到,在MSDN上寫道:預設情況下,對於這條訊息DefWindowProc函數將返回TRUE。(換句話說,任何沒有專門響應這條訊息的視窗,預設都是同意關閉的)
在.Net Framework中,相當於Win32體系中DefWindowProc的,就是Form.WndProc方法(當然還有其各個父類的WndProc方法)。其原型為:
protected override void WndProc(ref Message m);
其中,m是一個Message structure,其中的Msg代表訊息的類型,而Result正是對這條訊息的返回結果。詳細請參考MSDN。我們所關心的就是,在m.Msg = 0x11的情況下,預設的Form.WndProc到底給m.Result賦的什麼值。
系統在給視窗發送WM_QUERYENDSESSION詢問程式的意見後,系統會用WM_ENDSESSION來通知關閉動作的結果(究竟是詢問完所有程式再決定結果,還是詢問一個通知一個,這一點上Win9X和2K系列有若干區別,有興趣的朋友可以參看MSDN)。WM_ENDSESSION是0x16(10進位22)。同時WM_CLOSE(0x10,10進位16)也請記下。
另外,還有一點可能是題外話了。我們注意到上述現象僅在顯示一個Modal Dialog的情形下發生,而如果是用Show建立的Modeless Dialog則沒有任何問題。那麼我們查閱MSDN關於Form.ShowDialog方法的參考資料,可以看到MSDN上有如下介紹,我們不妨也記下來:
與modeless form不同,modal dialog在被使用者關閉的時候,.Net Framework不會調用其Close方法,而是將其隱藏(Hide)起來。這樣當你想再次顯示這個對話方塊的時候,你不必重新建立執行個體。但是同時也正因此,你在不再使用它的時候,需要調用其Dispose方法來回收資源。
好了,有了上面的基礎,我們就可以分析了。
用ildasm之類的反編譯工具開啟Windows.Form.dll,找到Form.WndProc方法並順藤摸瓜,我們很快就能找到與關閉視窗相關的幾條訊息都交給了一個叫做WmClose的方法處理。
該方法原型如下:
private void WmClose(System.Windows.Forms.Message m);
該方法的最開始便通過get_Modal()判斷當前視窗是否為Modal Dialog,如果是Modal Dialog,只是簡單地檢查DialogResult等等,之後就返回了;而如果是Modeless的,則要進行驗證、調用每個字視窗的OnClosing方法,最後綜合得出結果。下面是用虛擬碼寫的一個簡化版本(和實際情況稍有出入):
//Modal對話方塊時
if (Form.IsModal)
{
if (Form.DialogResult == DialogResult.None)
{
Form.DialogResult = DialogResult.Cancel;
}
}
else //Modeless對話方塊時
{
//對於可取消得事件,都有這樣一個事件參數,將其Cancel property設為True即表明要取消該事件
CancelEventArgs flag;
//如果是Mdi容器
if (Form.IsMdiContainer)
{
//對於每個Mdi子視窗
foreach(childForm in Form.MdiChildren)
{
//調用其OnClosing方法,一旦有希望Cancel的方法就退出迴圈
childForm.OnClosing(flag);
if (flag.Cancel == true)
break;
}
}
//自己也要做一下關閉前的收尾工作
Form.OnClosing(flag);
//最後對於訊息的傳回值就是,如果有任何一個子視窗或本身表示Cancel,就返回0,大家全體通過了返回1
Msg.Result = flag.Cancel ? 0 : 1;
}
通過上面的代碼,我們可以清楚地看到,在視窗為Modal的情況下,WndProc對於Message的Result沒有做任何修改,那麼也就是預設的0。換句話說:在.Net Framework下,Modal對話方塊顯示的時候將無法關閉系統。
III. 解決
我前面已經說過,由於我孤陋寡聞,不知道這個按理說是Well-known的問題;因此雖然通過分析IL知道了這的確是.Net Framework的邏輯使然,卻仍然不知道這是MS故意這麼做的,還是有什麼特別的說法,或者是一個Bug。
不過,既然在C++中使用DialogBox宏產生的Modal Dialog是不會阻撓關機,MSDN上也說了DefWindowProc對WM_QUERYENDSESSION將返回TRUE,因此至少可以說.Net Framework 1.1下的Modal Dialog違背了以往的原則。
解決這個問題非常簡單:只需要重載WndProc方法,當Form被以Modal方式顯示,並且接收到WM_QUERYENDSESSION訊息時令訊息的傳回值等於1即可。
由於我們已經分析過預設的WndProc的IL代碼,因此我們可以確認這樣做不會有任何問題。
另外,在.Net Framework 2.0當中,已經對這個問題進行了修正。有興趣的朋友可以自己看一下2.0當中WndProc的IL代碼(嫌讀IL麻煩的直接用Reflector之類的吧)。
IL看得比較匆忙,若有錯漏之處,還請指正。