WPF バインディングエラーの読み方と出力ウィンドウを使った原因特定

WPF の Binding が動かないとき、Visual Studio の出力ウィンドウにはバインディングエラーが記録される。エラーメッセージの構造の読み方、トレースの詳細化、パターン別の対処を整理する。

概要

WPF の Binding が期待どおりに動かないとき、コントロールには何も表示されないか、既定値のままになる。
例外が発生しないため、原因の特定が難しい。
しかし WPF はバインディング失敗を無視するのではなく、その詳細を Visual Studio の 出力ウィンドウ にトレースとして記録している。
このトレースを読めば、どのプロパティに、どのデータコンテキストで、なぜバインドが失敗したかが分かる。

本記事では、バインディングエラーが出力ウィンドウに現れる仕組み、エラーメッセージの構造の読み方、トレースの詳細度を上げる方法、そして典型的なエラーパターンごとの対処を整理する。
デバッグの初期切り分けを、推測ではなくトレースに基づいて進められるようにすることを目的とする。


前提・対象環境


バインディングエラーが出力ウィンドウに現れる仕組み

WPF のバインディングは System.Diagnostics.PresentationTraceSourcesDataBindingSource を通じて診断情報を出力する。
バインドの解決に失敗すると、WPF はこのトレースソースに警告レベルのメッセージを書き込む。
このメッセージは、Visual Studio でデバッグ実行している間、出力ウィンドウ(メニューの「表示」→「出力」、ショートカット Ctrl+Alt+O)の「出力元: デバッグ」に表示される。

出力ウィンドウが表示されない場合は、次を確認する。

重要な前提として、バインディングエラーは 例外ではない
そのため try/catch では捕捉できず、プログラムの実行も止まらない。
唯一の手がかりがこのトレース出力であるため、Binding が動かないときは最初に出力ウィンドウを確認する。


バインディングエラーメッセージの構造を読む

出力ウィンドウに現れる典型的なバインディングエラーは、次の形式を持つ。
存在しないプロパティ UserNam(正しくは UserName)にバインドした場合の例を示す。

System.Windows.Data Error: 40 : BindingExpression path error:
'UserNam' property not found on 'object' ''MainViewModel' (HashCode=12345678)'.
BindingExpression:Path=UserNam; DataItem='MainViewModel' (HashCode=12345678);
target element is 'TextBox' (Name='userNameBox');
target property is 'Text' (type 'String')

このメッセージは複数の要素から成り、それぞれが原因特定の手がかりになる。

バインディングエラーのメッセージを行ごとに分解し、左端に 1 から 6 の番号を振った図。エラー番号、解決できなかったプロパティ名、探索対象の型、バインド式のパス、バインド先の要素、バインド先のプロパティの順に並んでいる。
バインディングエラーのメッセージを構成要素ごとに分解したもの。左端の番号は、次の表の # 列に対応する。同じ情報が DataItem にも現れるため、3 は 2 か所に付いている。

各要素の意味は次のとおりである。

# 要素 内容 読み取れること
1 Error: 40 エラー番号 エラーの種類(40 はパス解決失敗)
2 path error: 'UserNam' property not found 失敗の内容 どのプロパティ名が解決できなかったか
3 on 'object' ''MainViewModel' 探索対象の型 どのオブジェクト(DataItem)を探しにいったか。多くの場合は DataContext だが、Source / RelativeSource / ElementName を指定していればそちらになる
4 BindingExpression:Path=UserNam バインド式のパス XAML に書いたパス文字列
5 target element is 'TextBox' (Name='userNameBox') バインド先の要素 どのコントロールか
6 target property is 'Text' バインド先のプロパティ どの依存関係プロパティか

このエラーの読み方は次のようになる。
TextBoxuserNameBox)の Text プロパティが、MainViewModel 型のデータコンテキスト上で UserNam というプロパティを探したが見つからなかった。
したがって、タイプミスか、ViewModel 側にそのプロパティが存在しないことが原因である。
DataItem の型名が期待した ViewModel と異なる場合は、DataContext の設定漏れが疑われる。


トレースの詳細度を上げる(PresentationTraceSources.TraceLevel)

既定のトレースは失敗時のみ出力される。
バインドの解決過程を段階的に確認したい場合は、PresentationTraceSources.TraceLevel 添付プロパティを使い、対象のバインドだけ詳細度を上げる。
この添付プロパティは特定の Binding に対して設定できるため、出力を必要なバインドに絞れる。

XAML でトレースの名前空間を宣言し、対象の BindingTraceLevel=High を指定する。

<Window ...
        xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase">
    <TextBox Text="{Binding UserName,
                    diag:PresentationTraceSources.TraceLevel=High}" />
</Window>

TraceLevel=High を指定すると、そのバインドについて DataContext の解決、パスの各段階の評価、値の変換など、成功時も含めた詳細なトレースが出力される。
成功しているように見えるのに値が表示されないケースでは、この詳細トレースにより、どの段階で想定と異なる値になっているかを追跡できる。
なお、詳細トレースは出力量が多いため、切り分けが終わったら設定を外す。


よくあるエラーパターンと対処

出力ウィンドウのメッセージは、原因ごとに現れる文言が異なる。
代表的なパターンとその対処を示す。

パス解決失敗(Error: 40)

property not found を含むメッセージは、パスに指定した名前がデータコンテキスト上に存在しないことを示す。
プロパティ名のタイプミス、public になっていないアクセサ、あるいは DataContext の型の取り違えが原因になる。
メッセージ中の DataItem の型名を確認し、その型に該当プロパティが public で存在するかを照合する。

DataContext が未設定(DataItem=null)

DataItem=null と出力される場合、バインド評価時点で DataContext が設定されていない。
DataContext を設定する前にバインドが評価されると、この状態になる。
初期化順序を見直すか、要素の読み込み完了後に DataContext を設定する。
なお、後から DataContext が設定されればバインドは再評価されるため、初期化直後の一時的な DataItem=null は問題にならないこともある。

型変換の失敗(Error: 7 など)

ConvertBack cannot convert valueCannot convert を含むメッセージは、値をバインド先の型へ変換できないことを示す。
数値プロパティに文字列を双方向バインドしていて、入力値が数値に変換できない場合、ConvertBack 側の変換失敗として Error: 7 が出力される。
入力文字列を数値へ戻す IValueConverterConvertBack を実装するか、ValidationRule で入力値を検証してから変換する。
StringFormat は表示(Convert 側)の整形にのみ影響し、ConvertBack の変換失敗は解決しないため、この用途には使わない。

コレクション変更が通知されない

エラーは出ないが一覧が更新されない場合、コレクション自体の変更通知が欠けている。
List<T> は要素の増減を通知しないため、ObservableCollection<T> を使う。
個々の要素のプロパティ変更は、要素側の INotifyPropertyChanged で通知する。


トレースをファイルやコンソールに集約する(TraceListener)

出力ウィンドウはデバッグ実行中にしか使えない。
テスト環境や結合テスト中に発生するバインディングエラーを後から確認したい場合は、TraceListener を登録し、バインディングトレースをファイルやコンソールなどのリスナーへ集約する。
PresentationTraceSources.DataBindingSource にリスナーを追加すると、バインディング関連のトレースをアプリケーション側で受け取れる。

アプリケーション起動時に、DataBindingSource へリスナーとスイッチレベルを設定する。

using System.Diagnostics;
using System.Windows;

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        PresentationTraceSources.Refresh();

        var source = PresentationTraceSources.DataBindingSource;
        source.Switch.Level = SourceLevels.Warning;
        source.Listeners.Add(new TextWriterTraceListener("binding-errors.log"));
        source.Listeners.Add(new ConsoleTraceListener());
    }
}

PresentationTraceSources.Refresh() を事前に呼ぶと、トレースソースの設定が確実に反映される。
このコードにより、バインディング警告が binding-errors.log に書き出される。
ConsoleTraceListener の出力は、コンソールが割り当てられている場合(コンソールを確保した状態やデバッガ接続時)にのみ現れる。
WPF の GUI アプリは既定でコンソールを持たないため、恒久的な記録はファイルへのリスナーに任せる。
リリースビルドで常時有効にすると出力量とファイルサイズが増えるため、診断ビルドや調査時に限定して有効化する。
また、Switch.LevelWarning 未満にすると失敗トレースが記録されない点に注意する。


注意点


まとめ

WPF のバインディングエラーは例外を出さないため、出力ウィンドウのトレースが最初の手がかりになる。
切り分けの手順は次のように使い分ける。

状況 使う手段 目的
Binding が効かない(初期切り分け) 出力ウィンドウのトレース エラー番号・パス・DataItem を読む
成功しているのに値が出ない PresentationTraceSources.TraceLevel=High 解決過程を段階的に追う
デバッグ実行外で確認したい TraceListener でファイル/コンソールへ集約 実行後にログを確認する
一覧が更新されない コレクション型の見直し ObservableCollection<T> を使う

まず出力ウィンドウのメッセージから property not foundDataItem=nullCannot convert かを読み分け、原因の方向を定める。
個別バインドの解決過程を追う必要があれば TraceLevel=High を使い、デバッグ実行外で確認する必要があれば TraceListener でトレースを集約する。
これらを状況に応じて選べば、Binding が動かない問題を推測ではなくトレースに基づいて特定できる。