メモの日々


2017年08月16日(水) [長年日記]

[c#] WPFのバインディング処理で投げられた例外をDispatcherUnhandledExceptionイベントのハンドラで受け取る

WPFアプリケーションにおいて例外をApplication.DispatcherUnhandledExceptionイベントのハンドラで受け取ってエラーダイアログを表示するようにしてみたが、バインディングの処理において投げられた例外は受け取れないことを知った。

対処として、例外を投げうる処理をasync/awaitを使った非同期メソッドで実行するようにすればハンドラで例外を受け取れそうなのでサンプルをメモ。

MainWindow.xaml

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Sample"
        xmlns:local_e="clr-namespace:Sample.例外"
        x:Class="Sample.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="100" Width="300">
    <Window.DataContext>
        <local_e:ExceptionViewModel/>
    </Window.DataContext>
    <Grid>
        <ComboBox Height="20" Width="200"
                  ItemsSource="{Binding Items}"
                  SelectedItem="{Binding Selected}"/>
    </Grid>
</Window>
  • ComboBoxで選択した内容がExceptionViewModelクラスのSelectedプロパティに設定されるようにしている。

ExceptionViewModel.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace Sample.例外
{
    class ExceptionViewModel
    {
        private List<string> items = new List<string>() { "throw", "task", "async" };
        private string selected;

        public List<string> Items => items;

        public string Selected
        {
            get => selected;
            set
            {
                selected = value;
                switch (selected)
                {
                    case "throw":
                        ThrowException(selected);
                        break;
                    case "task":
                        Task.Run(() => ThrowException(selected));
                        break;
                    case "async":
                        Async(() => ThrowException(selected));
                        break;
                }
                Debug.WriteLine($"{selected}: end of property");
            }
        }

        private static void ThrowException(string message)
        {
            Thread.Sleep(3000);
            Debug.WriteLine($"{message}: throw exception");
            throw new Exception(message);
        }

        private static async void Async(Action action)
        {
            await Task.Run(action);
        }
    }
}
  • Selectedプロパティのセッターで例外を投げるThrowException()を呼び出している。
    • 設定値が「throw」の場合は単にThrowException()を呼び出している。ここで投げられた例外は後述のイベントハンドラまで届かない。
    • 設定値が「task」の場合は、Taskクラスを使って別スレッドでThrowException()を呼び出している。ここで投げられた例外も後述のイベントハンドラまで届かない。
    • 設定値が「async」の場合は、非同期メソッドであるAsync()の中でThrowException()を呼び出している。ここで投げられた例外は後述のイベントハンドラまで届く。

App.xaml

<Application x:Class="Sample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Sample"
             StartupUri="MainWindow.xaml"
             DispatcherUnhandledException="HandleException">
    <Application.Resources>
         
    </Application.Resources>
</Application>
  • App.xamlでApplication.DispatcherUnhandledExceptionイベントのハンドラを設定している。

App.xaml.cs

using System.Windows;
using System.Windows.Threading;

namespace Sample
{
    public partial class App : Application
    {
        public void HandleException(
            object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine($"Handled: {e.Exception.Message}");
            e.Handled = true;
        }
    }
}
  • イベントハンドラではデバッグ出力をするだけ。

実行結果

ComboBoxでthrowを選択すると、デバッグ出力には

throw: throw exception
例外がスローされました: 'System.Exception' (Sample.exe の中)
System.Windows.Data Error: 8 : Cannot save value from target back to source. BindingExpression:Path=Selected; DataItem='ExceptionViewModel' (HashCode=66824994); target element is 'ComboBox' (Name=''); target property is 'SelectedItem' (type 'Object') Exception:'System.Exception: throw
   場所 System.ComponentModel.ReflectPropertyDescriptor.SetValue(Object component, Object value)
   場所 MS.Internal.Data.PropertyPathWorker.SetValue(Object item, Object value)
   場所 MS.Internal.Data.ClrBindingWorker.UpdateValue(Object value)
   場所 System.Windows.Data.BindingExpression.UpdateSource(Object value)'

と出力される。イベントハンドラには到達していない。バインディングの処理の中で例外がcatchされてしまっているからだろうか。

ComboBoxでtaskを選択すると、デバッグ出力には

task: end of property
task: throw exception
例外がスローされました: 'System.Exception' (Sample.exe の中)

と出力される。イベントハンドラには到達していない。例外はTaskオブジェクトが受け取ったけれども誰も参照しなかったと考えればいいだろうか。

ComboBoxでasyncを選択すると、デバッグ出力には

async: end of property
async: throw exception
例外がスローされました: 'System.Exception' (Sample.exe の中)
例外がスローされました: 'System.Exception' (mscorlib.dll の中)
Handled: async

と出力される。イベントハンドラに到達している。非同期メソッドは便利だと考えればいいだろうか。ただし、要件によっては当該処理を非同期で実行すると問題があるケースもありそう。重複実行を防ぐ必要があるとか。