场景:winform的程序中,有一个画面上放了一个Button,点击这个Button会调用.Net控件SaveFileDialog的ShowDialog方法。
场景很简单,但是碰到了这样一个有趣的问题:
在机器很慢的情况下,连续快速两次点击上述Button,会导致栈溢出异常(StackOverflowException)。
由于机器很慢的情况难以模拟且不能稳定重现,尝试在Button的点击事件中先用异步委托调一次SaveFileDialog.ShowDialog,然后再正常调用一次SaveFileDialog.ShowDialog,然后。。。问题重现了!
SaveFileDialog的基类CommonDialog代码如下:
1 public DialogResult ShowDialog(IWin32Window owner) 2 { 3 IntSecurity.SafeSubWindows.Demand(); 4 if (!SystemInformation.UserInteractive) 5 { 6 throw new InvalidOperationException(SR.GetString("CantShowModalOnNonInteractive")); 7 } 8 NativeWindow window = null; 9 IntPtr zero = IntPtr.Zero; 10 DialogResult cancel = DialogResult.Cancel; 11 try 12 { 13 if (owner != null) 14 { 15 zero = Control.GetSafeHandle(owner); 16 } 17 if (zero == IntPtr.Zero) 18 { 19 zero = UnsafeNativeMethods.GetActiveWindow(); 20 } 21 if (zero == IntPtr.Zero) 22 { 23 window = new NativeWindow(); 24 window.CreateHandle(new CreateParams()); 25 zero = window.Handle; 26 } 27 if (helpMsg == 0) 28 { 29 helpMsg = SafeNativeMethods.RegisterWindowMessage("commdlg_help"); 30 } 31 NativeMethods.WndProc d = new NativeMethods.WndProc(this.OwnerWndProc); 32 this.hookedWndProc = Marshal.GetFunctionPointerForDelegate(d); 33 IntPtr userCookie = IntPtr.Zero; 34 try 35 { 36 this.defOwnerWndProc = UnsafeNativeMethods.SetWindowLong(new HandleRef(this, zero), -4, d); 37 if (Application.UseVisualStyles) 38 { 39 userCookie = UnsafeNativeMethods.ThemingScope.Activate(); 40 } 41 Application.BeginModalMessageLoop(); 42 try 43 { 44 cancel = this.RunDialog(zero) ? DialogResult.OK : DialogResult.Cancel; 45 } 46 finally 47 { 48 Application.EndModalMessageLoop(); 49 } 50 return cancel; 51 } 52 finally 53 { 54 IntPtr windowLong = UnsafeNativeMethods.GetWindowLong(new HandleRef(this, zero), -4); 55 if ((IntPtr.Zero != this.defOwnerWndProc) || (windowLong != this.hookedWndProc)) 56 { 57 UnsafeNativeMethods.SetWindowLong(new HandleRef(this, zero), -4, new HandleRef(this, this.defOwnerWndProc)); 58 } 59 UnsafeNativeMethods.ThemingScope.Deactivate(userCookie); 60 this.defOwnerWndProc = IntPtr.Zero; 61 this.hookedWndProc = IntPtr.Zero; 62 GC.KeepAlive(d); 63 } 64 } 65 finally 66 { 67 if (window != null) 68 { 69 window.DestroyHandle(); 70 } 71 } 72 return cancel; 73 }
注意第36行代码:
this.defOwnerWndProc = UnsafeNativeMethods.SetWindowLong(new HandleRef(this, zero), -4, d);
上面的d指向这里:
1 protected virtual IntPtr OwnerWndProc(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam) 2 { 3 if (msg != helpMsg) 4 { 5 return UnsafeNativeMethods.CallWindowProc(this.defOwnerWndProc, hWnd, msg, wparam, lparam); 6 } 7 if (NativeWindow.WndProcShouldBeDebuggable) 8 { 9 this.OnHelpRequest(EventArgs.Empty); 10 } 11 else 12 { 13 try 14 { 15 this.OnHelpRequest(EventArgs.Empty); 16 } 17 catch (Exception exception) 18 { 19 Application.OnThreadException(exception); 20 } 21 } 22 return IntPtr.Zero; 23 }
它使用了自己的消息处理器替代了SaveFileDialog所属画面的消息处理器,然后把画面的消息处理器暂存在this.defOwnerWndProc中,然后在自己的消息处理中再转调画面的消息处理器(见上面第5行)。
简单来说,Dialog拦截了Form的消息处理,在自己的消息处理器处理完后,再将消息分发会Form。
基于以上处理,如果连续调两次ShowDialog会如何??
第一次是OK的,但是当第二次执行第36行的代码时,SetWindowLong将会返回之前的处理器,即Dialog自己的消息处理器,然后保存在this.defOwnerWndProc中,等到Dialog自己的消息处理器处理完后,想要将消息再分发给Form时,this.defOwnerWndProc已经被修改为自己的消息处理器,然后就没有然后了。。。
时间: 2024-10-04 21:05:24