Dispatcher.Invoke 和传播错误
Dispatcher.Invoke and propagating errors
我有一个 WPF 启动画面(使用 .net 4.5 和 mvvmlight),它必须以异步方式执行各种加载操作,显示进度并偶尔要求用户输入。
请求输入时,我将在 UI 线程之外创建 forms/dialogs 来调用 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();
}
}
这是我的(非常简化的)startup/splash 屏幕:
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");
});
});
}
所以我通过 Invoke 请求用户输入(因为我想等待答案)但是即使我通过 UIDispatcher 调用工作,Application_DispatcherUnhandledException 也永远不会被解雇并且异常丢失。我错过了什么?该示例为线程作业使用任务,但在使用 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!");
}
使用Task
异常由任务处理,因此 DispatcherUnhandledException
不会触发。这是因为您使用了同步 Dispatcher.Invoke
方法——这几乎总是一种不好的做法;您在等待 UI 执行某些操作的线程池线程上浪费时间。您应该更喜欢 Dispatcher.BeginInvoke
或(当使用 await
时)Dispatcher.InvokeAsync
.
此外,注册 TaskScheduler.UnobservedTaskException
事件可能是个好主意,这样可以记录此类异常(这仅在任务被垃圾回收后发生)。
最后,如果你会使用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");
}
使用Delegate.BeginInvoke
这里我们也在线程池上执行了一个操作,但是异常是由"async result"对象处理的。我完全不鼓励您使用这种旧的线程模型 (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);
}
但即便如此,DispatcherUnhandledException
也不会被调用,因为回调是在线程池线程上执行的。所以这个过程只会崩溃。
结论
使用同步 Dispatcher.Invoke
将始终将异常传播给调用者。使用起来也很浪费。如果调用者不是 UI 线程,异常将永远不会到达调度程序,并且根据使用的线程 API,异常将被吞没或抛出并使进程崩溃。
我有一个 WPF 启动画面(使用 .net 4.5 和 mvvmlight),它必须以异步方式执行各种加载操作,显示进度并偶尔要求用户输入。
请求输入时,我将在 UI 线程之外创建 forms/dialogs 来调用 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();
}
}
这是我的(非常简化的)startup/splash 屏幕:
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");
});
});
}
所以我通过 Invoke 请求用户输入(因为我想等待答案)但是即使我通过 UIDispatcher 调用工作,Application_DispatcherUnhandledException 也永远不会被解雇并且异常丢失。我错过了什么?该示例为线程作业使用任务,但在使用 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!");
}
使用Task
异常由任务处理,因此 DispatcherUnhandledException
不会触发。这是因为您使用了同步 Dispatcher.Invoke
方法——这几乎总是一种不好的做法;您在等待 UI 执行某些操作的线程池线程上浪费时间。您应该更喜欢 Dispatcher.BeginInvoke
或(当使用 await
时)Dispatcher.InvokeAsync
.
此外,注册 TaskScheduler.UnobservedTaskException
事件可能是个好主意,这样可以记录此类异常(这仅在任务被垃圾回收后发生)。
最后,如果你会使用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");
}
使用Delegate.BeginInvoke
这里我们也在线程池上执行了一个操作,但是异常是由"async result"对象处理的。我完全不鼓励您使用这种旧的线程模型 (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);
}
但即便如此,DispatcherUnhandledException
也不会被调用,因为回调是在线程池线程上执行的。所以这个过程只会崩溃。
结论
使用同步 Dispatcher.Invoke
将始终将异常传播给调用者。使用起来也很浪费。如果调用者不是 UI 线程,异常将永远不会到达调度程序,并且根据使用的线程 API,异常将被吞没或抛出并使进程崩溃。