WinUIで少し大きめのアプリを作り始めると、必ず「画面のコードがどんどん膨らんで、収拾がつかない」壁にぶつかります。私も最初はボタンのクリックイベントに処理を直書きし続け、あとで手に負えなくなりました。その処方箋が MVVM です。
この記事では、開発ロードマップのステップ4にあたる MVVMの考え方と、最小コードで始める型を、初心者向けに解説します。難しい理屈より「なぜ必要で、どう書くか」に絞って進めます。
Table of contents
Open Table of contents
そもそもMVVMとは
MVVMは、アプリを3つの役割に分ける設計パターンです。
- Model … データや業務ロジック(例: 計算・保存・通信)
- View … 画面そのもの(XAML)。表示に専念する
- ViewModel … ViewとModelの橋渡し。画面に表示する値や、ボタンが押されたときの処理を持つ
ポイントはただ一つ、「画面(View)には見た目だけを置き、処理はViewModelに寄せる」こと。まずはこの一点だけ押さえれば十分です。
なぜMVVMが必要か(Before / After)
MVVMを使わないと、処理が画面のコード(コードビハインド)に溜まっていきます。たとえばカウンターを作るとき、こう書きがちです。
// Before:画面のコードに直書き
private int _count;
private void Button_Click(object sender, RoutedEventArgs e)
{
_count++;
CountText.Text = _count.ToString(); // UI部品を直接いじる
}
一見動きますが、①ロジックがUIと密結合してテストできない ②再利用できない ③画面が育つほどコードが肥大化するという問題を抱えます。
MVVMにすると、処理はViewModelへ移り、画面はViewModelを見るだけになります。
// After:処理はViewModelへ。画面はデータを表示するだけ
public partial class CounterViewModel : ObservableObject
{
[ObservableProperty]
private int count;
[RelayCommand]
private void Increment() => Count++;
}
UI部品を直接触るコードが消え、ロジックが独立しました。これがMVVMの効果です。
手書きは大変:だからCommunityToolkit.Mvvm
MVVMの肝は、「ViewModelの値が変わったら画面に自動で反映される」仕組み(INotifyPropertyChanged)です。ところが、これを手書きすると定型コードが大量になります。プロパティ1つごとに変更通知を書くのは、正直かなり面倒です。
そこで使うのが CommunityToolkit.Mvvm。この定型コードを属性を付けるだけで自動生成してくれる、MVVMの必須級ライブラリです。NuGetで追加できます(入門記事のNuGet紹介も参照)。
3つの主役だけ覚える
CommunityToolkit.Mvvmは機能豊富ですが、最初は次の3つだけで十分です。先ほどのCounterViewModelが、この3つでできています。
ObservableObject… ViewModelが継承する土台クラス。変更通知の仕組みを提供します。[ObservableProperty]… フィールドに付けると、変更通知つきのプロパティを自動生成。private int count;からCountプロパティが作られます。[RelayCommand]… メソッドに付けると、ボタンなどにバインドできるコマンドを自動生成。Increment()からIncrementCommandが作られます。
クラスに partial を付けるのを忘れないでください(自動生成コードと合体させるため)。たったこれだけで、変更通知もコマンドも書かずに済みます。
ViewとViewModelのつなぎ方(x:Bind)
あとは画面(XAML)から、ViewModelの値とコマンドをバインドでつなぐだけです。WinUIでは高速な x:Bind が推奨です。
<TextBlock Text="{x:Bind ViewModel.Count, Mode=OneWay}" />
<Button Content="+1" Command="{x:Bind ViewModel.IncrementCommand}" />
- 値の表示は
x:Bind。変化を反映したいのでMode=OneWay - ボタンは
CommandにIncrementCommand(自動生成された名前)をつなぐ
これで「ボタンを押す→ViewModelのIncrementが走る→Countが変わる→画面が自動更新」という流れが、画面側にロジックを一切書かずに実現します。
WPFのMVVMとの違い
WPF経験者がWinUIでMVVMを書くと、いくつか戸惑う差分があります。ここは実際につまずいたポイントです。
Bindingよりx:Bindが基本 … WinUIでは型安全で高速なx:Bindが推奨。書き方や既定のモードがWPFのBindingと少し違います(x:Bindの既定はOneTime)。- コマンドの自作が不要に … WPFでは
ICommandを自前実装しがちでしたが、[RelayCommand]で不要になります。 - ViewModelの渡し方が違う … 次に説明する「バインドの探し先」の違いから、
DataContextにセットするWPF流とは作法が変わります。
一番の違いは「バインドが値をどこから探すか」です。
WPFの {Binding} は、実行時に DataContext を起点に値を探します。だからMVVMでは、画面にViewModelをDataContextとして流し込むのが定石でした。
// WPF:DataContext に ViewModel をセット
DataContext = new CounterViewModel();
<TextBlock Text="{Binding Count}" />
一方WinUIの {x:Bind} は DataContext を見ず、**そのページの分離コード(this)**を起点にします。{x:Bind Count} は「ページクラスに Count があるか」を探すのです。そのためViewModelにつなぐには、ViewModelをページの公開プロパティとして出すのが基本形になります。
// WinUI:ページの公開プロパティとして ViewModel を出す
public sealed partial class MainPage : Page
{
public CounterViewModel ViewModel { get; } = new();
public MainPage() => InitializeComponent();
}
<TextBlock Text="{x:Bind ViewModel.Count, Mode=OneWay}" />
要点を対比するとこうなります。
WPF {Binding} | WinUI {x:Bind} | |
|---|---|---|
| 値の探し先 | DataContext | ページの分離コード(this) |
| ViewModelの渡し方 | DataContext = vm | public ViewModel { get; } を公開 |
| 解決タイミング | 実行時(リフレクション) | コンパイル時(型安全・高速) |
| 既定モード | OneWay | OneTime |
(WinUIでも従来型の {Binding}+DataContext は使えますが、推奨は型安全な x:Bind です。)考え方(ViewとModelの分離)は同じでも、つなぎ方の道具がモダンになっている、と捉えると移行がスムーズです。
まとめ
MVVMは難しく見えて、要点は「画面には見た目、処理はViewModel」の一言に尽きます。
- なぜやるか … UIとロジックを分離し、肥大化とテスト不能を防ぐ
- 何でやるか … CommunityToolkit.Mvvm で定型コードを自動生成
- どう書くか …
ObservableObject+[ObservableProperty]+[RelayCommand]の3点セット - どうつなぐか … 画面から
x:Bindでバインド
まずは今回のカウンターのような小さな例で、この型を一度自分の手で書いてみてください。動かせれば、あとはプロパティやコマンドを増やしていくだけです。次回は、WPFからWinUIへ移行する際の具体的な勘所を整理します。
