Santé

为明天干杯!
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

阻碍关机的Modal Dialog [.Net 1.1]

Posted on 2006-01-04 19:06  smalldust  阅读(2776)  评论(3编辑  收藏  举报

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看得比较匆忙,若有错漏之处,还请指正。