5

最近,我遇到了一个问题,我仍然很头疼。在一个应用程序中,我注册了一个调度程序异常处理程序。在同一个应用程序中,第三方组件 ( DevExpress Grid Control) 在Control.LayoutUpdated. 我希望调度程序异常处理程序被触发一次。但相反,我得到了堆栈溢出。我制作了一个没有第三方组件的示例,并发现它发生在每个 WPF 应用程序中。

    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 上。我不能只包装事件处理程序try-catchLayoutUpdated因为它位于第三方组件中,而且我认为不应该发生堆栈溢出。

4

3 回答 3

4

我认为Florian GI对消息框的看法是正确的,但是如果您在该方法中执行了其他操作(或什么都不做,即只是设置Handledtrue)而不是消息框,OnDispatcherUnhandledException它仍然会永远循环并且不会到达该mControl.LayoutUpdated -= ThrowException;行。

所以我想我会用 dotPeek 稍微窥探一下代码......

当您调用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方法),所以这个标志不会被设置为 false。

但是,当然,finally 在循环之外,所以它不会永远循环(如果是这样,你就不会得到 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了——因此,我会冒一个稍微有根据的猜测,那就是你的问题的根本原因。

因此,我认为您对此无能为力。

于 2013-01-08T13:00:43.403 回答
1

我不太确定以下内容,但也许这是对正确方向的猜测。

在 MSDN 中它说:

但是,LayoutUpdated 也可能在对象生命周期的运行时发生,原因有很多:属性更改、窗口大小调整或显式请求(UpdateLayout 或 ApplyTemplate)。

所以在 MessageBox 显示后它也可能被触发?

如果是这种情况,MessageBox 将通过未处理的异常打开,这会触发 LayoutUpdated-EventHandler,这会再次引发未处理的异常。这将导致无限循环,并在一段时间后导致堆栈溢出。

如果您没有在 LayoutUpdated-EventHandler 中抛出未处理的异常,而是调用 MessageBox.Show() 方法,它也会以无限循环结束,这可以证明我的观点。

于 2013-01-08T12:28:59.310 回答
0

我意识到我迟到了四年多,但也许有人会觉得这很有用......

在调用MessageBox.Show(). 只要有任何与布局相关的工作要做,就会触发该LayoutUpdated事件,这比您预期的要频繁得多。在显示消息框时它将继续触发,如果任何触发错误的条件继续存在,则会引发新的异常,并且您的处理程序将显示越来越多的消息框。并且因为MessageBox.Show()是阻塞调用,所以它不会从调用堆栈中消失,直到它返回。处理程序的后续调用将被越来越深地推入调度程序的调用堆栈,直到它溢出。

你真的有两个不同的问题:

  1. 您显示崩溃对话框的代码是可重入的。

  2. 在显示崩溃对话框时,您的应用程序会继续在调度程序线程上引发异常。

您可以使用不可重入队列解决第一个问题。不要立即显示崩溃对话框,而是让您的处理程序将异常放入队列中。仅当您尚未在堆栈的更远处处理队列时,才让您的处理程序处理队列。这可以防止同时显示多个崩溃对话框,并且应该防止调用堆栈增长太深,从而避免堆栈溢出问题。

要解决第二个问题,您可能应该在看到第一个异常后(并且在显示崩溃对话框之前)立即关闭应用程序的违规部分。或者,您可以设计一种方法来过滤掉重复的异常并确保相同的错误不会同时出现在队列中。但考虑到异常发生的速度有多快,我会选择第一个选项。

请记住,您需要解决这两个问题。如果您不解决第二个问题,您最终可能会StackOverflowExceptionOutOfMemoryException. 或者,您将一个接一个地显示无数个崩溃对话框。不管怎样,都会很糟糕。

于 2017-06-20T20:01:38.023 回答