標籤:
情境: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 try12 {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 try35 {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 try43 {44 cancel = this.RunDialog(zero) ? DialogResult.OK : DialogResult.Cancel;45 }46 finally47 {48 Application.EndModalMessageLoop();49 }50 return cancel;51 }52 finally53 {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 finally66 {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 else12 {13 try14 {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已經被修改為自己的訊息處理器,然後就沒有然後了。。。
一個關於.Net的SaveFileDialog控制項(Winform)的有趣問題