場景:winform的程序中,有一個畫面上放了一個Button,點擊這個Button會調用.Net控件SaveFileDialog的ShowDialog方法。
場景很簡單,但是碰到了這樣一個有趣的問題:
在機器很慢的情況下,連續快速兩次點擊上述Button,會導致棧溢出異常(StackOverflowException)。
由於機器很慢的情況難以模擬且不能穩定重現,所以做了一個簡單的Demo,嘗試在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已經被修改為自己的消息處理器,然后就沒有然后了。。。