WPFアプリ開発において重要なアーキテクチャパターンであるMVVM(Model-View-ViewModel)について学びます。
この記事では、MVVMパターンの基本的な概念とメリットを理解することに専念し、実際のクラス設計と実装を通じて役割分担(責務分離)の効果を体感していきます。
以下の方に役立つ内容となっています。
- WPFアプリの設計パターンを学びたい方
- MVVMパターンの各層の責務を理解したい方
- コードの保守性・再利用性・テスタビリティを向上させたい方
前回までのデータバインディングとコマンドが、MVVMパターンを支える2本柱でしたね。今回はそれらを活用した設計パターンを学びます!
MVVMパターンは、WPF以外にもMAUI、WinUI3、AvaloniaUIとかでも使われている大事な考え方だね!
これまでのWPF実践入門編シリーズの各記事、特にデータバインディング、コマンドの基礎についてあらかじめ学んでおくと、理解が深まります。


前回の記事(第8回)で「UI」と「ロジックとデータ」の役割を分離したクラス設計を行いました。今回これをベースに、更なる役割の分離を進めたものであるMVVMを学びます。
演習のコード一式はGitHubに置いてあります。
動画も作成しています。
講義:MVVMパターンとは?
これまでのアプローチの問題
これまでの記事では、以下のような構成でカウンターアプリを作成してきました。

データバインディングとコマンドの仕組みによって、「UI」と「データとロジック」の分離は実現できました。
しかし、Counterクラスに「UI用の処理」が混在しているという問題がまだ残っているのです。
以下のように、CounterクラスにはWPFの実装に依存したコード(プロパティ通知やコマンド)が含まれていますね。
public class Counter : INotifyPropertyChanged
{
...
public ICommand IncrementCommand => _incrementCommand;
...
public event PropertyChangedEventHandler? PropertyChanged;
...
}
これにより、具体的には次のような問題が生じます。
- 「データ+ロジック」を再利用しにくい
(例)別UIフレームワーク(Blazor、MAUIなど)で再利用しにくい - 純粋な「データ+ロジック」のみのテストを行いにくい
Counterクラスはもうちょっと役割で分割できる余地がある、ということなんですね。
MVVMパターンによる解決
MVVM(Model-View-ViewModel)パターンは、アプリを以下の3つの層に分離する設計パターンです。

View→ViewModel→Modelという方向で依存しています。逆方向への依存はNGです。
この分離により、以下のように役割を明確化できます。
- View層 (CounterWindow.xaml/.xaml.cs)
- UI要素の定義(XAML)
- DataContextの設定のみ(コードビハインド)
- ViewModel層 (CounterViewModel.cs)
- プロパティ変更通知 (INotifyPropertyChanged)
- コマンドの公開 (ICommand)
- Modelを操作
- Model層 (CounterModel.cs) 【UIフレームワーク非依存】
- 純粋なビジネスロジック
- データの保持と操作
Model層のCounterModelクラスはUI実装に依存しないコードになっていて、例えばBlazorとか別フレームワークでも再利用できるんだね!
その通りです!あらためて、MVVMパターンのメリットを整理してみましょう。
MVVMパターンのメリット
MVVMパターンで View・ViewModel・Model を分けることで、次のようなメリットがあります。
「UI」と「データとロジック」が分離される
UIの見た目(XAML)とアプリの動作が分かれるため、デザイナーと開発者が同時に作業しやすくなります。(「View」 と 「View Model/Model」分離の利点)
「データとロジック」を再利用できる
ModelはUIフレームワークに依存しないため、他の環境(Blazor、MAUI、コンソールアプリなど)でも流用可能です。(「View/ViewModel」と「Model」分離の利点)
テストがしやすい
ModelやViewModelはUI依存がないため、単体テストを簡単に書けます。
特に ViewModelをテスト すると、「UIを起動せずに、UI操作に近い挙動」を確認できます。
UIを含めたEndToEndテストは実装/実行にコストがかかるため、これは嬉しい点ですね。
保守性・拡張性が高まる
役割が明確に分かれているため、修正や追加の影響範囲を限定できます。
例えば「UIを新しく作り直す」「ロジックを改良する」といった変更が、他の層に波及しにくくなります。
演習:カウンターアプリをMVVMで再構築
作成するアプリの概要
前回作成したカウンターアプリを、MVVMパターンに従って新規プロジェクトとして再構築します。
主要なクラスは以下のような構成になります。(アプリの動作は前回と全く同じです)
WpfCounterApp/
├── CounterWindow.xaml (View)
├── CounterWindow.xaml.cs (View - コードビハインド)
├── CounterViewModel.cs (ViewModel)
├── CounterModel.cs (Model)
└── SimpleCommand.cs (汎用コマンドクラス)
…
今回、MVVMの基本的な概念理解に専念するため、プロジェクトフォルダ直下へ全てのコードを配置します。
MVVMにおけるより実践的なフォルダ配置については次回説明をします。
以下の手順で作成します。
- 手順1:SimpleCommand.csを作成(汎用コマンド)
- 手順2:CounterModel.csを作成(Model)
- 手順3: CounterViewModel.cs (ViewModel)
- 手順4:CounterWindow.xaml(View)
手順1:SimpleCommand.csを作成(汎用コマンド)
WpfCounterAppという名前のWPF新規プロジェクトを作成し、前回と同じ汎用コマンドクラスとして「SimpleCommand.cs」を作成します。
using System.Windows.Input;
namespace WpfCounterApp
{
public class SimpleCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;
public SimpleCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
SimpleCommandは、CommunityToolkit.MvvmにおけるRelayCommand相当のものです。
手順2: CounterModel.cs(Model)
UIフレームワークに一切依存しない、純粋な「データとロジック」です。
namespace WpfCounterApp
{
public class CounterModel
{
public int Value { get; private set; }
public void Increment()
{
Value++;
}
public bool CanDecrement()
{
return Value > 0;
}
public void Decrement()
{
if (CanDecrement())
{
Value--;
}
}
}
}
手順3: CounterViewModel.cs (ViewModel)
ViewとModelの橋渡しを行うクラスです。
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace WpfCounterApp
{
public class CounterViewModel : INotifyPropertyChanged
{
private readonly CounterModel _model;
private readonly SimpleCommand _incrementCommand;
private readonly SimpleCommand _decrementCommand;
public CounterViewModel()
{
_model = new CounterModel();
_incrementCommand = new SimpleCommand(_ => ExecuteIncrement());
_decrementCommand = new SimpleCommand(_ => ExecuteDecrement(), _ => _model.CanDecrement());
}
public int Count => _model.Value;
public ICommand IncrementCommand => _incrementCommand;
public ICommand DecrementCommand => _decrementCommand;
private void ExecuteIncrement()
{
_model.Increment();
OnPropertyChanged(nameof(Count));
}
private void ExecuteDecrement()
{
_model.Decrement();
OnPropertyChanged(nameof(Count));
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
if (propertyName == nameof(Count))
{
_decrementCommand.RaiseCanExecuteChanged();
}
}
}
}
純粋な「データとロジック」に関する役割は、CounterModelクラスへ任せています。
手順4: CounterWindow.xaml(View)
以下のステップでMainWindow(WPF新規プロジェクトひな型に含まれる)をCounterWindowで置き換えましょう。
- MainWindow.xamlを削除
- CounterWindow.xamlを追加
- App.xamlで、初期起動画面をCounterWindow.xamlへ修正
①MainWindow.xamlを削除
ソリューションエクスプローラ上でMainWindow.xamlを削除します。●●.xamlを削除すると対応するコードビハインド(●●.xaml.cs)も自動削除されます。
②CounterWindow.xamlを追加
新しい項目で「ウィンドウ(WPF)」を選択して「CounterWindow」という名前で追加しましょう。

CounterWindow.xamlは前回と同様、以下のようにします。
<Window x:Class="WpfCounterApp.CounterWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfCounterApp"
mc:Ignorable="d"
Title="CounterWindow" Height="450" Width="800">
<StackPanel Margin="20" VerticalAlignment="Center">
<!-- カウンター表示 -->
<TextBlock Text="{Binding Count, StringFormat='Count: {0}'}"
FontSize="24"
HorizontalAlignment="Center"
Margin="0,0,0,20"/>
<!-- ボタン群 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="-"
Command="{Binding DecrementCommand}"
Width="40" Height="30"/>
<Button Content="+"
Command="{Binding IncrementCommand}"
Width="40" Height="30"/>
</StackPanel>
<!-- 説明テキスト -->
<TextBlock Text="「-」ボタンは0以下で自動無効化されます"
HorizontalAlignment="Center"
Margin="0,15,0,0" />
</StackPanel>
</Window>
そして、コードビハインド(CounterWindow.xaml.cs)で、DataContextとしてViewModelであるCounterViewModelのインスタンスを設定します。
using System.Windows;
namespace WpfCounterApp
{
/// <summary>
/// CounterWindow.xaml の相互作用ロジック
/// </summary>
public partial class CounterWindow : Window
{
public CounterWindow()
{
InitializeComponent();
DataContext = new CounterViewModel();
}
}
}
③App.xamlで、初期起動画面をCounterWindow.xamlへ修正
最後にApp.xamlで「StartupUri=”CounterWindow.xaml”」と修正しましょう。これで、アプリ起動時にCounterWindowが表示されます。
<Application x:Class="WpfCounterApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCounterApp"
StartupUri="CounterWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
これで、MVVM版のカウンターアプリが完成です!
アプリを実行
アプリを実行すると、前回と同じ機能を持つカウンターアプリが動作します。

しかし、内部的には以下のような役割分離が実現されています。
- CounterModel: カウンターの値と操作ロジック(WPFに依存しない)
- CounterViewModel: WPFとの橋渡し(データバインディング・コマンド)
- CounterWindow: UI表示(XAML + 最小限のコードビハインド)
前回と見た目は同じだけど、内部では明確に役割分担しているんだね!
その通りです!
今回は非常にシンプルな例で、「MVVMパターンのHello World」的なものですね。これからさらに理解を深めていきましょう。
まとめ
本記事では、WPFアプリ開発における重要な設計パターンであるMVVM(Model-View-ViewModel)について学びました。
MVVMパターンによる役割分離には以下の特徴と利点があります。
- 明確な責務の分離:View(UI)、ViewModel(UIとの橋渡し)、Model(純粋なデータとロジック)の3層に分けることで、それぞれの役割が明確になる
- 再利用性の向上:ModelはUIフレームワークに依存しないため、Blazor、MAUI、コンソールアプリなど他の環境でも流用できる
- テスタビリティの改善:UIを起動せずにViewModelやModelの単体テストが可能で、開発効率が大幅に向上する
- 保守性・拡張性の向上:役割が分離されているため、UIの変更やロジックの修正が他の層に波及しにくい
カウンターアプリの演習を通じて、これまでの「UI」と「データとロジック」の2層構造から、より洗練された3層構造への発展を実践しました。
次回は、MVVMパターンにおけるフォルダ構成の基本について解説予定です。
引き続き、一緒にWPFアプリ開発における実践的な設計パターンを学んでいきましょう!