【WPF】例外処理を共通化する

【WPF】例外処理を共通化する

はじめに

本記事では、WPF アプリケーション開発において例外処理を共通化する方法について解説しています。

ある程度オブジェクト指向を勉強して、Csharp に触れたことがある人向けの記事になっています。

開発環境

  • OS: Windows11 22H2(22621.1555)
  • エディタ: Visual Studio 2022
  • フレームワーク: .NET6

実際のアプリケーション

実際のアプリケーションは以下のようなものです。

custom-exception

仕様は以下のように設定しました。

  • 捕捉していない例外も観測できるように実装
  • 意図した例外はレベルに応じてアプリケーションを落とさず警告だけ表示させる
  • エラーレベルが Error だった場合はアプリケーションを落とす
  • 補足していない例外だった場合もアプリケーションを落とす

ファイル構成は画像のようになっています。

custom-exception-01

共通化することのメリット

例外処理を共通化することで以下のようなメリットがあります。

  • 意図していない例外が発生したときでも、一箇所に集めることで確実にログファイルに記録することができる
  • 自分で実装した意図した例外は独自の処理を実装することができる(アプリケーションを落とさないなど)
  • 共通したメッセージボックスを表示することができるので、コードの記述量が減る

抽象クラスを作成

まずはベースとなる抽象クラス(基底クラス)を作成します。このクラスを継承させることで共通の機能を持たせることが出来ます。

"BaseException.cs"
using System;

namespace Global_Exception_Handler.Exceptions
{
    /// <summary>
    /// 例外の抽象クラスを作成
    /// </summary>
    public abstract class BaseException : Exception
    {
        /// <summary>
        /// サブクラスに実装を強制させる
        /// </summary>
        public abstract ExceptonKind Kind { get; }

        /// <summary>
        /// Exceptionの区分
        /// </summary>
        public enum ExceptonKind
        {
            Info,
            Warning,
            Error
        }

        /// <summary>
        /// コンストラクター(インナーエクセプションも受ける場合)
        /// </summary>
        /// <param name="message"></param>
        /// <param name="exception"></param>
        public BaseException(string message, Exception exception)
            // base コンストラクタ初期化子
            // 基底クラス(ここではException)のコンストラクタを明示的に呼び出す
            : base(message, exception)
        { }

        /// <summary>
        /// コンストラクター(メッセージのみの場合)
        /// </summary>
        /// <param name="message"></param>
        public BaseException(string message) : base(message)
        { }
    }
}

まずはコンストラクターから見ていきます。

/// <summary>
/// コンストラクター(インナーエクセプションも受ける場合)
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public BaseException(string message, Exception exception)
    // base コンストラクタ初期化子
    // 基底クラス(ここではException)のコンストラクタを明示的に呼び出す
    : base(message, exception)
{ }

/// <summary>
/// コンストラクター(メッセージのみの場合)
/// </summary>
/// <param name="message"></param>
public BaseException(string message) : base(message)
{ }

ここでのポイントは、base コンストラクタ初期化子を使用してExceptionのコンストラクタを明示的に呼び出していることです。こうすることで、この抽象クラスを継承した独自の Exception クラスでも、インスタンス化する際のコンストラクタでExceptionの機能を使用することが出来ます。

また、引数で何を指定するかで呼び出すコンストラクタも分類するようにしています。

第一引数が文字列型のみだった場合はpublic BaseException(string message)が呼び出され、第一引数に文字列と、Exception クラスが渡されていた場合はpublic BaseException(string message, Exception exception)が呼び出されます。

次に以下のコードを見ていきます。

/// <summary>
/// サブクラスに実装を矯正させる
/// </summary>
public abstract ExceptonKind Kind { get; }

/// <summary>
/// Exceptionの区分
/// </summary>
public enum ExceptonKind
{
    Info,
    Warning,
    Error
}

public abstract ExceptonKind Kind { get; }で継承先のクラスへこのコードの実装を矯正しています。つまり、継承先の独自 Exception クラスでこのコードを実装していないとエラーになります。

ExceptonKindと言うのは、public enum ExceptonKindで指定している構造体です。

具象クラス

基底クラスを継承した、実際にインスタンス化する具象クラスを見ていきます。

"ErrorException.cs"
using System;

namespace Global_Exception_Handler.Exceptions
{
    public sealed class ErrorException : BaseException
    {
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <param name="message">出力メッセージ</param>
        /// <param name="exception">インナーエクセプション</param>
        public ErrorException(string message, Exception exception) : base(message, exception)
        { }

        /// <summary>
        /// 例外の区分
        /// </summary>
        public override ExceptonKind Kind => ExceptonKind.Error;
    }
}

コンストラクタで、独自に表示させるメッセージとインナーエクセプションを受け取るような実装になっています。

例外の区分はErrorを指定しています。

次にInfoExceptionを見ていきます。

"InfoException.cs"
namespace Global_Exception_Handler.Exceptions
{
    public sealed class InfoException : BaseException
    {
        /// <summary>
        /// コンストラクター
        /// </summary>
        public InfoException() : base("情報です。")
        { }

        /// <summary>
        /// 例外の区分
        /// </summary>
        public override ExceptonKind Kind => ExceptonKind.Info;
    }
}

InfoExceptionではメッセージだけをコンストラクタに渡すパターンで実装しています。

App.xaml.cs に共通化処理を書く

App.xaml.csというコードビハインドに例外を共通化する処理を記述していきます。

"App.xaml.cs"
using Global_Exception_Handler.Exceptions;
using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;

namespace Global_Exception_Handler
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        /// <summary>
        /// コンストラクター
        /// </summary>
        public App()
        {
            // 処理されていない例外をここでまとめてキャッチする
            DispatcherUnhandledException += App_DispatcherUnhandledException;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            // デフォルトのアイコンはエラーに設定
            MessageBoxImage iconImage = MessageBoxImage.Error;
            // キャプション
            string caption = "エラー";

            // 型が変換できなかったらnullが返却される(意図していない例外であればnullになる)
            var exceptionBase = e.Exception as BaseException;

            if (exceptionBase != null)
            {
                if (exceptionBase.Kind == BaseException.ExceptonKind.Info)
                {
                    iconImage = MessageBoxImage.Information;
                    caption = "情報";

                    Debug.WriteLine(e.Exception.Message);

                    // 警告などの場合はアプリケーションを落とさないようにする(デフォルトでは false なのでアプリケーションは落ちる)
                    e.Handled = true;
                }

                if (exceptionBase.Kind == BaseException.ExceptonKind.Warning)
                {
                    iconImage = MessageBoxImage.Warning;
                    caption = "警告";

                    Debug.WriteLine(e.Exception.Message);

                    // 警告などの場合はアプリケーションを落とさないようにする(デフォルトでは false なのでアプリケーションは落ちる)
                    e.Handled = true;
                }

                // ErrorException は BaseException を継承して意図している例外であるが、
                // アプリケーションは落ちる動きとする

            }

            MessageBox.Show(e.Exception.Message, caption, MessageBoxButton.OK, iconImage);
        }
    }
}

DispatcherUnhandledExceptionというイベントハンドラにメソッドを登録します。

DispatcherUnhandledExceptionは UI スレッドで実行されているコードで、処理されていなかった場合に発火します。

Debug.WriteLine(e.Exception.Message);となっているコードの部分で、NLog などのロギングを行ってくれるライブラリに置き換えることで実際に例外が発生した場合に原因を追求するための情報を出力することが出来ます。

また、MessageBox のアイコンや見出しなども共通化しているので、毎回try catchのブロックで MessageBox の実装をしなくて済むメリットがります。

呼び出し元の実装

コードビハインドで実装した処理です。0 を除算する例外を意図的に出しています。

"MainWindow.xaml.cs"
using Global_Exception_Handler.Exceptions;
using System;
using System.Windows;

namespace Global_Exception_Handler
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Faital_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                var zero = 0;

                var cal = 10 / zero;
            }
            catch (Exception ex)
            {
                throw new ErrorException($"Faitalが発生しました。", ex);
            }

        }

        private void Warning_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                var zero = 0;

                var cal = 10 / zero;
            }
            catch (Exception ex)
            {
                throw new WarningException($"FaitalWarningが発生しました。", ex);
            }
        }

        private void Info_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                var zero = 0;

                var cal = 10 / zero;
            }
            catch (Exception)
            {
                throw new InfoException();
            }
        }

        private void Other_Click(object sender, RoutedEventArgs e)
        {
            var zero = 0;

            // try catch で例外を補足しない
            var cal = 10 / zero;
        }
    }
}

今回作成したコードのリポジトリです。

まとめ

この例外処理の共通化は便利なので、実際アプリケーションを開発するときによく実装します。

注意点として、例外が発生してもきちんと意図したところでアプリケーションの実行を継続させているかのチェックは必要になります。

全ての例外を握りつぶしてアプリケーションの実行を継続してしまうと、思わぬ不具合に見舞われるかも知れません。