.NET 10 WinForms/WPF で 共通のグローバル例外ログ基盤を C# と log4net で実装する
後から思う事・・。まず、はじめに、この項目だけ読んで、当記事全体は読む価値ないと感じたら、読まなくてもいいかと思います
この記事に書いたような、大掛かりなことせんでも、
純粋に、Program.cs にて、
namespace XXX
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.ThreadException += (sender, e) =>
{
var msg = e.Exception.Message + "\n\n" + e.Exception.StackTrace;
MessageBox.Show(msg, "Unhandled Thread Exception", MessageBoxButtons.OK, MessageBoxIcon.Error);
};
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
}
}
の
Application.ThreadException += (sender, e) =>
{
var msg = e.Exception.Message + "\n\n" + e.Exception.StackTrace;
MessageBox.Show(msg, "Unhandled Thread Exception", MessageBoxButtons.OK, MessageBoxIcon.Error);
};
に相当する部分だけでした。
NuGetで、log4netをインストールした後、
この中に、例外のスタックトレースをログに出力する実装も、
加えるなどすればとは思うです
この例では、エラーの詳細まで、メッセージボックスで表示しちゃってます
それが、ユーザに見えます。
それが、まずいときも多いです。
エラーがおきたことだけ、簡易的に示すような文言だけ
メッセージボックスで表示しとくのが普通だと思うです
ただ、それだと、エンジニアにとって、何が原因か不明なので、
log4netで、ログに、例外のスタックトレースが
出力されてる状況にしとけばよいと思うです。
単にそれだけなんでしょうけど
この部分だけ、部品化したいようならば、
当記事で説明してるような、Common.Loggingのクラスライブラリを実装するするんだと思うです
それだけの話題です。大したことのない記事です。
log4netが必要になったときとか、調べるの、メンドイし、
この記事からのコピペで、ある程度のところまで、早めに行けるから
初期実装は楽できるのではないか。当記事は、それぐらいの価値しかないです。
0. 前提環境と Visual Studio 2026 での準備(UI 操作手順)
ここでは、Visual Studio Community 2026 日本語版を前提に、この記事のコードをそのまま動かすための
具体的な UI 操作手順をまとめます。
- IDE:Visual Studio Community 2026(C# / .NET デスクトップ開発ワークロードがインストール済み)
- .NET:.NET 10(WinForms / WPF プロジェクトが作成できる状態)
- ログライブラリ:log4net(NuGet 経由で導入)
補足:
NuGet パッケージのインストール方法や、プロジェクト参照の追加方法は、
Visual Studio 2022 以降で統一された UI が使われており、
Visual Studio 2026 でも同じ「右クリック → 参照の追加」「ソリューションの NuGet パッケージの管理」
という流れになっています。
0-1. ソリューション構成(3 プロジェクト)
最終的なソリューション構成は次の 3 つを想定します。
- 共通クラスライブラリ:
Common.Logging(C# クラス ライブラリ / .NET 10 -windows ターゲット) - Windows Forms アプリ:
MyWinFormsApp(.NET 10 Windows フォーム アプリ) - WPF アプリ:
MyWpfApp(.NET 10 WPF アプリ)
(1) Common.Logging プロジェクトを追加する
-
ソリューション エクスプローラーでソリューション(最上位ノード)を右クリック
-
[追加] → [新しいプロジェクト] を選択
-
「プロジェクトの作成」ダイアログで、右上の検索ボックスに
クラス ライブラリと入力 -
「クラス ライブラリ(C# / .NET 用)」テンプレートを選択して [次へ]
-
プロジェクト名に
Common.Loggingを入力
ソリューション名は(空なら)GlobalExceptionLoggingSampleなど任意で設定 -
フレームワークに .NET 10 を選択して [作成]
-
作成後に
Common.Logging.csprojを開き、<TargetFramework>などを次のように書き換えます。

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="log4net" Version="3.2.0" />
</ItemGroup>
</Project>
- net10.0 を net10.0-windows に変更する -windows を後ろに追加する
- <UseWindowsForms>true</UseWindowsForms> を追加する
- <UseWPF>true</UseWPF> を追加する
ただし、
<ItemGroup>
<PackageReference Include="log4net" Version="3.2.0" />
</ItemGroup>
の部分は、後述のNuGetでのインストールをすると自動で追加されるので、
この段階で手で追加する必要はない。
補足:
Common.LoggingではSystem.Windows.Forms(WinForms)とSystem.Windows(WPF)の両方を参照するため、
クラス ライブラリ側も Windows 専用 TFM(net10.0-windows)にしておく必要があります。
(2) WinForms プロジェクトを追加する
- ソリューション エクスプローラーでソリューション(最上位ノード)を右クリック
- [追加] → [新しいプロジェクト] を選択
- 検索ボックスに
Windows フォームと入力 - 「Windows フォーム アプリ」テンプレート(C#)を選択し [次へ]
- プロジェクト名を
MyWinFormsAppとして [次へ] - フレームワークに .NET 10 を選択して [作成]
(3) WPF プロジェクトを追加する
- ソリューションを右クリック → [追加] → [新しいプロジェクト]
- 検索ボックスに
WPFと入力 - 「WPF アプリ」テンプレート(C#)を選択し [次へ]
- プロジェクト名を
MyWpfAppとして [次へ] - フレームワークに .NET 10 を選択して [作成]
ここまでで、ソリューション内に
Common.Logging/MyWinFormsApp/MyWpfApp
の 3 プロジェクトが並んでいる状態になります。
0-2. プロジェクト参照を追加する(Common.Logging を WinForms / WPF から使えるようにする)
WinForms / WPF 側から例外ハンドラクラスを呼ぶため、
それぞれのプロジェクトから Common.Logging に プロジェクト参照を追加します。
WinForms → Common.Logging
- ソリューション エクスプローラーで
MyWinFormsAppを右クリック - [追加] → [プロジェクト参照] または [プロジェクトの依存関係] → [参照の追加] を選択
- 左側で「プロジェクト」を選択
- 一覧から
Common.Loggingにチェックを入れる - [OK] をクリック
WPF → Common.Logging
-
MyWpfAppプロジェクトを右クリック - [追加] → [プロジェクト参照]
-
Common.Loggingにチェックを入れる - [OK]
これで、MyWinFormsApp / MyWpfApp のコードから
using Common.Logging;
という using で共通クラスを参照できるようになります。
0-3. log4net を NuGet でインストールする(UI 操作)
次に、log4net を NuGet から導入します。
ここでは Common.Logging プロジェクトに対してインストールする前提で説明します。
1) UI からインストールする手順
- ソリューション エクスプローラーで
Common.Loggingを右クリック - コンテキストメニューから [NuGet パッケージの管理] を選択
(もしくは上部メニュー [ツール] → [NuGet パッケージ マネージャー] → [ソリューションの NuGet パッケージの管理] を開き、
プロジェクトとしてCommon.Loggingを選択) - 上部タブで [参照](Browse)を選択
- 検索ボックスに
log4netと入力 - 発行元が Apache Software Foundation になっている公式
log4netパッケージを選択

パッケージ詳細のタブに、

作成者: The Apache Software Foundation
と記載がある。 - 右側の「プロジェクト」で
Common.Loggingにチェックが入っていることを確認 -
[インストール] ボタンをクリック

- ライセンス確認ダイアログが出たら内容を確認し [同意する] をクリック
これで
Common.Loggingプロジェクトにlog4netの参照が追加されます。
必要に応じて、MyWinFormsApp/MyWpfApp側にも同じ手順でlog4netを入れて構いませんが、
この記事の構成では ログの実処理は Common.Logging に寄せる想定なので、
最低限 Common.Logging だけに入っていれば動作します。
2) パッケージ マネージャー コンソールから入れる場合(参考)
Visual Studio 上部メニューから:
- [ツール] → [NuGet パッケージ マネージャー] → [パッケージ マネージャー コンソール]
- 画面下部にコンソールが開くので、右上の「既定のプロジェクト」を
Common.Loggingにする - 次のコマンドを実行
Install-Package log4net
どちらの方法でも結果は同じです。UI 操作に慣れていない場合は、右クリック → [NuGet パッケージの管理] の方が分かりやすいと思います。
0-4. log4net の設定ファイル(app.config / log4net.config)を追加する
この記事のサンプルでは、もっともシンプルな構成として
- プロジェクト直下に
app.configを作るパターン - その中に
<log4net>セクションを直接書くパターン
を想定しています。
(1) app.config の追加手順(WinForms / WPF いずれも同様)
- 例外ログを出したいプロジェクトを右クリック
(典型的にはアプリ本体MyWinFormsApp/MyWpfAppのプロジェクト) - [追加] → [新しい項目] を選択
- 「新しい項目の追加」ダイアログで、左側から「C# 項目」を選択
- 一覧から [アプリケーション構成ファイル](
Application Configuration File)を選択 - 名前が
App.configであることを確認して [追加]
既に
App.config/app.configがある場合は、この手順は不要です。

(2) log4net 設定セクションを App.config に追加する
作成された App.config を開き、
次のように <log4net> セクションと <configSections> の設定を追加します。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
<file value="logs\app.log" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="10MB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline%exception" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="RollingFile" />
</root>
</log4net>
</configuration>
logsフォルダが存在しない場合は、実行時に自動作成されますが、
事前にプロジェクト直下にフォルダを作っておいても構いません。
(3) XmlConfigurator 属性を指定する
log4net に対して「設定ファイルをここから読み込め」と教えるため、
アセンブリ属性として XmlConfigurator を指定します。
手順(.NET 10 プロジェクトの場合の一例):
-
Common.Loggingプロジェクトを右クリック → [追加] → [新しい項目] - 「クラス」ではなく、「アセンブリ情報ファイル」がテンプレートにあればそれを選択
(ない場合はクラスを 1 つ作り、そこで属性を書いても構いません)
- ここでは例として
AssemblyInfo.csに次の 1 行を追加します。
using log4net.Config;
[assembly: XmlConfigurator(Watch = true)]
ConfigFile = "log4net.config"のように別ファイルを指定する構成もありますが、
この記事の例ではApp.config内の<log4net>セクションを読む前提なので、
ConfigFile指定は省略しています。
0-5. Common.Logging のコードを貼り付ける
このあと全文を掲載する「ベース記事」の Common.Logging 側コード(
GlobalExceptionLoggerWinFormsGlobalExceptionHandlerWpfGlobalExceptionHandler
)を、そのまま Common.Logging プロジェクトにクラスとして追加します。
-
Common.Loggingプロジェクトを右クリック → [追加] → [クラス] - 名前を
GlobalExceptionLogger.csにして作成 → ベース記事中のコードをそのまま貼り付け - 同様に
WinFormsGlobalExceptionHandler.cs/WpfGlobalExceptionHandler.csを追加し、
それぞれのコードを貼り付け
貼り付け先の名前空間は、記事の例では Common.Logging になっています。
プロジェクトの既定名前空間も Common.Logging に合わせておくと、そのまま動きます。
0-6. WinForms / WPF 側から Initialize() を呼び出す
最後に、アプリ側のエントリポイントから、共通ハンドラの Initialize() を 1 回だけ呼び出します。
WinForms(MyWinFormsApp / Program.cs)
using System;
using System.Windows.Forms;
using Common.Logging;
internal static class Program
{
[STAThread]
static void Main()
{
// 1) グローバル例外ハンドラの初期化
WinFormsGlobalExceptionHandler.Initialize();
// 2) 通常の WinForms 初期化
ApplicationConfiguration.Initialize();
// 3) メインフォーム実行
Application.Run(new Form1());
}
}
この Program.cs は、Visual Studio で WinForms アプリを作成したときに自動生成されるファイルに、
WinFormsGlobalExceptionHandler.Initialize(); の 1 行を追加するイメージです。
WPF(MyWpfApp / App.xaml.cs)
using System.Windows;
using Common.Logging;
namespace MyWpfApp
{
public partial class App : Application
{
public App()
{
// Application インスタンス自身を渡して初期化
WpfGlobalExceptionHandler.Initialize(this);
}
}
}
こちらも、Visual Studio が自動生成した App.xaml.cs に 1 行追加するだけです。
0-7. 動作確認のポイント
- 任意のイベントで、わざと未処理例外を投げるコードを入れる
例:ボタンクリック時にthrow new InvalidOperationException("テスト例外"); - アプリを実行し、そのボタンを押してアプリをクラッシュさせる
- プロジェクト直下(実行 EXE のカレントディレクトリ)に
logs/app.logができているか確認 -
app.logを開き、Unhandled exception in ...のメッセージとstack traceが出ていることを確認
ここまで動けば、この記事の「ベース記事」で説明している設計(共通ライブラリ+グローバル例外フック)が、
Visual Studio Community 2026 / .NET 10 の環境で実際に動いていると言えます。
この記事のゴール
.NET 10 の Windows デスクトップアプリ(Windows Forms / WPF)で、
- catch していない例外(未処理例外)を、すべて 1 か所のログ出力に集約する
- ログ出力には log4net を使う
- 各アプリケーションごとにバラバラに実装するのではなく、
共通クラスライブラリ(Common プロジェクト)に例外ハンドリング処理をまとめる
という構成を、実際に貼り付けて使えるコード付きでまとめます。
結論から言うと、
- log4net の設定ファイルが正しく置かれている
- 参照設定(プロジェクト参照・NuGet)が正しい
という前提が満たされていれば、この記事のコードは WinForms / WPF 双方で問題なく動作する構成になっています。
全体像:どのイベントで「未処理例外」を拾うか
.NET デスクトップアプリでは、「catch していない例外」を最後の砦として拾うイベントが複数あります。
WinForms
-
Application.ThreadException- Windows Forms の UI スレッドで処理されなかった例外ごとに発生するイベント。
-
AppDomain.CurrentDomain.UnhandledException- 非 UI スレッドやバックグラウンド処理など、アプリケーションドメイン全体での未処理例外を通知するイベント。
-
TaskScheduler.UnobservedTaskException-
Task/Task<T>で発生した例外のうち、await/Waitなどで観測されず GC 時に収集されるタイミングで通知されるイベント。
-
WPF
-
Application.DispatcherUnhandledException- WPF アプリのメイン UI スレッド(Dispatcher)で処理されなかった例外ごとに発生するイベント。
AppDomain.CurrentDomain.UnhandledExceptionTaskScheduler.UnobservedTaskException
つまり、
- WinForms では
Application.ThreadException - WPF では
Application.DispatcherUnhandledException - 共通部分として
AppDomain.CurrentDomain.UnhandledExceptionとTaskScheduler.UnobservedTaskException
をフックすれば、
「catch していない例外を 1 か所のログ出力(log4net)に集約する」
という目的は十分に達成できます。
設計方針
設計をシンプルに保つため、次のような方針にします。
-
Common.Logging というクラスライブラリプロジェクトを作る
-
log4netに対して実際にログを書き込むのは Common 側の責務。
-
- Common.Logging には 3 つのクラスを用意する
-
GlobalExceptionLogger
→ 例外オブジェクトと「どこで発生したか」の文字列を受け取り、log4net に書き込む。 -
WinFormsGlobalExceptionHandler
→ WinForms アプリの Program.Main から 1 回だけ呼び出して、
Application.ThreadException/AppDomain.CurrentDomain.UnhandledException/
TaskScheduler.UnobservedTaskExceptionをまとめて登録する。 -
WpfGlobalExceptionHandler
→ WPF アプリの App.xaml.cs から 1 回だけ呼び出して、
Application.DispatcherUnhandledException/AppDomain.CurrentDomain.UnhandledException/
TaskScheduler.UnobservedTaskExceptionをまとめて登録する。
-
- 各アプリ側(WinForms / WPF)は、入口で Initialize() を 1 回呼ぶだけにする
こうすることで、
- 例外ログの実装は Common.Logging 側に集中
- 各アプリ側のコード変更は最小限(Program.cs / App.xaml.cs の数行だけ)
- WinForms / WPF のどちらでも同じパターンを使い回せる
という状態を作ります。
1. Common.Logging プロジェクトの実装
まずはクラスライブラリプロジェクト Common.Logging を作成し、log4net を NuGet で追加します。
dotnet add package log4net
ここでは、
GlobalExceptionLoggerWinFormsGlobalExceptionHandlerWpfGlobalExceptionHandler
の 3 クラスを定義します。
1-1. GlobalExceptionLogger:log4net に例外を流す共通クラス
using log4net;
namespace XXPro.Common.Logging
{
/// <summary>
/// グローバル例外のログ出力とユーザー通知(エラーダイアログ)を一元化するクラス
/// </summary>
public static class GlobalExceptionLogger
{
// log4net の Logger(クラス単位で 1 つ)
private static readonly ILog Logger =
LogManager.GetLogger(typeof(GlobalExceptionLogger));
/// <summary>
/// 指定された context が「このあとアプリケーションが終了する経路」かどうかを判定する。
/// </summary>
private static bool IsTerminatingContext(string context)
{
return context switch
{
// WinForms UI スレッドは Application.ThreadException でハンドルされ、アプリは継続
"WinForms UI thread (Application.ThreadException)" => false,
// AppDomain.CurrentDomain.UnhandledException 経由は必ずアプリケーションが終了する
"WinForms non-UI thread (AppDomain.CurrentDomain.UnhandledException)" => true,
"WPF non-UI thread (AppDomain.CurrentDomain.UnhandledException)" => true,
// 現状の実装では DispatcherUnhandledException で e.Handled を true にしていないため、
// 最終的に AppDomain.CurrentDomain.UnhandledException へ流れてアプリケーションが終了する
"WPF UI thread (Application.DispatcherUnhandledException)" => true,
// TaskScheduler.UnobservedTaskException はハンドラ内で e.SetObserved() を呼んでいるため、
// 既定の例外エスカレーション(プロセス終了)は発生しない
"TaskScheduler.UnobservedTaskException" => false,
// 想定外の context は安全側に倒して「終了する」と扱う
_ => true,
};
}
/// <summary>
/// グローバル例外をログに出力し、エラーダイアログも表示する。
/// log4net 側の設定(app.config / log4net.config)は別途済んでいる前提。
/// </summary>
public static void Log(Exception? exception, string context)
{
bool willExit = IsTerminatingContext(context);
if (exception == null)
{
Logger.Error($"Unhandled exception (null) in {context}");
string message = willExit
? "エラーが発生しました。アプリケーションを終了します。"
: "エラーが発生しました。(アプリケーションは継続します)";
ShowErrorDialog(message);
}
else
{
// Error(message, exception) は stack trace 付きでログ出力される
Logger.Error($"Unhandled exception in {context}", exception);
string header = willExit
? "エラーが発生しました。アプリケーションを終了します。"
: "エラーが発生しました。(アプリケーションは継続します)";
string message =
header + "\r\n\r\n"
+ $"発生箇所: {context}\r\n"
+ $"メッセージ: {exception.Message}";
ShowErrorDialog(message);
}
}
/// <summary>
/// エラーアイコン付きのメッセージボックスを表示する共通メソッド
/// </summary>
private static void ShowErrorDialog(string message)
{
try
{
MessageBox.Show(
message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
catch
{
// ここでさらに例外が出てもどうしようもないので握りつぶす
}
}
}
}
GlobalExceptionLogger のコード解説(初心者向け)
-
public static class GlobalExceptionLogger- アプリ全体で 1 つだけ使う「例外ログの窓口」です。
staticクラスなので、毎回newせずにGlobalExceptionLogger.Log(...)のように直接呼び出せます。
- アプリ全体で 1 つだけ使う「例外ログの窓口」です。
-
private static readonly ILog Logger = LogManager.GetLogger(typeof(GlobalExceptionLogger));- log4net が提供する
ILogインターフェイスのインスタンスを 1 個だけ作って保持しています。LogManager.GetLogger(...)でクラスごとのロガーを取得し、このロガー経由でファイルなどにログが書き出されます。
- log4net が提供する
-
private static bool IsTerminatingContext(string context)- どのイベント(
Application.ThreadException/AppDomain.CurrentDomain.UnhandledException/TaskScheduler.UnobservedTaskExceptionなど)から来た例外なのかを表すcontext文字列を受け取り、「このあとアプリケーションが終了する経路かどうか」をtrue/falseで判定します。 - ここで
trueを返すケースでは、メッセージダイアログに「アプリケーションを終了します」と表示します。falseの場合は「アプリケーションは継続します」と表示し、ユーザーに「落ちるかどうか」が明確に伝わるようにしています。
- どのイベント(
-
public static void Log(Exception? exception, string context)- WinForms / WPF のすべてのグローバル例外ハンドラから呼び出される「共通処理の本体」です。
大まかな流れは次の 3 ステップです。-
bool willExit = IsTerminatingContext(context);で、このあとアプリが終了するかどうかを判定する -
Logger.Error(...)で、例外情報を log4net に書き出す -
ShowErrorDialog(...)で、ユーザーにエラーダイアログを表示する
-
-
exception == nullの場合も考慮しており、そのときはスタックトレースなしのシンプルなエラーメッセージを表示するようになっています。
- WinForms / WPF のすべてのグローバル例外ハンドラから呼び出される「共通処理の本体」です。
-
private static void ShowErrorDialog(string message)- 実際に
MessageBox.Showを呼び出す小さな共通メソッドです。
WinForms / WPF のどちらから呼ばれても使えるように、常に「OK ボタン 1 つ」「エラーアイコン付き」のメッセージボックスとして固定しています。
- 実際に
1-2. WinFormsGlobalExceptionHandler:WinForms 用のグローバル例外ハンドラ
namespace XXPro.Common.Logging
{
/// <summary>
/// Windows Forms アプリで未処理例外を一括ログするための初期化クラス
/// </summary>
public static class WinFormsGlobalExceptionHandler
{
private static bool _initialized;
/// <summary>
/// Program.Main から 1 回だけ呼び出す。
/// </summary>
public static void Initialize()
{
if (_initialized) return;
_initialized = true;
// UI スレッドの未処理例外を Application.ThreadException に流す設定
// (最初のウィンドウを作る前に呼ぶ必要がある)
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
// 1) UI スレッドの未処理例外
Application.ThreadException += (sender, e) =>
{
GlobalExceptionLogger.Log(
e.Exception,
"WinForms UI thread (Application.ThreadException)");
};
// 2) 非 UI スレッド・プロセスレベルの未処理例外
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
var ex = e.ExceptionObject as Exception;
GlobalExceptionLogger.Log(
ex,
"WinForms non-UI thread (AppDomain.CurrentDomain.UnhandledException)");
};
// 3) 未観測 Task の例外
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
GlobalExceptionLogger.Log(
e.Exception,
"TaskScheduler.UnobservedTaskException");
e.SetObserved(); // 既定の「プロセス終了」ポリシーを防ぐ
};
}
}
}
WinFormsGlobalExceptionHandler のコード解説(初心者向け)
- このクラスの役割
-
Program.MainからWinFormsGlobalExceptionHandler.Initialize();を 1 回呼ぶだけで、-
Application.ThreadException(UI スレッド) -
AppDomain.CurrentDomain.UnhandledException(非 UI スレッド・プロセスレベル) -
TaskScheduler.UnobservedTaskException(未観測タスクの例外)
に対するグローバル例外ハンドラを一括で登録し、最終的にすべてGlobalExceptionLogger.Log(...)に流す「初期化専用クラス」です。
-
-
-
private static bool _initialized;とif (_initialized) return;-
Initialize()が誤って複数回呼ばれても同じイベントに重複登録されないようにするためのフラグです。
.NET のイベントは+=を繰り返すとその分だけハンドラが増えてしまうため、1 回だけ登録する仕組みを用意しています。
-
-
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);- WinForms では、これを最初のフォームを作る前に呼ぶと「UI スレッドで発生した未処理例外を
Application.ThreadExceptionイベントに流す」モードになります。
これにより、UI 例外をすべて共通の場所で捕まえられるようになります。
- WinForms では、これを最初のフォームを作る前に呼ぶと「UI スレッドで発生した未処理例外を
-
Application.ThreadException += (sender, e) => { ... };- WinForms の UI スレッド上で catch されなかった例外がこのイベントに流れてきます。
ここではe.Exceptionと"WinForms UI thread (Application.ThreadException)"という context 文字列をGlobalExceptionLogger.Logに渡し、「UI スレッドで起きた例外」としてログとダイアログを出しています。
- WinForms の UI スレッド上で catch されなかった例外がこのイベントに流れてきます。
-
AppDomain.CurrentDomain.UnhandledException += (sender, e) => { ... };- バックグラウンドスレッドなど、UI 以外で発生した未処理例外はこのイベントで通知されます。
e.ExceptionObjectはException型とは限らないため、as Exceptionでキャストした上でGlobalExceptionLogger.Logに渡しています。
- バックグラウンドスレッドなど、UI 以外で発生した未処理例外はこのイベントで通知されます。
-
TaskScheduler.UnobservedTaskException += (sender, e) => { ... };-
Task/Task<T>の例外で、await/Wait/Resultなどで誰にも観測されないまま GC されそうになったときに発生するイベントです。 - ここでも
GlobalExceptionLogger.Logに例外を流したあと、e.SetObserved();を呼ぶことで「この例外はハンドル済み」とマークし、.NET の既定の「プロセス終了」挙動を止めています。
-
1-3. WpfGlobalExceptionHandler:WPF 用のグローバル例外ハンドラ
namespace XXPro.Common.Logging
{
/// <summary>
/// WPF アプリで未処理例外を一括ログするための初期化クラス
/// </summary>
public static class WpfGlobalExceptionHandler
{
private static bool _initialized;
/// <summary>
/// App.xaml.cs のコンストラクタか OnStartup から 1 回だけ呼び出す。
/// </summary>
public static void Initialize(System.Windows.Application app)
{
if (_initialized) return;
_initialized = true;
if (app == null) throw new ArgumentNullException(nameof(app));
// 1) UI スレッド(Dispatcher)の未処理例外
app.DispatcherUnhandledException += (sender, e) =>
{
GlobalExceptionLogger.Log(
e.Exception,
"WPF UI thread (Application.DispatcherUnhandledException)");
// アプリを落としたくない場合は、ポリシーに応じてここで e.Handled = true; を検討
// e.Handled = true;
};
// 2) 非 UI スレッド・プロセスレベルの未処理例外
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
var ex = e.ExceptionObject as Exception;
GlobalExceptionLogger.Log(
ex,
"WPF non-UI thread (AppDomain.CurrentDomain.UnhandledException)");
};
// 3) 未観測 Task の例外
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
GlobalExceptionLogger.Log(
e.Exception,
"TaskScheduler.UnobservedTaskException");
e.SetObserved();
};
}
}
}
WpfGlobalExceptionHandler のコード解説(初心者向け)
- このクラスの役割
-
App.xaml.csのコンストラクタやOnStartupからWpfGlobalExceptionHandler.Initialize(this);を 1 回だけ呼ぶことで、-
Application.DispatcherUnhandledException(WPF の UI スレッド/Dispatcher) -
AppDomain.CurrentDomain.UnhandledException(非 UI スレッド・プロセスレベル) -
TaskScheduler.UnobservedTaskException(未観測タスクの例外)
に対するグローバル例外ハンドラをまとめて登録し、すべてGlobalExceptionLogger.Log(...)に流すクラスです。
-
-
-
public static void Initialize(System.Windows.Application app)のapp引数- ここで受け取っている
appは WPF アプリケーションのインスタンスそのものです。
app.DispatcherUnhandledExceptionにアクセスするためには、このインスタンスが必須なので、nullで渡された場合はArgumentNullExceptionを投げて明示的にエラーにしています。
- ここで受け取っている
-
app.DispatcherUnhandledException += (sender, e) => { ... };- WPF の UI スレッド(Dispatcher)で catch されなかった例外がこのイベントに流れてきます。
ここではGlobalExceptionLogger.Logに例外を渡しつつ、context として"WPF UI thread (Application.DispatcherUnhandledException)"を指定しています。 - コメントにある通り、「UI 例外が起きてもアプリを落としたくない」ポリシーにしたい場合は、ここで
e.Handled = true;を設定することで、例外発生後もアプリを継続させることができます。
- WPF の UI スレッド(Dispatcher)で catch されなかった例外がこのイベントに流れてきます。
-
AppDomain.CurrentDomain.UnhandledException += (sender, e) => { ... };- WPF でもバックグラウンドスレッドなど UI 以外で発生した未処理例外は、このイベントで通知されます。
WinForms の場合と同様に、e.ExceptionObject as ExceptionでキャストしてからGlobalExceptionLogger.Logに渡しています。
- WPF でもバックグラウンドスレッドなど UI 以外で発生した未処理例外は、このイベントで通知されます。
-
TaskScheduler.UnobservedTaskException += (sender, e) => { ... };- 非同期タスクの未観測例外を拾うためのイベントです。WinForms 側と同じように、
GlobalExceptionLogger.Logでログとダイアログを出したあと、e.SetObserved();で既定のプロセス終了を防いでいます。
- 非同期タスクの未観測例外を拾うためのイベントです。WinForms 側と同じように、
2. 各アプリ側からの呼び出し
2-1. Windows Forms アプリ側(Program.cs)
using System;
using System.Windows.Forms;
using Common.Logging;
internal static class Program
{
[STAThread]
static void Main()
{
// 1) グローバル例外ハンドラの初期化
WinFormsGlobalExceptionHandler.Initialize();
// 2) 通常の WinForms 初期化
ApplicationConfiguration.Initialize();
// 3) メインフォーム実行
Application.Run(new Form1());
}
}
Program.cs のコード解説(初心者向け)
-
internal static class Program- WinForms アプリのエントリポイント(入り口)を定義するクラスです。Visual Studio のテンプレートで自動生成される
Programクラスをベースにしています。
- WinForms アプリのエントリポイント(入り口)を定義するクラスです。Visual Studio のテンプレートで自動生成される
-
[STAThread]属性- Windows の UI コンポーネント(WinForms や WPF)は「シングルスレッドアパートメント(STA)」というモードで動くことが前提なので、エントリポイントの
Mainメソッドにはこの属性が必須です。
- Windows の UI コンポーネント(WinForms や WPF)は「シングルスレッドアパートメント(STA)」というモードで動くことが前提なので、エントリポイントの
-
WinFormsGlobalExceptionHandler.Initialize();- 自作した
WinFormsGlobalExceptionHandlerの初期化を、アプリ起動直後に 1 回だけ呼び出しています。
ここでグローバル例外ハンドラのイベント登録が行われるため、以降の処理で発生した未処理例外はすべて共通のGlobalExceptionLogger.Logに流れます。
- 自作した
-
ApplicationConfiguration.Initialize();/Application.EnableVisualStyles();など(テンプレートに応じて)- ここでは WinForms アプリの見た目や高 DPI 設定などを初期化します。Visual Studio 2022 以降では
ApplicationConfiguration.Initialize();というメソッドにまとめられている場合もあります。
- ここでは WinForms アプリの見た目や高 DPI 設定などを初期化します。Visual Studio 2022 以降では
-
Application.Run(new Form1());- 実際にメインフォーム(ここでは
Form1)を作成し、そのフォームが閉じられるまでメッセージループを回し続けるメソッドです。
この呼び出し以降に UI スレッドで起きた未処理例外は、WinFormsGlobalExceptionHandler.Initializeで登録したApplication.ThreadExceptionからGlobalExceptionLoggerに渡されます。
- 実際にメインフォーム(ここでは
2-2. WPF アプリ側(App.xaml.cs)
using System.Windows;
using Common.Logging;
namespace MyWpfApp
{
public partial class App : Application
{
public App()
{
// Application インスタンス自身を渡して初期化
WpfGlobalExceptionHandler.Initialize(this);
}
}
}
App.xaml.cs のコード解説(初心者向け)
-
public partial class App : Application- WPF アプリケーション全体を表すクラスです。XAML 側の
App.xamlと対になっており、アプリのライフサイクル(起動・終了など)を管理します。
- WPF アプリケーション全体を表すクラスです。XAML 側の
-
public App()コンストラクタ- アプリケーションインスタンスが作成された直後に 1 回だけ呼ばれます。
ここでWpfGlobalExceptionHandler.Initialize(this);を呼び出すことで、WPF 全体のグローバル例外ハンドラをセットアップしています。
- アプリケーションインスタンスが作成された直後に 1 回だけ呼ばれます。
-
OnStartupを使う場合のイメージ- コンストラクタではなく
protected override void OnStartup(StartupEventArgs e)をオーバーライドして、その中でWpfGlobalExceptionHandler.Initialize(this);を呼び出す書き方もよく使われます。
いずれの場合も、「アプリのウィンドウを開く前に 1 回だけ初期化する」という点が重要です。
- コンストラクタではなく
3. log4net 側の設定のイメージ
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
<file value="logs\app.log" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="10MB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger - %message%newline%exception" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="RollingFile" />
</root>
</log4net>
</configuration>
using log4net.Config;
[assembly: XmlConfigurator(Watch = true)]
4. 注意点・限界
- すべてのケースで「アプリが落ちない」ことを保証するものではない。
-
AppDomain.CurrentDomain.UnhandledExceptionはプロセス終了直前の通知イベントであり、
ここでログを書いてもアプリ自体は終了するケースがある。
-
-
TaskScheduler.UnobservedTaskExceptionは GC やファイナライザのタイミングに依存して発火する。 - WPF の
DispatcherUnhandledExceptionでe.Handledを true にするかどうかは、
アプリ側のポリシーに応じて決める必要がある。
5. まとめ
- WinForms / WPF で「catch していない例外を 1 か所で log4net に流す」には、
- WinForms:
Application.ThreadException - WPF:
Application.DispatcherUnhandledException - 共通:
AppDomain.CurrentDomain.UnhandledException/TaskScheduler.UnobservedTaskException
をフックする構成が実務的な標準パターン。
- WinForms:
- これらのイベント登録と log4net へのログ出力を Common.Logging にまとめてしまえば、
各アプリ側はInitialize()を 1 回呼ぶだけで済む。 - 本記事のコードは、
- Common.Logging プロジェクト
- WinForms プロジェクト
- WPF プロジェクト
に貼り付けていけば、そのまま動作する構成になっている。
ログの出力先やメッセージ内容、アプリ継続可否のポリシーなどは、
各プロジェクトごとに GlobalExceptionLogger や各ハンドラクラス内で調整していく形になります。
Discussion