Обработка исключений внутри "async void" обработчиков команд WPF
Я просматриваю код WPF моих коллег, который представляет собой библиотеку компонентов UserControl
с большим количеством обработчиков событий и команд async void
, В настоящее время эти методы не реализуют обработку ошибок внутри.
Код в двух словах:
<Window.CommandBindings>
<CommandBinding
Command="ApplicationCommands.New"
Executed="NewCommand_Executed"/>
</Window.CommandBindings>
private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
// do some fake async work (and may throw if timeout < -1)
var timeout = new Random(Environment.TickCount).Next(-100, 100);
await Task.Delay(timeout);
}
Исключения, вызванные, но не наблюдаемые внутри NewCommand_Executed
, могут обрабатываться только на глобальном уровне (например, с AppDomain.CurrentDomain.UnhandledException
). По-видимому, это не очень хорошая идея.
Я мог бы обрабатывать исключения локально:
private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
try
{
// do some fake async work (throws if timeout < -1)
var timeout = new Random(Environment.TickCount).Next(-100, 100);
await Task.Delay(timeout);
}
catch (Exception ex)
{
// somehow log and report the error
MessageBox.Show(ex.Message);
}
}
Однако в этом случае приложение-хозяин ViewModel не будет знать об ошибках внутри NewCommand_Executed
. Не идеальное решение, а также пользовательский интерфейс, сообщающий об ошибках, не всегда должен быть частью кода библиотеки.
Другой подход заключается в том, чтобы обрабатывать их локально и запускать выделенное событие ошибки:
public class AsyncErrorEventArgs: EventArgs
{
public object Sender { get; internal set; }
public ExecutedRoutedEventArgs Args { get; internal set; }
public ExceptionDispatchInfo ExceptionInfo { get; internal set; }
}
public delegate void AsyncErrorEventHandler(object sender, AsyncErrorEventArgs e);
public event AsyncErrorEventHandler AsyncErrorEvent;
private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
ExceptionDispatchInfo exceptionInfo = null;
try
{
// do some fake async work (throws if timeout < -1)
var timeout = new Random(Environment.TickCount).Next(-100, 100);
await Task.Delay(timeout);
}
catch (Exception ex)
{
// capture the error
exceptionInfo = ExceptionDispatchInfo.Capture(ex);
}
if (exceptionInfo != null && this.AsyncErrorEvent != null)
this.AsyncErrorEvent(sender, new AsyncErrorEventArgs {
Sender = this, Args = e, ExceptionInfo = exceptionInfo });
}
Мне больше нравится последний, но я был бы признателен за любые другие предложения, поскольку мой опыт работы с WPF несколько ограничен.
-
Есть ли установленный шаблон WPF для распространения ошибок из обработчиков команд async void
на ViewModal?
-
Как правило, плохая идея делать асинхронную работу внутри обработчиков команд WPF, поскольку, возможно, они предназначены для быстрого синхронного обновления пользовательского интерфейса?
Я задаю этот вопрос в контексте WPF, но я думаю, что он может также применяться к обработчикам событий async void
в WinForms.
Ответы
Ответ 1
Проблема заключается в том, что ваша библиотека UserControl не архивируется обычным способом MVVM. Обычно для нетривиальных команд ваш код UserControl не будет напрямую связываться с командами, но вместо этого будет иметь свойства, которые при установке (через привязку к ViewModel) вызовут действие в элементе управления. Затем ваша ViewModel привяжется к команде приложения и задает соответствующие свойства. (В качестве альтернативы, ваша среда MVVM может иметь другой сценарий передачи сообщений, который может использоваться для взаимодействия между ViewModel и View).
Что касается исключений, которые выбрасываются внутри пользовательского интерфейса, я снова чувствую, что существует проблема архитектуры. Если UserControl делает больше, чем действует как представление (например, запускает любую бизнес-логику, которая может вызвать непредвиденные исключения), тогда это следует разделить на View и ViewModel. ViewModel будет запускать логику и может быть либо создан другими вашими приложениями ViewModels, либо обмениваться данными с помощью другого метода (как упоминалось выше).
Если есть исключения, которые вызывают код компоновки/визуализации UserControl, это должно (почти без исключения) не быть пойманным каким-либо образом вашей ViewModel. Это должно, как вы упомянули, обрабатывать только для ведения журнала с помощью обработчика глобального уровня.
Наконец, если в Контрольном коде действительно известны "исключения", о которых вам нужно уведомить о вашем представлении ViewModel, я предлагаю поймать известные исключения и создать событие/команду и установить свойство. Но опять же, это действительно не должно использоваться для исключений, просто ожидаемых состояний "ошибки".
Ответ 2
Распространение исключений, о которых пользователи почти на 100% не знают, не является хорошей практикой, на мой взгляд. См. this
Я вижу два варианта, которые у вас действительно есть, поскольку WPF не предоставляет каких-либо из ящиков механизмов такого уведомления о любых проблемах:
- То, как вы уже предложили, поймать и запустить мероприятие.
- Верните объект Task из метода async (в вашем случае вам кажется, что вам придется выставить его через свойство). Пользователи смогут проверить, были ли какие-либо ошибки во время выполнения, и приложить задачу продолжения, если они этого захотят. Внутри обработчика вы можете поймать любые исключения и использовать TaskCompletionSource, чтобы установить результат обработчика.
В целом вам нужно написать некоторые xml-комментарии для такого кода, потому что это не так просто понять.
Самое главное, что вы никогда не должны (почти) бросать любые исключения из любых вторичных потоков.