概要
WPF の双方向バインディングでは、TextBox に入力した文字が即座に ViewModel のプロパティへ反映されないことがある。
文字を入力してもコマンドや別コントロールの表示が古い値のまま更新されず、原因が分からないまま PropertyChanged の実装を疑ってしまう場面が典型である。
この挙動の中心にあるのが Binding.UpdateSourceTrigger であり、TextBox.Text の既定値が他コントロールと異なることが混乱の元になる。
LostFocus / PropertyChanged / Explicit の三値がソース更新のタイミングをどう変えるかを、原因となる設計背景から解き、IME 入力・検証タイミング・フォーカスを奪わないボタンという実務上の落とし穴まで実装例で示す。
前提・対象環境
- フレームワーク/言語: .NET 8 / C# 12(
UpdateSourceTriggerは .NET Framework 3.0 以降の WPF で利用可能) - 対象コントロール・機能:
TextBox.Textの双方向バインディング(Mode=TwoWay/OneWayToSource) - アーキテクチャ: MVVM(ViewModel のプロパティを View の
TextBoxへバインド) - 前提知識:
INotifyPropertyChangedによる変更通知、基本的なデータバインディング
UpdateSourceTrigger は、双方向(TwoWay)または OneWayToSource のバインディングでのみ意味を持つ。
ターゲット(TextBox.Text)からソース(ViewModel)へ値を書き戻す方向の「タイミング」を決める設定であり、ソースからターゲットへの表示更新には影響しない。
問題
ViewModel のプロパティに TextBox をバインドし、その値を使う「保存」ボタンや別の TextBlock を用意する。
ユーザーが TextBox に文字を入力し、まだ入力欄にカーソルがある状態で、フォーカスを移動させない操作(後述の Focusable="False" のボタンなど)で確定すると、ViewModel 側のプロパティには入力前の古い値が入っている。
<!-- 既定のバインディング。UpdateSourceTrigger を指定していない -->
<TextBox Text="{Binding UserName, Mode=TwoWay}" />
<Button Content="保存" Command="{Binding SaveCommand}" Focusable="False" />
上記で Focusable="False" のボタンをクリックすると、TextBox はフォーカスを失わないため、入力した文字が UserName に反映されないまま SaveCommand が実行される。
INotifyPropertyChanged は正しく実装しているのに値が届かない、という形で表面化する。
UserName の初期値は suzuki で、入力欄にフォーカスを残したまま sato へ書き換えた状態。右側は同じ UserName を OneWay で表示している。既定(上)ではフォーカスが外れていないためソースが更新されず、PropertyChanged(下)では即座に反映される。原因・背景
原因は、TextBox.Text の既定 UpdateSourceTrigger が LostFocus である点にある。
UpdateSourceTrigger の既定は Default であり、これは「ターゲット依存プロパティごとに定義された既定の更新タイミング」を意味する。
多くの依存プロパティ(CheckBox.IsChecked など)ではこの既定が PropertyChanged だが、TextBox.Text に限っては LostFocus が既定になっている。
これは意図的な設計である。
テキスト入力のたびにソースを更新すると、1 文字ごとに変更通知・検証・関連処理が走り、性能を損なう。
また、確定前に入力を修正(バックスペース)する機会をユーザーから奪ってしまう。
このため WPF は、TextBox がフォーカスを失った時点でまとめてソースを更新する LostFocus を既定に選んでいる。
ある依存プロパティの既定値はコードでも確認できる。
DependencyProperty.GetMetadata で取得したメタデータの DefaultUpdateSourceTrigger を見ればよい。
// TextBox.Text の既定 UpdateSourceTrigger を取得する
var metadata = (FrameworkPropertyMetadata)TextBox.TextProperty.GetMetadata(typeof(TextBox));
UpdateSourceTrigger def = metadata.DefaultUpdateSourceTrigger; // => LostFocus
この結果が LostFocus であることが、上記の問題が起きる根拠である。
フォーカスが移動しない限りソース更新は起きない。
解決方法
タイミングを制御するには、バインディングに UpdateSourceTrigger を明示する。
選べる値は次の 3 つであり、更新の契機がそれぞれ異なる。
PropertyChanged—TextBox.Textが変わるたび(1 文字入力ごと)に即座にソースを更新する。LostFocus—TextBoxがフォーカスを失った時点でソースを更新する(TextBox.Textの既定)。Explicit— アプリが明示的にUpdateSource()を呼んだ時にのみソースを更新する。
問題のケース(入力途中でボタン実行しても値を届けたい)では、PropertyChanged を指定すれば入力ごとに反映される。
一方、送信ボタンを押したときにだけまとめて確定したいフォームでは Explicit が適する。
実装例
入力の即時反映が必要な場合は、UpdateSourceTrigger=PropertyChanged を指定する。
検索ボックスやチャット入力のように、1 文字ごとの反映が自然な UI に向く。
<!-- 入力のたびに UserName へ反映する -->
<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />
この指定により、TextBox がフォーカスを保持したままでも 1 文字入力するたびに UserName が更新される。
入力の確定をユーザーの操作(送信ボタン)まで遅延させたい場合は、Explicit を指定する。
まず XAML で TextBox に x:Name を付与し、バインディングの UpdateSourceTrigger を Explicit にする。
<!-- 明示的な更新に切り替える -->
<TextBox x:Name="userNameBox" Text="{Binding UserName, UpdateSourceTrigger=Explicit}" />
付与した名前を使い、コードビハインドから対象の BindingExpression を取得し、任意のタイミングで UpdateSource() を呼んでソースを更新する。
// 送信ボタンのクリック時などに呼び出す
BindingExpression be = userNameBox.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
Explicit では UpdateSource() を呼ばない限りソースは一切更新されない。
呼び忘れると値が永遠に反映されないため、送信処理の先頭で確実に呼ぶ設計にする。
注意点
- IME 変換中の即時更新:
PropertyChangedは日本語入力の変換中(未確定文字列)でもソースを更新するため、確定前の中間文字列が ViewModel へ流れ込む。変換確定を待ってから処理したい場合はLostFocusにする。後述のDelayは更新頻度を抑えるだけで、未確定文字列の流入自体は防げない点に注意する。 - 検証(Validation)のタイミング:
ValidationRulesはBindingに付き、ValidationStep(既定RawProposedValue)に応じてソース更新の前後で走るため、UpdateSourceTriggerの契機に連動する。一方INotifyDataErrorInfoは ViewModel 側でソース更新後に検証し、結果はErrorsChangedの通知で反映されるため、非同期検証では更新契機と表示タイミングが一致しないことがある。LostFocusでは入力欄を離れるまで、PropertyChangedでは 1 文字ごとにソース更新(とValidationRules)が走る。 Delayによる抑制:PropertyChangedの過剰な更新は、Binding.Delay(.NET Framework 4.5 以降)で最後の入力から指定ミリ秒後に 1 回だけ更新するよう抑制できる。例:{Binding UserName, UpdateSourceTrigger=PropertyChanged, Delay=500}。- フォーカスを移動させない確定操作:
TextBoxがフォーカスを失わず、かつUpdateSource()も呼ばれない経路では、既定のLostFocusでソース更新が起きない。クリックで起動するFocusable="False"のボタン、Enter で起動する既定ボタン(IsDefault="True")、アクセスキーがこれに該当する。Focusable="False"はボタンへのフォーカス移動を防ぐだけで、IsDefaultやアクセスキーによる起動自体は妨げない点にも注意する。この経路で確定する UI ではPropertyChangedかExplicitを使う。 x:Bindとの違い: WPF の{Binding}はExplicitを含む 3 値をサポートする。UWP/WinUI の{x:Bind}はExplicitを持たない点が異なるため、他プラットフォームの記事を参照する際は混同しない。
代替案・比較
TextBox.Text のソース更新タイミングは、UI の性質に応じて次のように選ぶ。
| 値 | 更新の契機 | メリット | デメリット | 適するケース |
|---|---|---|---|---|
LostFocus(既定) |
フォーカスを失った時 | 入力確定後にまとめて更新・検証できる | フォーカスが移らないと反映されない | 通常の入力フォーム、フォーカス移動で確定する UI |
PropertyChanged |
1 文字入力ごと | 入力が即座に反映される | 更新頻度が高く IME 中間文字列も流入 | 検索ボックス、リアルタイムプレビュー、チャット入力 |
Explicit |
UpdateSource() 呼び出し時 |
確定タイミングを完全に制御できる | 呼び忘れると反映されない | 送信ボタンで一括確定する編集フォーム |
PropertyChanged の即時性を保ちつつ更新頻度を抑えたい場合は、Delay を併用して最後の入力から一定時間後に 1 回だけ更新する構成が有効である。
まとめ
TextBox の入力が ViewModel へ届かない問題の大半は、TextBox.Text の既定 UpdateSourceTrigger が LostFocus であることに起因する。
フォーカスが移動せず UpdateSource() も呼ばれなければソースは更新されないため、Focusable="False" のボタン・IsDefault="True" の既定ボタン・アクセスキーで確定する UI では値が古いまま処理が走る。
入力を即座に反映したい検索・プレビュー系では PropertyChanged、送信ボタンで一括確定する編集フォームでは Explicit、フォーカス移動で自然に確定する通常フォームでは既定の LostFocus を選ぶ。
PropertyChanged の更新頻度が問題になる場合は Delay で抑制するが、Delay は IME 未確定文字列の流入を防ぐものではなく、変換確定後にだけ処理したい場合は LostFocus を選ぶ。
更新タイミングは ValidationRules の実行時点を左右する(INotifyDataErrorInfo の結果は ErrorsChanged で別途反映される)ため、UpdateSourceTrigger は入力体験と検証設計の両面から選択する。
なお、Explicit にしたバインディングを View から書き戻す UpdateSource() の呼び出し自体の落とし穴(GetBindingExpression が null を返す条件・複数バインディングの一括更新・UpdateTarget() との違い)は、WPF で TextBox の UpdateSource を View から呼び出すときの落とし穴と実装で扱う。