2013-01-08 47 views
5

最近,我遇到了一個問題,我仍然頭痛不已。在一個應用程序中,我註冊了一個調度程序異常處理程序。在同一應用程序中,第三方組件(DevExpress Grid Control)在Control.LayoutUpdated的事件處理程序中導致異常。我期望調度程序異常處理程序被觸發一次。但是,我得到一個堆棧溢出。我製作了一個沒有第三方組件的樣本,並發現它發生在每個WPF應用程序中。如果發生DispatcherUnhandledException異常,則在Control.LayoutUpdated中發生異常之後發生堆棧溢出

using System; 
    using System.Windows; 
    using System.Windows.Controls; 
    using System.Windows.Threading; 

    namespace MyApplication 
    { 
     /* App.xaml 

      <Application 
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
       x:Class="MyApplication.App" 
       Startup="OnStartup" 
      /> 

     */ 
     public partial class App 
     { 
      private void OnStartup(object sender, StartupEventArgs e) 
      { 
       DispatcherUnhandledException += OnDispatcherUnhandledException; 
       MainWindow = new MainWindow(); 
       MainWindow.Show(); 
      } 
      private static void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) 
      { 
       MessageBox.Show(e.Exception.Message); 
       e.Handled = true; 
      } 
     } 

     public class MainWindow : Window 
     { 
      private readonly Control mControl; 

      public MainWindow() 
      { 
       var grid = new Grid(); 
       var button = new Button(); 

       button.Content = "Crash!"; 
       button.HorizontalAlignment = HorizontalAlignment.Center; 
       button.VerticalAlignment = VerticalAlignment.Center; 
       button.Click += OnButtonClick; 

       mControl = new Control(); 

       grid.Children.Add(mControl); 
       grid.Children.Add(button); 

       Content = grid; 
      } 

      private void OnButtonClick(object sender, RoutedEventArgs e) 
      { 
       mControl.LayoutUpdated += ThrowException; 
       mControl.UpdateLayout(); 
       mControl.LayoutUpdated -= ThrowException; 
      } 

      private void ThrowException(object sender, EventArgs e) 
      { 
       throw new NotSupportedException(); 
      } 
     } 
    } 

有沒有什麼辦法可以防止這種行爲?它發生在.NET框架3.0,3.5,4.0和4.5。我不能只圍繞LayoutUpdated事件處理程序包裝try-catch,因爲它位於第三方組件中,我不認爲應該發生堆棧溢出。

+1

您在'OnButtonClick'中的代碼看起來非常荒謬。 – leppie

+0

第三方組件在構造函數中註冊事件處理程序。在這個例子中,它永遠不會調用mControl。LayoutUpdated - = ThrowException,因爲之前發生堆棧溢出。 – Georg

+0

我複製了你的代碼,並沒有得到堆棧溢出...... –

回答

4

我覺得Florian GI是對有關消息框,但如果不是你做的OnDispatcherUnhandledException方法別的東西(有或全無即只設置Handledtrue),它還是循環永遠無法得到的一個消息框mControl.LayoutUpdated -= ThrowException;一行。

所以我想我會通過與dotPeek代碼有點SNOP ...

當你調用UpdateLayout上的控制,最終它獲取的方法ContextLayoutManager.UpdateLayout這種方法的一個片段是這樣的:

// ... some code I snipped 
bool flag2 = true; 
UIElement element = (UIElement) null; 
try 
{ 
    this.invalidateTreeIfRecovering(); 
    while (this.hasDirtiness || this._firePostLayoutEvents) 
    { 

     //... Loads of code that I think will make sure 
     // hasDirtiness is false (since there is no reason 
     // for anything remaining dirty - also the event is 
     // raised so I think this is a safe assumption 

     if (!this.hasDirtiness) 
     { 
      this.fireLayoutUpdateEvent(); 
      if (!this.hasDirtiness) 
      { 
      this.fireAutomationEvents(); 
      if (!this.hasDirtiness) 
       this.fireSizeChangedEvents(); 
      } 
     } 
     //... a bit more 
     flag2 = false; 
    } 
} 
finally 
{ 
    this._isUpdating = false; 
    this._layoutRequestPosted = false; 
    //... some more code 
    if (flag2) 
    { 
     //... some code that I can't be bothered to grok 
     this.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, (Delegate) ContextLayoutManager._updateLayoutBackground, (object) this); 
    } 
} 

// ... and for good measure a smidge more code 

我要採取平底船並建議_ firePostLayoutEvents標誌在你的情況是真實的。

唯一的地方,_firePostLayoutEvents設置爲false在fireAutomationEvents方法,以便讓我們假設fireAutomationEvents方法的異常被拋出結束前的某個地方(我猜的fireLayoutUpdateEvent方法),因此這個標誌將不會設置爲假。

但是,當然,最後是在循環之外,所以它不會永遠循環(如果它沒有,你不會得到一個StackOverflowException)。

右,向前,所以我們調用UpdateLayoutBackground功能,這實際上只是調用NeedsRecalc所以讓我們看看......

private void NeedsRecalc() 
{ 
    if (this._layoutRequestPosted || this._isUpdating) 
    return; 
    MediaContext.From(this.Dispatcher).BeginInvokeOnRender(ContextLayoutManager._updateCallback, (object) this); 
    this._layoutRequestPosted = true; 
} 

Rightyho,正在調用UpdateLayoutCallback所以眯眼在那...

private static object UpdateLayoutCallback(object arg) 
{ 
    ContextLayoutManager contextLayoutManager = arg as ContextLayoutManager; 
    if (contextLayoutManager != null) 
    contextLayoutManager.UpdateLayout(); 
    return (object) null; 
} 

哇,這很有意思 - 它再次調用UpdateLayout - 我會的,因此,哈扎德稍微受過教育的猜測,那是你的問題的根本原因。

因此,我不認爲有什麼事情可以做,我害怕。

+0

謝謝您的詳細解釋。我使用.NET Reflector檢查了您的研究,我可以確認您的觀察結果。看來我們在BCL有一個問題。可悲的是,我甚至無法在LayoutUpdated處理程序的代碼中包裝try-catch,因爲我正在處理髮生原始異常的第三方組件。我會讓開發人員知道這個問題。 – Georg

1

我對以下內容不太確定,但也許這是一個正確方向的猜測。

在MSDN它的SAI:

然而,LayoutUpdated也可以將對象 壽命期間發生在運行時,對於各種原因:一個屬性變化,窗口 大小調整,或一個顯式請求(UpdateLayout或ApplyTemplate)。

所以在MessageBox顯示後它可能會被觸發?

如果這是一個MessageBox將通過未處理的異常打開的情況,該異常會觸發LayoutUpdated-EventHandler,並且這會再次引發未處理的異常。這將導致無限循環,並在一段時間後堆棧溢出。

如果你沒有在LayoutUpdated-EventHandler中拋出一個未處理的異常,但是調用MessageBox.Show()方法,它也會以無限循環結束,那麼我的觀點就會證明什麼。

0

我意識到我在四年遲到了,但也許有人會覺得有幫助...

您的應用程序的調度員不停止調用MessageBox.Show()期間響應輸入和佈局事件。任何時候任何佈局相關的工作都會觸發LayoutUpdated事件,這種情況比您預期的要多得多。在顯示消息框時它將繼續激活,如果觸發錯誤的任何情況持續存在,則會引發新的異常,並且您的處理程序將顯示越來越多的消息框。因爲MessageBox.Show()是一個阻塞調用,它不會從調用堆棧中消失,直到它返回。您的處理程序的後續調用將會越來越深入調度程序的調用堆棧,直至其溢出。

你真的有兩個單獨的問題:

  1. 你的代碼來顯示一個錯誤對話框是折返。

  2. 在顯示崩潰對話框時,您的應用程序會繼續在調度程序線程上引發異常。

您可以使用非重入隊列解決第一個問題。不要立即顯示崩潰對話框,請讓您的處理程序將異常放入隊列中。只有在之前,您的處理程序纔會處理隊列,您尚未將其處理得更遠。這可以防止同時顯示多個崩潰對話框,並且應該防止調用堆棧變得太深,從而避免堆棧溢出問題。

要解決第二個問題,您應該在看到第一個異常(並在顯示崩潰對話框之前)立即關閉應用程序的有問題的部分。或者,您可以設計一種方法來過濾掉重複的異常,並確保等同的錯誤不會同時在隊列中結束。但鑑於例外情況經常發生,我會選擇第一種方法。

請記住,您需要解決這兩個問題。如果您沒有解決第二個問題,那麼您可能最終會在StackOverflowException之間交易OutOfMemoryException。或者你會一個接一個地顯示無數個碰撞對話框。無論哪種方式,它都會很糟糕。