前回はMVVMパターンの実践的なフォルダ構成とサービス層について学びました。今回は、複数のViewModelで1つのModelを共有する方法を学習します。
実際のアプリ開発では、複数の画面やコンポーネントが同じデータを扱うことがよくあります。MVVMパターンでこれをどう実現するかを理解することが、本記事のゴールです。
以下の方に役立つ内容となっています。
- MVVMでコードを適切に分割したい
- 複数のViewModelで同じModelを共有する方法を知りたい
- View・ViewModel・Modelの適切な構築方法を知りたい
- 依存性注入の実例を学びたい
この記事では、前回作成したカウンターアプリをベースに、偶数/奇数を表示する新しいView/ViewModelを追加し、複数のViewModelが1つのModelを共有する実装を行います。
MVVM解説の記事①・②から続けて見ていただくと、理解が深まるかと思います。


今回は複数のViewModelを扱う方法を学びましょう!実務ではとても重要なテクニックです。
演習のコード一式はGitHubに置いてあります。
動画も作成しています。
講義:複数ViewModel間でのModelの共有
前回の振り返り
前回は、MVVMパターンの実践的な設計として以下を学習しました。
- 適切なフォルダ構成による整理(Commands、Models、Services、ViewModels、Views)
- サービス層を使った外部連携(JSONファイルによる永続化)
現在のアプリは1つのViewModel(CounterViewModel)のみで以下のように構成されていますが、

実際のアプリでは複数のViewModelを扱うことが一般的です。
複数のViewModelを使うとどんなメリットがあるの?
1つの画面でも、機能ごとにViewModelを分けることで、それぞれの役割が明確になり、コードの可読性や再利用性が上がるのです。
今回、その方法を学びましょう!
今回の演習で作る機能
前回のカウンターアプリにおいてカウンターの値が「偶数/奇数」のどちらかであるかを表示する機能を追加すると考えましょう。以下の2つの機能をもつアプリになります。
- (a)カウンターの増減操作と表示
- (b)カウンターの「偶数/奇数」表示
以下が画面の完成イメージです。

それぞれの機能ごとにView/ViewModelを作ると以下の構成になります。→は依存の方向です。(View→ViewModel→Modelという一方向の依存関係にします)

重要なポイントは以下です。
- 1つのModelを2つのViewModelで共有
- カウンターの値が変更されたら、両方のViewModelが更新される
(そして、それぞれのViewも更新される) - CounterViewにはカウンター値と増減ボタン
- EvenOddViewには偶数/奇数の表示
この仕組みを実現するために必要な要素技術・概念について説明します。その後に、演習で具体的にアプリを作ります。
必要な要素技術・概念
(1)Modelの変更通知の仕組み
複数のViewModelで同じModelを共有する場合、Modelの値が変更されたことを各ViewModelに通知する必要があります。
このために、Modelには変更通知用のイベントを持たせます。
public class CounterModel
{
public int Value { get; private set; }
public event Action? ValueChanged; // ★変更通知用のイベント
public void Increment()
{
Value++;
ValueChanged?.Invoke(); // ★ViewModelへ変更を通知
}
}
そして、各ViewModelは、Modelの変更通知イベントを購読します。
public class CounterViewModel
{
private readonly CounterModel _model;
public CounterViewModel(CounterModel model, ...)
{
_model = model;
_model.ValueChanged += OnCountChanged; // ★変更通知を購読
}
private void OnCountChanged()
{
OnPropertyChanged(nameof(Count)); // ★Viewに変更を通知
}
}
なるほど!Modelが変更されたら、それを購読している全てのViewModelに通知が届き、Viewも更新されるってことだね。
(2)ユーザコントロールを使ってViewを部品化
今回のアプリでは、MainWindow(Window) の中に、CounterViewとEvenOddView(ユーザコントロール) を配置します。
以下のようなイメージです。views:CounterView、views:EvenOddViewがユーザコントロールです。
...
<StackPanel Margin="20" VerticalAlignment="Center">
<views:CounterView x:Name="CounterView" Margin="0,0,0,20"/>
<Separator Margin="20,0" Background="LightGray" Height="1"/>
<views:EvenOddView x:Name="EvenOddView" Margin="0,20,0,0"/>
</StackPanel>
...
ユーザコントロールは「自分で定義したコントロール」です。コントロールについては以下で学習しました。

今回のように、1つの画面を機能ごとに分割する場合、ユーザコントロールが便利です。
(3)コンポジションルートと依存性注入
今回、複数のView、ViewModel、Modelを生成してそれらを組み立てる(依存関係を構築する)必要があります。
このような依存関係を構築する場所をコンポジションルートと呼びます。WPFアプリでは、App.xaml.csのOnStartupメソッドがコンポジションルートとして適しています。
protected override void OnStartup(StartupEventArgs e)
{//ここが、コンポジションルート
base.OnStartup(e);
// サービスを作成
var storage = new JsonCounterStorage();
// Modelを作成
var model = new CounterModel();
// ViewModelを作成(依存性注入)
var counterViewModel = new CounterViewModel(model, storage);
var evenOddViewModel = new EvenOddViewModel(model);
// MainWindowを作成し、ViewModelを設定(依存性注入)
var mainWindow = new MainWindow();
mainWindow.CounterView.SetViewModel(counterViewModel);
mainWindow.EvenOddView.SetViewModel(evenOddViewModel);
// ウィンドウを表示
mainWindow.Show();
}
コンポジションルートを決めて依存関係の構築を1箇所に集約すると、コードの見通しが良くなります。
このように、必要な部品(ModelやStorageなど)を外側で作り、ViewModelやViewに「渡して組み立てる」設計を「依存性注入(Dependency Injection: DI)」と呼びます。
(modelとか、storageとか、)ViewModelの中でnewするんじゃなくて、外側から渡すんだね。
今回の例では、ModelやStorageを App.xaml.cs
で生成し、それらをコンストラクタ引数やメソッド引数としてViewModel・Viewに渡しています。
これにより、各クラスが自分で依存するオブジェクトを new
せずに済むため、再利用性やテストのしやすさが高まります。
演習:カウンターアプリに偶数/奇数表示機能を追加
講義で説明した通り、前回作成したカウンターアプリへ「偶数/奇数表示機能」を追加します。
カウンター用・偶数/奇数表示用のView/ViewModelをそれぞれ作成し、カウンター値を保持するモデルは共有させます。
以下の手順で作成します。
- 手順1:CounterModel修正
- 手順2:EvenOddViewModel、EvenOddViewを作成
- 手順3:CounterViewModel、CounterViewを作成
- 手順4:MainWindow.xamlを作成
- 手順5:App.xaml.csでコンポジションルートを実装

講義で学んだ考え方を活用して、コードを書いていきましょう!
手順1:CounterModel修正
CounterModel.csを以下のように修正します。
namespace WpfCounterApp.Models
{
public class CounterModel
{
public int Value { get; private set; }
public event Action? ValueChanged;
public CounterModel(int initialValue = 0)
{
Value = initialValue;
}
private void Notify() => ValueChanged?.Invoke();
public void SetValue(int value)
{
Value = value;
Notify();
}
public void Increment()
{
Value++;
Notify();
}
public void Decrement()
{
if (!CanDecrement()) return;
Value--;
Notify();
}
public bool CanDecrement()
{
return Value > 0;
}
}
}
Model内でValueChanged
イベントを定義し、値変更時にNotify()
で一括通知することで、複数のViewModelに変更を伝達できるようにしています。
初期値設定時も通知が必要なため、SetValue
メソッドを使います。これにより、すべてのViewModelに初期状態が確実に伝わります。
手順2:EvenOddViewModel、EvenOddViewを作成
ViewModelとしてEvenOddViewModel.csを作成します。
using System.ComponentModel;
using System.Runtime.CompilerServices;
using WpfCounterApp.Models;
namespace WpfCounterApp.ViewModels
{
public class EvenOddViewModel : INotifyPropertyChanged
{
private readonly CounterModel _model;
public EvenOddViewModel(CounterModel model)
{
_model = model;
// ★モデルの変更を購読
_model.ValueChanged += OnCountChanged;
}
public string EvenOddText => _model.Value % 2 == 0 ? "Even" : "Odd";
private void OnCountChanged()
{
OnPropertyChanged(nameof(EvenOddText));
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ModelのValueChanged
イベントを購読し、カウンター値の変化に応じてEvenOddText
プロパティを更新する仕組みを実装しています。
次に、EvenOddViewをユーザコントロールとして追加します。
ソリューションエクスプローラで「Views」フォルダを右クリックし、「追加>新しい項目>ユーザコントロール(WPF)」で追加しましょう。
EvenOddView.xamlは以下のように修正します。
<UserControl x:Class="WpfCounterApp.Views.EvenOddView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfCounterApp.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel Margin="20">
<TextBlock Text="Even/Odd"
FontWeight="Bold"
HorizontalAlignment="Center"
Margin="0,0,0,10"/>
<TextBlock Text="{Binding EvenOddText}"
FontSize="18"
HorizontalAlignment="Center"/>
</StackPanel>
</UserControl>
EvenOddView.xaml.csは以下のように修正します。
using System.Windows.Controls;
using WpfCounterApp.ViewModels;
namespace WpfCounterApp.Views
{
/// <summary>
/// EvenOddView.xaml の相互作用ロジック
/// </summary>
public partial class EvenOddView : UserControl
{
public EvenOddView()
{
InitializeComponent();
}
public void SetViewModel(EvenOddViewModel viewModel)
{
DataContext = viewModel;
}
}
}
これで、EvenOddViewModelに基づいて、EvenOddViewへ偶数/奇数の判定結果が表示されます。
手順3:CounterViewModel、CounterViewを作成
CounerViewModelについては、もともとのViewModelへ以下を追加します。
…
public class CounterViewModel : INotifyPropertyChanged
{
…
// ★コンストラクタで依存関係を受け取る
public CounterViewModel(CounterModel model, JsonCounterStorage storage)
{
_model = model;
_storage = storage;
var initialValue = _storage.Load();
_model.SetValue(initialValue);
// ★モデルの変更を購読
_model.ValueChanged += OnCountChanged;
_incrementCommand = new SimpleCommand(_ => ExecuteIncrement());
_decrementCommand = new SimpleCommand(_ => ExecuteDecrement(), _ => _model.CanDecrement());
}
...
private void ExecuteIncrement()
{
_model.Increment();
// ★ 削除:OnPropertyChanged(nameof(Count));
_storage.Save(_model.Value);
}
private void ExecuteDecrement()
{
_model.Decrement();
// ★ 削除:OnPropertyChanged(nameof(Count));
_storage.Save(_model.Value);
}
// ★Modelの変更通知を受け取る
private void OnCountChanged()
{
OnPropertyChanged(nameof(Count));
}
…
}
}
CounterViewをユーザコントロールとして作成します。
CounterView.xamlを以下のように修正します。内容的には、もとのアプリにおけるCounterWindowと同じものですね。
<UserControl x:Class="WpfCounterApp.Views.CounterView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfCounterApp.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel Margin="20">
<TextBlock Text="Counter"
FontWeight="Bold"
HorizontalAlignment="Center"
Margin="0,0,0,10"/>
<TextBlock Text="{Binding Count}"
FontSize="24"
HorizontalAlignment="Center"
Margin="0,0,0,15"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="-"
Command="{Binding DecrementCommand}"
Width="40" Height="30"
Margin="5"/>
<Button Content="+"
Command="{Binding IncrementCommand}"
Width="40" Height="30"
Margin="5"/>
</StackPanel>
</StackPanel>
</UserControl>
CounterView.xaml.csは以下のように修正します。
using System.Windows.Controls;
using WpfCounterApp.ViewModels;
namespace WpfCounterApp.Views
{
/// <summary>
/// CounterView.xaml の相互作用ロジック
/// </summary>
public partial class CounterView : UserControl
{
public CounterView()
{
InitializeComponent();
}
public void SetViewModel(CounterViewModel viewModel)
{
DataContext = viewModel;
}
}
}
Modelとストレージを外部から受け取り、ValueChanged
イベントを購読してViewに反映することで、状態変更を自動同期できるようにしています。
手順4:MainWindow.xamlを作成
まず、前回のCounterWindow.xaml
をMainWindow.xaml
にリネームします。
ソリューションエクスプローラでCounterWindow.xamlを右クリックし、「名前の変更」で名前を「MainWindow.xaml」へ修正します。
MainWindows.xamlは以下のように修正します。
<Window x:Class="WpfCounterApp.Views.MainWindow"
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"
xmlns:views="clr-namespace:WpfCounterApp.Views"
mc:Ignorable="d"
Title="CounterWindow" Height="450" Width="800">
<StackPanel Margin="20" VerticalAlignment="Center">
<views:CounterView x:Name="CounterView" Margin="0,0,0,20"/>
<Separator Margin="20,0" Background="LightGray" Height="1"/>
<views:EvenOddView x:Name="EvenOddView" Margin="0,20,0,0"/>
</StackPanel>
</Window>
2つのユーザコントロール(CounterView
とEvenOddView
)を同一ウィンドウ内に配置し、1つのModelを共有して動作させる構成にしています。
MainWindow.xaml.csは以下のように修正します。(DataContextの設定はここでは行わず、コンポジションルートでまとめて行います。)
using System.Windows;
namespace WpfCounterApp.Views
{
/// <summary>
/// CounterWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
手順5:App.xaml.csでコンポジションルートを実装
App.xaml.cs
を以下のように修正します。
using System.Windows;
using WpfCounterApp.Models;
using WpfCounterApp.Services;
using WpfCounterApp.ViewModels;
using WpfCounterApp.Views;
namespace WpfCounterApp
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{//ここが、コンポジションルート
base.OnStartup(e);
// サービスを作成
var storage = new JsonCounterStorage();
// Modelを作成
var model = new CounterModel();
// ViewModelを作成(依存性注入)
var counterViewModel = new CounterViewModel(model, storage);
var evenOddViewModel = new EvenOddViewModel(model);
// MainWindowを作成し、ViewModelを設定(依存性注入)
var mainWindow = new MainWindow();
mainWindow.CounterView.SetViewModel(counterViewModel);
mainWindow.EvenOddView.SetViewModel(evenOddViewModel);
// ウィンドウを表示
mainWindow.Show();
}
}
}
アプリ起動時に依存関係(Model・ViewModel・View)をまとめて生成・接続し、全体構成を一元管理する「コンポジションルート」を実装しています。
App.xaml
からStartupUriを削除します。(mainWindow.Show()をApp.xaml.csで明示的に行っているため不要となります)
<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">
<Application.Resources>
</Application.Resources>
</Application>
アプリを実行
アプリを実行すると、以下のように動作します。

きちんとカウンターの値に応じて偶数/奇数を判定してくれているね!コードも全体的に整理された気がする!
今回、ちょっと長かったですが、無事にできましたね!
複数のViewやViewModelを活用しModelを共有するような、実践的なMVVMのコードができあがりました。
まとめ
本記事では、MVVMパターンにおける複数ViewModelでの単一Model共有について学びました。
MVVMパターンでこれを適切に実現する方法として、以下の重要な要素を習得しました。
- Modelの変更通知:Modelにイベントを持たせ、値の変更を各ViewModelへ通知する仕組み
- ユーザコントロール:機能ごとにViewを部品化する仕組み
- コンポジションルート:App.xaml.csで部品の組み立てを行い、依存関係を一元管理することで、コードの見通しを良くする
- 依存性注入:必要な部品を外側で作り、それをコンストラクタ等で渡すことで組み立てる
カウンターアプリの演習を通じて、2つのViewModelが1つのModelを共有し、それぞれが同期して動作する実装を体験しました。
次回は「データテンプレート」と「コンテンツコントロール」を学び、ViewとViewModelの紐付けを宣言的に、より簡潔に行う方法を習得します。
引き続き、一緒にC# WPFアプリ開発を学んでいきましょう!