Обработка исключений внутри "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-комментарии для такого кода, потому что это не так просто понять. Самое главное, что вы никогда не должны (почти) бросать любые исключения из любых вторичных потоков.