2013-01-08 48 views
5

Gần đây, tôi đã gặp phải sự cố mà tôi vẫn đang phá vỡ. Trong một ứng dụng, tôi đã đăng ký một trình xử lý ngoại lệ cho trình điều phối. Trong cùng một ứng dụng, một thành phần của bên thứ ba (DevExpress Grid Control) gây ra một ngoại lệ trong trình xử lý sự kiện cho Control.LayoutUpdated. Tôi mong đợi, rằng xử lý ngoại lệ dispatcher được kích hoạt một lần. Nhưng thay vào đó, tôi nhận được một tràn ngăn xếp. Tôi đã tạo ra một mẫu không có thành phần của bên thứ ba và được phát hiện, rằng nó xảy ra trong mọi ứng dụng WPF.Ngăn xếp tràn sau khi ngoại lệ trong Control.LayoutCập nhật nếu DispatcherUnhandledException được đăng ký

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(); 
      } 
     } 
    } 

Có cách nào để ngăn chặn hành vi này không? Nó xảy ra với .NET framework 3.0, 3.5, 4.0 và 4.5. Tôi không thể chỉ quấn try-catch xung quanh trình xử lý sự kiện LayoutUpdated vì nó nằm trong thành phần của bên thứ ba và tôi không nghĩ rằng xảy ra tràn ngăn xếp.

+1

Mã của bạn trong 'OnButtonClick' trông rất vô nghĩa. – leppie

+0

Thành phần của bên thứ ba đăng ký trình xử lý sự kiện trong hàm tạo. Trong mẫu này, nó sẽ không bao giờ gọi mControl.LayoutUpdated - = ThrowException vì tràn ngăn xếp xảy ra trước đó. – Georg

+0

Tôi đã sao chép mã của bạn và không nhận được tràn ngăn xếp ... –

Trả lời

4

Tôi nghĩ Florian GI là đúng về hộp tin nhắn, nhưng nếu thay vì một hộp thông báo bạn đã làm cái gì khác (hoặc không có gì tức là chỉ cần đặt Handled để true) trong phương pháp OnDispatcherUnhandledException nó vẫn lặp mãi mãi và không đến được mControl.LayoutUpdated -= ThrowException; dòng.

Vì vậy, tôi nghĩ rằng tôi sẽ có một chút snop qua mã với dotPeek ...

Khi bạn gọi UpdateLayout trên sự kiểm soát, cuối cùng nó được cho là phương pháp ContextLayoutManager.UpdateLayout và một đoạn của phương pháp này như sau:

// ... 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 

Tôi sẽ thực hiện và đề xuất rằng cờ _ firePostLayoutEvents là đúng trong trường hợp của bạn.

Nơi duy nhất nơi _firePostLayoutEvents được thiết lập là false là trong phương pháp fireAutomationEvents vì vậy hãy giả sử rằng ở đâu đó trước khi kết thúc các fireAutomationEvents phương pháp ngoại lệ được ném (Tôi sẽ đoán phương pháp fireLayoutUpdateEvent) để cờ này sẽ không được thiết lập để sai.

Nhưng, tất nhiên, cuối cùng là bên ngoài vòng lặp để nó sẽ không lặp mãi mãi (và nếu nó đã làm bạn không nhận được một StackOverflowException).

Phải, trở đi, vì vậy chúng tôi viện dẫn UpdateLayoutBackground chức năng, mà thực sự chỉ cần gọi NeedsRecalc vì vậy hãy nhìn vào đó ...

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

Rightyho, đó là cách gọi UpdateLayoutCallback nên nheo mắt ở đó ...

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

Oooooh, đó là thú vị - nó gọi UpdateLayout một lần nữa - tôi sẽ, do đó, Hazzard một đoán một chút rằng đó là nguyên nhân gốc rễ của vấn đề của bạn.

Vì vậy, tôi không nghĩ có bất kỳ điều gì bạn có thể làm về điều đó tôi sợ.

+0

Cảm ơn bạn đã giải thích chi tiết. Tôi đã xem xét nghiên cứu của bạn bằng cách sử dụng .NET Reflector và tôi có thể xác nhận các quan sát của bạn. Dường như chúng ta có một vấn đề trong BCL ở đây. Đáng buồn thay, tôi thậm chí không thể quấn thử một mã xung quanh mã trong trình xử lý LayoutUpdated vì tôi đang làm việc với một thành phần của bên thứ ba trong đó ngoại lệ ban đầu xảy ra. Tôi sẽ cho nhà phát triển biết về sự cố. – Georg

1

Tôi không hoàn toàn chắc chắn về những điều sau đây, nhưng có thể đó là sự đoán đúng hướng.

Trong MSDN nó sais:

Tuy nhiên, LayoutUpdated cũng có thể xảy ra vào thời gian chạy trong đối tượng đời, cho một loạt các lý do: một sự thay đổi bất động sản, một thay đổi kích thước cửa sổ , hoặc một yêu cầu rõ ràng (UpdateLayout hoặc ApplyTemplate).

Vì vậy, nó cũng có thể bị sa thải sau khi MessageBox hiển thị?

Nếu trường hợp đó một MessageBox sẽ mở thông qua một ngoại lệ chưa được xử lý, sẽ kích hoạt LayoutUpdated-EventHandler và điều này lại làm tăng một ngoại lệ chưa được giải quyết. Điều này sẽ dẫn đến một vòng lặp vô tận và sau một thời gian để tràn ngăn xếp.

Nếu bạn không ném ngoại lệ chưa được xử lý trong LayoutUpdated-EventHandler, nhưng hãy gọi phương thức MessageBox.Show() - nó cũng kết thúc bằng một vòng lặp vô tận, điều gì sẽ chứng minh quan điểm của tôi.

0

Tôi nhận ra tôi trong vòng bốn năm muộn để đảng, nhưng có lẽ ai đó sẽ tìm thấy điều phối này hữu ích ...

của ứng dụng của bạn không ngừng đáp ứng đầu vào và bố trí các sự kiện trong suốt cuộc gọi đến MessageBox.Show(). Sự kiện LayoutUpdated kích hoạt bất kỳ lúc nào có bất kỳ công việc liên quan đến bố cục nào để thực hiện, điều này xảy ra thường xuyên hơn bạn mong đợi. Nó sẽ tiếp tục kích hoạt trong khi hộp thông báo được hiển thị và nếu bất kỳ điều kiện nào kích hoạt lỗi vẫn tiếp tục tồn tại, các ngoại lệ mới sẽ được nâng lên và trình xử lý của bạn sẽ hiển thị nhiều hộp thông báo hơn. Và bởi vì MessageBox.Show() là một cuộc gọi chặn, nó sẽ không biến mất khỏi ngăn xếp cuộc gọi cho đến khi nó trở lại. Các lời gọi tiếp theo của trình xử lý của bạn sẽ được đẩy sâu hơn và sâu hơn vào ngăn xếp cuộc gọi của người điều phối cho đến khi nó tràn.

Bạn thực sự có hai vấn đề riêng biệt:

  1. Mã của bạn để hiển thị một hộp thoại sụp đổ là lõm.

  2. Ứng dụng của bạn tiếp tục tăng ngoại lệ lên luồng của trình điều phối trong khi hộp thoại tai nạn của bạn đang được hiển thị.

Bạn có thể giải quyết vấn đề đầu tiên với hàng đợi không lặp lại. Thay vì hiển thị hộp thoại sự cố ngay lập tức, hãy để trình xử lý của bạn đặt ngoại lệ trong hàng đợi. Yêu cầu trình xử lý của bạn xử lý hàng đợi chỉ khi bạn chưa xử lý nó ở phía xa ngăn xếp. Điều này ngăn không cho nhiều hộp thoại sụp đổ được hiển thị đồng thời, và nên giữ ngăn xếp cuộc gọi của bạn phát triển quá sâu, do đó tránh sự cố tràn ngăn xếp của bạn.

Để giải quyết vấn đề thứ hai, có lẽ bạn nên tắt phần vi phạm của ứng dụng ngay khi bạn thấy ngoại lệ đầu tiên (và trước khi bạn hiển thị hộp thoại sự cố). Ngoài ra, bạn có thể đưa ra một cách để lọc ra các ngoại lệ trùng lặp và đảm bảo rằng các lỗi tương đương không kết thúc trong hàng đợi cùng một lúc. Nhưng với những ngoại lệ được lặp lại nhanh như thế nào, tôi sẽ chọn tùy chọn đầu tiên.

Hãy nhớ rằng bạn cần phải giải quyết cả hai vấn đề này. Nếu bạn không giải quyết vấn đề thứ hai, bạn có thể sẽ kết thúc giao dịch trong số StackOverflowException của mình cho số OutOfMemoryException. Hoặc bạn sẽ hiển thị một số lượng vô hạn các hộp thoại sự cố, cái khác. Dù bằng cách nào, nó sẽ là xấu.

Các vấn đề liên quan