Dispatcher.Invoke和传播错误

MoS*_*Slo 7 c# wpf multithreading dispatcher mvvm-light

我有一个WPF(使用.net 4.5和mvvmlight)的启动画面,它必须以异步方式执行各种加载操作,显示进度并偶尔要求用户输入.

当要求输入,我将创建表单/对话框关闭UI线程调用的ShowDialog(与闪屏作为父),这样不会出现跨线程问题.这一切都很好但是如果在请求输入时发生错误,则导致的异常丢失.

为简单起见,下面的示例根本不遵循MVVM.

这是我的app.cs,它设置了UI调度程序,并准备处理错误报告的任何未处理的调度程序异常:

public partial class App : Application
    {
        private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
        {
            e.Handled = true;
            System.Windows.Forms.MessageBox.Show("Exception Handled");
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            GalaSoft.MvvmLight.Threading.DispatcherHelper.Initialize();
        }
    }
Run Code Online (Sandbox Code Playgroud)

这是我的(非常简化的)启动/启动屏幕:

    private void Window_ContentRendered(object sender, EventArgs e)
        {
            System.Windows.Forms.MessageBox.Show("Starting long running process...");

            var t = System.Threading.Tasks.Task.Factory.StartNew(() =>
            {
                //some kind of threaded work which decided to ask for user input.
                    GalaSoft.MvvmLight.Threading.DispatcherHelper.UIDispatcher.Invoke(() =>
                {
                    //Show form for user input, launched on UIDispatcher so that it's created on the UI thread for ShowDialog etc
                    throw new Exception("issue in capturing input");
                });
            });
        }
Run Code Online (Sandbox Code Playgroud)

所以我要通过Invoke请求用户输入(因为我想等待答案)但是即使我通过UIDispatcher调用工作,也从不触发Application_DispatcherUnhandledException并且异常丢失.我错过了什么?该示例使用Task作为线程作业,但在使用BeginInvoke()时也会发生这种情况.当然应该在UIDispatcher上进行工作(以及由此产生的异常)?

更新:使用BeginInvoke的替代演示(异常未处理)

private void Window_ContentRendered(object sender, EventArgs e)
        {
            System.Windows.Forms.MessageBox.Show("Starting long running process...");

            Action anon = () =>
                {
                    //some kind of threaded work which decided to ask for user input.
                        GalaSoft.MvvmLight.Threading.DispatcherHelper.UIDispatcher.Invoke(() =>
                    {
                        //Show form for user input, launched on UIDispatcher so that it's created on the UI thread for ShowDialog etc
                        throw new Exception("issue in capturing input");
                    });
                };

            anon.BeginInvoke(RunCallback, null);
        }

        private void RunCallback(IAsyncResult result)
        {
            System.Windows.Forms.MessageBox.Show("Completed!");
        }
Run Code Online (Sandbox Code Playgroud)

Eli*_*bel 5

使用 Task

异常由任务处理,因此DispatcherUnhandledException不会触发。这是因为您使用了同步Dispatcher.Invoke方法-这几乎总是一个坏习惯;您在线程池线程上浪费时间,等待UI执行某些操作。您应该首选Dispatcher.BeginInvoke或(使用时awaitDispatcher.InvokeAsync

另外,注册该TaskScheduler.UnobservedTaskException事件可能是一个好主意,这样就可以记录此类异常(仅在对Task进行垃圾收集之后才会发生)。

最后,如果您能够使用C#5或更高版本,我强烈建议您看一下async / await。上面的方法可以改写为:

    private async void Window_ContentRendered(object sender, EventArgs e)
    {
        MessageBox.Show("Starting long running process...");

        await Task.Run(() =>
        {
            //some kind of threaded work
            throw new Exception("foo");
        });

        // code after the await will automatically be executed on the UI thread
        // the await will also propagate exceptions from within the task
        throw new Exception("issue in capturing input");
    }
Run Code Online (Sandbox Code Playgroud)

使用 Delegate.BeginInvoke

在这里,我们也在线程池上执行一个操作,但是异常是由“异步结果”对象处理的。我完全不鼓励您使用此旧线程模型(APM)。

顺便说一句,如果调用相应的代码,您可能会抛出异常EndInvoke(无论如何,您都应该这样做):

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        Action a = () => { Dispatcher.Invoke(() => { throw new Exception(); }); };
        a.BeginInvoke(Callback, a);
    }

    private void Callback(IAsyncResult ar)
    {
        ((Action)ar.AsyncState).EndInvoke(ar);
    }
Run Code Online (Sandbox Code Playgroud)

但是即使那样,DispatcherUnhandledException由于回调是在线程池线程上执行的,因此不会被调用。因此,该过程将完全崩溃。

结论

使用同步Dispatcher.Invoke将始终将异常传播给调用方。使用也非常浪费。如果调用者不是UI线程,则该异常将永远不会到达调度程序,并且根据所使用的线程API,该异常将被吞没或引发并使进程崩溃。