連載Index(読む順・公開済リンクが最新)
S00_門前の誓い_総合Index
WPFでMVVMを書くときは、DataContext と Binding の関係を先に押さえておくと、XAMLとViewModelのつながりを追いやすくなります。
ここが曖昧なままだと、ViewModelにプロパティを書いても、XAML側が何を見ているのか分かりにくくなります。
たとえば、XAMLに次のように書いたとします。
<TextBox Text="{Binding CustomerName}" />
この CustomerName は、XAMLの中だけで完結する名前ではありません。
ViewModel側にあるプロパティ名です。
public string CustomerName { get; set; } = "";
では、XAMLはどうやってViewModelを見に行くのか。
そこで使うのが DataContext です。
DataContext = new CustomerViewModel();
WPFのMVVMでは、まず次のつながりを作ります。
View
↓ DataContext
ViewModel
↓ Binding
XAMLの表示・入力・操作
この記事では、小さな入力画面を作りながら、WPFのMVVMでよく使う次の要素を整理します。
DataContextBindingButton.CommandINotifyPropertyChangedRelayCommand
この記事は WPFのMVVM実装編 です。
先に全体像を押さえるなら、
C#のMVVM入門|WPF/MAUIに共通するViewModel・Binding・Commandの考え方【外伝G27】
XAMLの要素名・属性・Bindingの読み方を確認するなら、
XAML読み方辞典|要素・属性・Binding・CommandとC#側へのつながりを整理する【外伝G28】
この記事で扱うこと
この記事では、WPFでMVVMを組むときの基本を扱います。
-
DataContextでViewModelを画面に渡す -
TextBox.TextとViewModelのプロパティをBindingする -
TextBlock.TextにViewModelの値を表示する -
Button.CommandからViewModelの処理を呼ぶ -
INotifyPropertyChangedで画面へ変更を伝える - コードビハインドに書くもの、書かない方がよいものを分ける
DI、画面遷移、Prism、ReactiveProperty、CommunityToolkit.Mvvmの属性構文までは深く扱いません。
まずは、WPF標準の仕組みでMVVMの流れを確認します。
今回作る画面
今回作るのは、顧客名を入力して保存するだけの小さな画面です。
顧客名
[ ]
[ 保存 ]
保存しました。
この画面では、次の3つをViewModelにつなぎます。
| 画面側 | ViewModel側 | 役割 |
|---|---|---|
TextBox.Text |
CustomerName |
入力された顧客名 |
Button.Command |
SaveCommand |
保存ボタンの処理 |
TextBlock.Text |
Message |
結果メッセージ |
WPFでは、このつながりを Binding で書きます。
<TextBox Text="{Binding CustomerName}" />
<Button Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding Message}" />
ただし、Bindingを書くだけではViewModelにはつながりません。
画面側にViewModelを渡す必要があります。
その入口が DataContext です。
DataContext = new CustomerViewModel();
WPFのMVVMでは、まずここを押さえると理解しやすくなります。
DataContext でViewModelを渡す。
Binding でViewModelのプロパティを見る。
Command でViewModelの処理を呼ぶ。
DataContextはBindingが見るViewModel
DataContext は、Bindingが値を探しに行く相手です。
たとえば、画面側で次のように設定します。
DataContext = new CustomerViewModel();
この状態で、XAMLに次のBindingがあるとします。
<TextBox Text="{Binding CustomerName}" />
この場合、WPFは DataContext に設定された CustomerViewModel の中から CustomerName を探します。
この1行は、次の順につながっています。
TextBox.Text
↓
{Binding CustomerName}
↓
DataContext に設定された CustomerViewModel
↓
CustomerViewModel.CustomerName
つまり、DataContext を設定することで、XAMLとViewModelがつながります。
Bindingを書いているのに値が出ない場合、まず DataContext を確認します。
XAML側の名前が合っていても、ViewModelが渡されていなければBindingは期待通りに動きません。
ファイルの置き場所
今回のサンプルでは、役割ごとにファイルを分けます。
SampleApp
├ Views
│ └ MainWindow.xaml
├ ViewModels
│ └ CustomerViewModel.cs
├ Services
│ └ CustomerService.cs
├ Models
│ └ SaveResult.cs
└ Commands
└ RelayCommand.cs
目的は、コードを探しやすくすることです。
| 場所 | 置くもの |
|---|---|
Views |
XAML画面 |
ViewModels |
画面に表示する値、画面操作の入口 |
Services |
保存や取得などの処理 |
Models |
処理結果やデータ |
Commands |
Command用の共通クラス |
大事なのはフォルダ名そのものではありません。
見た目は Views、入力値やメッセージは ViewModels、保存判断は Services のように、変更理由が違うものを同じ場所に詰め込みすぎないことです。
ViewModelを書く
ここから、画面とつながるViewModelを書きます。
今回のViewModelに必要なのは3つです。
| メンバー | XAML側 | 役割 |
|---|---|---|
CustomerName |
TextBox.Text |
入力された顧客名 |
Message |
TextBlock.Text |
画面に表示するメッセージ |
SaveCommand |
Button.Command |
保存ボタンの処理 |
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using SampleApp.Commands;
using SampleApp.Services;
namespace SampleApp.ViewModels;
public class CustomerViewModel : INotifyPropertyChanged
{
private readonly CustomerService _customerService = new();
private string _customerName = "";
private string _message = "";
public CustomerViewModel()
{
SaveCommand = new RelayCommand(Save);
}
public string CustomerName
{
get => _customerName;
set
{
if (_customerName == value)
{
return;
}
_customerName = value;
OnPropertyChanged();
}
}
public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}
_message = value;
OnPropertyChanged();
}
}
public ICommand SaveCommand { get; }
private void Save()
{
var result = _customerService.Save(CustomerName);
Message = result.Success
? "保存しました。"
: result.Message;
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
// BindingしているViewへ「値が変わった」ことを通知する
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
このViewModelは、XAML側から次のように参照されます。
<TextBox Text="{Binding CustomerName}" />
<Button Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding Message}" />
XAMLのBinding名とViewModelのプロパティ名は対応します。
{Binding CustomerName} と書くなら、ViewModel側に CustomerName が必要です。
画面更新にはINotifyPropertyChangedを使う
ViewModelの値を変えたとき、画面にも反映したい場合があります。
たとえば、保存後に次のように Message を変えます。
Message = "保存しました。";
この変更を画面へ伝えるために、INotifyPropertyChanged を使います。
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Message のsetterでは、値を変えたあとに OnPropertyChanged() を呼んでいます。
public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}
_message = value;
OnPropertyChanged();
}
}
これで、TextBlock.Text にBindingしている画面側へ変更が伝わります。
<TextBlock Text="{Binding Message}" />
流れはこうです。
Message を変更
↓
OnPropertyChanged()
↓
Bindingへ通知
↓
TextBlock.Text が更新される
ViewModelの値を変更しても画面が変わらない場合、OnPropertyChanged() を呼んでいるか確認します。
RelayCommandを書く
WPFの Button.Command にBindingするには、ViewModel側に ICommand を用意します。
ここでは、最小限の RelayCommand を作ります。
using System;
using System.Windows.Input;
namespace SampleApp.Commands;
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return _canExecute?.Invoke() ?? true;
}
public void Execute(object? parameter)
{
_execute();
}
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
ViewModelでは、この RelayCommand に Save メソッドを渡しています。
public CustomerViewModel()
{
SaveCommand = new RelayCommand(Save);
}
XAML側では、ボタンの Command にBindingします。
<Button Content="保存"
Command="{Binding SaveCommand}" />
流れはこうです。
Button.Command
↓ Binding
SaveCommand
↓ Execute
Save()
この RelayCommand は、Commandの流れを説明するための最小版です。
ボタンの有効/無効を動的に切り替える場合は、CanExecute と CanExecuteChanged の扱いも追加で考えます。
保存処理はServiceへ分ける
保存処理そのものは、ViewModelに直接書きすぎないようにします。
今回は、CustomerService に分けます。
using SampleApp.Models;
namespace SampleApp.Services;
public class CustomerService
{
public SaveResult Save(string customerName)
{
if (string.IsNullOrWhiteSpace(customerName))
{
return SaveResult.Failed("顧客名を入力してください。");
}
// 本来はここでDB保存やAPI呼び出しなどを行う
return SaveResult.Succeeded();
}
}
結果を表すクラスも用意します。
namespace SampleApp.Models;
public class SaveResult
{
public bool Success { get; }
public string Message { get; }
private SaveResult(bool success, string message)
{
Success = success;
Message = message;
}
public static SaveResult Succeeded()
{
return new SaveResult(true, "");
}
public static SaveResult Failed(string message)
{
return new SaveResult(false, message);
}
}
ViewModelでは、Serviceの結果を受け取り、画面状態である Message に反映します。
private void Save()
{
var result = _customerService.Save(CustomerName);
Message = result.Success
? "保存しました。"
: result.Message;
}
Serviceで判定する。
ViewModelで画面に出す状態へ変える。
XAMLはBindingで表示する。
この流れにすると、責務が混ざりにくくなります。
Viewを書く
次に、WPFのXAMLを書きます。
MainWindow.xaml です。
<Window x:Class="SampleApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="Customer Sample"
Height="220"
Width="360">
<StackPanel Margin="20">
<TextBlock Text="顧客名" />
<TextBox
Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}"
Width="240"
HorizontalAlignment="Left" />
<Button
Content="保存"
Command="{Binding SaveCommand}"
Width="120"
Margin="0,12,0,0"
HorizontalAlignment="Left" />
<TextBlock
Text="{Binding Message}"
Margin="0,12,0,0" />
</StackPanel>
</Window>
このXAMLとViewModelの対応はこうです。
| XAML | ViewModel | 役割 |
|---|---|---|
TextBox.Text |
CustomerName |
顧客名の入力 |
Button.Command |
SaveCommand |
保存処理の実行 |
TextBlock.Text |
Message |
結果メッセージの表示 |
XAMLの {Binding CustomerName} は、ViewModelの CustomerName を見に行きます。
そのためには、Viewの DataContext にViewModelが設定されている必要があります。
DataContextにViewModelを設定する
XAMLにBindingを書いたら、画面側にViewModelを渡します。
MainWindow.xaml.cs で、DataContext にViewModelを設定します。
using System.Windows;
using SampleApp.ViewModels;
namespace SampleApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new CustomerViewModel();
}
}
これで、XAMLのBindingが CustomerViewModel を参照できるようになります。
MainWindow
↓ DataContext
CustomerViewModel
↓ Binding
CustomerName / Message / SaveCommand
XAMLのBindingが正しく見えても、DataContext が設定されていなければ値は出ません。
WPFのBinding不調では、まず DataContext を確認します。
TextBox.TextのUpdateSourceTrigger
WPFの TextBox.Text では、入力した値がViewModelへ反映されるタイミングに注意が必要です。
今回のXAMLでは、次のように書いています。
<TextBox Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" />
UpdateSourceTrigger=PropertyChanged を付けると、入力するたびにViewModelの CustomerName へ反映されます。
指定しない場合、TextBox.Text はフォーカスが外れたタイミングで反映される動きになりやすいです。
| 書き方 | ViewModelへ反映されるタイミング |
|---|---|
Text="{Binding CustomerName}" |
主にフォーカスが外れたとき |
Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" |
入力するたび |
入力中の値をすぐViewModelへ反映したい場合は、UpdateSourceTrigger=PropertyChanged を付けると分かりやすいです。
Bindingの追い方
WPFでBindingを追うときは、次の順で見ると迷いにくいです。
例として、このXAMLを見ます。
<TextBox Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" />
追い方はこうです。
| 順番 | 見る場所 | 内容 |
|---|---|---|
| 1 | TextBox |
入力欄 |
| 2 | Text |
入力欄の文字プロパティ |
| 3 | {Binding CustomerName} |
Binding先の名前 |
| 4 | DataContext |
どのViewModelを見るか |
| 5 | ViewModel |
CustomerName プロパティを探す |
流れで書くとこうです。
TextBox.Text
↓ Binding
CustomerName
↓ DataContext
CustomerViewModel.CustomerName
Bindingで迷ったら、XAMLだけを見続けない方がよいです。
DataContext とViewModel側のプロパティ名までセットで追います。
Commandの追い方
次に、Commandを追います。
<Button Content="保存"
Command="{Binding SaveCommand}" />
追い方はこうです。
| 順番 | 見る場所 | 内容 |
|---|---|---|
| 1 | Button |
ボタン |
| 2 | Command |
ボタン操作の入口 |
| 3 | {Binding SaveCommand} |
Binding先のCommand |
| 4 | ViewModel |
SaveCommand プロパティを探す |
| 5 | ViewModelのコンストラクタ |
SaveCommand = new RelayCommand(Save) を見る |
| 6 | Save() |
実際に呼ばれる処理を見る |
流れで書くとこうです。
Button.Command
↓ Binding
SaveCommand
↓ RelayCommand
Save()
↓
CustomerService.Save(CustomerName)
↓
Message更新
↓
TextBlock.Textへ反映
Commandは、XAML側の操作とViewModel側の処理をつなぐ入口です。
Button.Command から SaveCommand、さらに Save() へ追うと流れが見えます。
コードビハインドに書くもの
MVVMだからといって、コードビハインドを完全にゼロにする必要はありません。
ただし、何を書くかは分けた方がよいです。
コードビハインドに書いてよいものは、主に画面そのものに関係する処理です。
| 書いてよいもの | 理由 |
|---|---|
InitializeComponent() |
Viewの初期化だから |
DataContext の設定 |
ViewとViewModelをつなぐ入口だから |
| 画面固有のUI制御 | Viewに閉じた処理だから |
今回のコードビハインドはこれだけです。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new CustomerViewModel();
}
}
一方で、コードビハインドに集めない方がよいものもあります。
| 避けたいもの | 理由 |
|---|---|
| 入力チェック | ViewModelやServiceへ寄せたい |
| 保存処理 | Serviceへ寄せたい |
| DBアクセス | RepositoryやServiceへ寄せたい |
| 複雑な業務判断 | ModelやServiceへ寄せたい |
| 状態管理 | ViewModelへ寄せたい |
MVVMで避けたいのは、コードビハインドに業務処理や状態管理が集まることです。
コードビハインドをゼロにすること自体が目的ではありません。
よくある詰まりどころ
ここでは、WPFのMVVMでよく詰まるところを整理します。
Bindingしているのに値が出ない
まず見るところは DataContext です。
DataContext = new CustomerViewModel();
次に、Binding名とViewModelのプロパティ名が一致しているか見ます。
<TextBlock Text="{Binding Message}" />
public string Message { get; set; } = "";
Massage と Message のようなスペル違いでもBindingは期待通りに動きません。
まずはBinding名とプロパティ名を確認します。
値を変えても画面が更新されない
ViewModelが INotifyPropertyChanged を実装しているか確認します。
public class CustomerViewModel : INotifyPropertyChanged
さらに、setterで OnPropertyChanged() を呼んでいるか見ます。
_message = value;
OnPropertyChanged();
TextBoxの入力値がすぐViewModelに入らない
UpdateSourceTrigger=PropertyChanged が必要か確認します。
<TextBox Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" />
Buttonを押しても処理が呼ばれない
Command のBinding先を確認します。
<Button Command="{Binding SaveCommand}" />
ViewModel側に SaveCommand があるか確認します。
public ICommand SaveCommand { get; }
さらに、コンストラクタで処理を登録しているか確認します。
SaveCommand = new RelayCommand(Save);
ViewModelに書くもの、書かないもの
WPFのMVVMでは、ViewModelに何を書くかが大事です。
ViewModelに書きやすいものは次です。
| 内容 | 理由 |
|---|---|
| 画面に表示する値 | 画面状態だから |
| 入力中の値 | 画面状態だから |
| 選択中の項目 | 画面状態だから |
| Command | 画面操作の入口だから |
| 画面表示用のメッセージ | 画面状態だから |
逆に、ViewModelに集めすぎない方がよいものは次です。
| 内容 | 理由 |
|---|---|
| DB接続 | 外部アクセスだから |
| API通信 | 通信処理だから |
| ファイル保存 | I/O処理だから |
| 複雑な業務ルール | ServiceやModelへ分けたいから |
| UI部品そのものの操作 | Viewに依存するから |
ViewModelは、画面状態と操作の入口を持つ場所です。
業務処理や外部アクセスを全部抱える場所ではありません。
今回の全体の流れ
今回の画面全体の流れをもう一度並べます。
MainWindow
↓ DataContext
CustomerViewModel
↓
CustomerName / Message / SaveCommand
入力時の流れです。
TextBox.Text
↓ Binding
CustomerName
↓
CustomerViewModel.CustomerName
保存ボタンを押したときの流れです。
Button.Command
↓ Binding
SaveCommand
↓
Save()
↓
CustomerService.Save(CustomerName)
↓
Message更新
↓
OnPropertyChanged()
↓
TextBlock.Text更新
この流れが見えると、WPFのMVVMで「どこからどこへつながっているか」を追いやすくなります。
まとめ
WPFのMVVMでは、まず DataContext と Binding を押さえることが大事です。
DataContext は、Bindingが見るViewModelを決めます。
DataContext = new CustomerViewModel();
XAML側では、ViewModelのプロパティへBindingします。
<TextBox Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding Message}" />
<Button Command="{Binding SaveCommand}" />
対応関係はこうです。
| XAML | ViewModel |
|---|---|
TextBox.Text |
CustomerName |
TextBlock.Text |
Message |
Button.Command |
SaveCommand |
DataContext |
CustomerViewModel |
値を画面へ反映するには、INotifyPropertyChanged で変更通知を出します。
OnPropertyChanged();
ボタン操作は、Command を使ってViewModelへ渡します。
Button.Command
↓
SaveCommand
↓
Save()
MVVMは、コードを難しくするためのものではありません。
画面、画面状態、業務処理を混ぜないための整理方法です。
WPFでは、次の3つを最初に押さえると進めやすくなります。
-
DataContextでViewModelを渡す -
BindingでViewModelの値を画面につなぐ -
Commandで画面操作をViewModelへ渡す
WPFのMVVMで迷ったら、まず次を追います。
DataContext は何か。
Binding はどのプロパティを見ているか。
Command はどの処理につながっているか。
この3点を見ると、原因を絞りやすくなります。
関連記事
MVVMの共通概念はこちらです。
XAMLの読み方はこちらです。
MVVMの前提となる画面設計の整理はこちらです。
UIスレッドや非同期処理が絡む場合はこちらも関連します。
次に読む
この流れで読むと、WPFとMAUIの違いが整理しやすくなります。
- G30: .NET MAUIのMVVM入門|BindingContextとCommandで画面処理を分ける
連載Index(読む順・公開済リンクが最新)
S00_門前の誓い_総合Index