Microsoft 公式の .NET 財団 によって提供されている .NET Community Toolkit 内の、MVVM (Model-View-ViewModel) ライブラリです。
WPF や MAUI、WinUI 3 などで、データバインディングで View と ViewModel 間で値の反映や処理の実行をやり取りするときに INotifyPropertyChanged インターフェース や ICommand インターフェース を実装してプロパティやメソッドを用意しますが、これは非常に長いボイラープレート(定型文)となりがちです。
ここでは WPF で、自力で実装した場合の、実際の例を見ていきます。

/// <summary>
/// ビューモデルの基底クラス
/// </summary>
public class ViewModelBase : INotifyPropertyChanged
{
/// <summary>
/// プロパティが変更されたときに発生するイベント
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// プロパティが変更されたことを通知する
/// </summary>
/// <param name="propertyName">変更されたプロパティの名前</param>
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// メイン画面のビューモデル
/// </summary>
public class MainWindowViewModel : ViewModelBase
{
/// <summary>
/// テキスト (バッキングフィールド)
/// </summary>
private string _text = string.Empty;
/// <summary>
/// テキスト
/// </summary>
public string Text
{
get => this._text;
set
{
if (this._text == value) return;
this._text = value;
OnPropertyChanged(nameof(this.Text));
}
}
}

/// <summary>
/// メインコマンド
/// </summary>
public sealed class MainCommand(
Action action
) : ICommand
{
/// <summary>
/// コマンドの実行可否が変更されたときに発生するイベント
/// </summary>
public event EventHandler? CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
/// <summary>
/// コマンドが実行可能かどうかを判断する
/// </summary>
/// <param name="parameter">コマンドに渡されるパラメータ</param>
/// <returns>コマンドが実行可能かどうか</returns>
public bool CanExecute(object? parameter)
=> true;
/// <summary>
/// コマンドを実行する
/// </summary>
/// <param name="parameter">コマンドに渡されるパラメータ</param>
public void Execute(object? parameter)
=> action();
}
/// <summary>
/// メイン画面のビューモデル
/// </summary>
public class MainWindowViewModel : ViewModelBase
{
/// <summary>
/// メインコマンド
/// </summary>
public ICommand MainCommand { get; private set; }
/// <summary>
/// コンストラクタ
/// </summary>
public MainWindowViewModel()
=> this.MainCommand = new MainCommand(action: () => this.Text += "!");
}

<Window x:Class="MvvmSelfWpfAppSample.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:MvvmSelfWpfAppSample"
mc:Ignorable="d"
Title="MainWindow" Height="160" Width="320">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Text, Mode=TwoWay}"
Margin="10"
Grid.Row="0" />
<Button Content="実行"
Command="{Binding MainCommand}"
Width="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="1" />
</Grid>
</Window>
/// <summary>
/// メイン画面
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
=> InitializeComponent();
}

1個のプロパティと、1個のコマンドだけで、これだけのコード量となり、手間がかかって大変です。
ここで、MVVM Toolkit を使って、以下のように非常に簡潔に記述するだけで、まったく同様のことが実現できます。

/// <summary>
/// メイン画面ビューモデル
/// </summary>
public sealed partial class MainWindowViewModel : ObservableObject
{
/// <summary>
/// テキスト
/// </summary>
[ObservableProperty]
public partial string Text { get; set; } = string.Empty;
/// >summary>
/// メインコマンド
/// </summary>
[RelayCommand]
private void Main()
=> Text += '!';
}

<Window x:Class="MvvmSelfWpfAppSample.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:MvvmToolkitWpfAppSample"
mc:Ignorable="d"
Title="MainWindow" Height="160" Width="320">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Text, Mode=TwoWay}"
Margin="10"
Grid.Row="0" />
<Button Content="実行"
Command="{Binding MainCommand}"
Width="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="1" />
</Grid>
</Window>
/// <summary>
/// メイン画面
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
=> InitializeComponent();
}

MVVM Toolkit では、ソースジェネレータによって、必要なコードが自動生成される仕組みとなっています。
今度は、WinUI 3 で、MVVM Toolkit を使った簡単なデスクトップアプリを作ってみながら、ソースジェネレータによって、どのようなコードが自動生成されているのかを見てみます。

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.26100.0</TargetPlatformMinVersion>
<RootNamespace>MvvmToolkitWinUI3AppSample</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
<EnableMsixTooling>true</EnableMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2270" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
</ItemGroup>

<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="MvvmToolkitWinUI3AppSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MvvmToolkitWinUI3AppSample">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

今回 Microsoft.Extensions.Hosting (ジェネリックホスト) による、DI (依存性の注入) や ロギング を使っています。
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace MvvmToolkitWinUI3AppSample;
/// <summary>
/// アプリのメインエントリポイント
/// </summary>
public sealed partial class App : Application
{
/// <summary>
/// ロガー
/// </summary>
private ILogger<App>? logger;
/// <summary>
/// メイン画面
/// </summary>
private Window? mainWindow;
/// <summary>
/// ジェネリックホストアプリ
/// </summary>
public static IHost? HostApp { get; private set; }
/// <summary>
/// アプリのカレントインスタンス
/// </summary>
public new static App Current
=> (App)Application.Current;
/// <summary>
/// アプリのコンストラクタ
/// </summary>
public App()
{
InitializeComponent();
// 例外処理イベントリスナー登録
UnhandledException += OnUnhandledException;
}
/// <summary>
/// ジェネリックホストアプリ生成処理
/// </summary>
/// <param name="args">起動時引数</param>
private static void BuildHostApp(string[]? args)
{
// ジェネリックホストアプリビルダーの生成
HostApplicationBuilder hostAppBuilder = Host.CreateApplicationBuilder(args);
// ロギング設定
hostAppBuilder.Logging
.AddDebug()
.SetMinimumLevel(LogLevel.Debug);
// DIコンテナにビューを登録
hostAppBuilder.Services
.AddSingleton<MainWindow>();
// DIコンテナにビューモデルを登録
hostAppBuilder.Services
.AddSingleton<MainWindowViewModel>();
// ジェネリックホストアプリの生成
HostApp = hostAppBuilder.Build();
}
/// <summary>
/// アプリ起動時の処理
/// </summary>
/// <param name="args">起動時引数</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// ジェネリックホストアプリを生成
BuildHostApp([.. args.Arguments.Split(' ')]);
// DIコンテナからロガーを取得
this.logger = HostApp?.Services.GetRequiredService<ILogger<App>>();
// DIコンテナからメイン画面を取得して表示
this.mainWindow = HostApp?.Services.GetRequiredService<MainWindow>();
this.mainWindow?.Activate();
}
/// <summary>
/// アプリ全体の例外処理
/// </summary>
/// <param name="sender">イベント送信元</param>
/// <param name="e">イベント引数</param>
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// 例外をハンドル済みにする (アプリをクラッシュさせないようにする)
e.Handled = true;
// ログ出力
this.logger?.LogError("{Message}", e.Exception.Message);
// エラーダイアログを非同期で表示
this.mainWindow?.DispatcherQueue?.TryEnqueue(async () =>
{
var dialog = new ContentDialog
{
Title = "エラー",
Content = $"""
エラーが発生しました。
詳細:
{e.Exception.Message}
""",
CloseButtonText = "閉じる",
XamlRoot = this.mainWindow?.Content.XamlRoot
};
await dialog.ShowAsync();
});
}
}

MVVM Toolkit を使うことで、通知プロパティやコマンドを、非常に簡潔に記述できています。
using System;
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
namespace MvvmToolkitWinUI3AppSample;
/// <summary>
/// メイン画面ビューモデル
/// </summary>
/// <param name="logger">ロガー</param>
public sealed partial class MainWindowViewModel(
ILogger<MainWindowViewModel> logger
) : ObservableValidator
{
/// <summary>
/// 現在の値
/// </summary>
[ObservableProperty]
[NotifyDataErrorInfo]
[Range(0, 100, ErrorMessage = "0から100までが適正範囲です。")]
[NotifyPropertyChangedFor(nameof(ErrorMessage))]
public partial sbyte CurrentValue { get; set; } = 0x00;
/// <summary>
/// バリデーションエラーメッセージ
/// </summary>
public string ErrorMessage
=> string.Join(" ", GetErrors());
/// <summary>
/// ボタン押下時の処理
/// </summary>
/// <param name="param">操作パラメータ</param>
[RelayCommand]
private void ChangeCurrentValue(string param)
{
// 現在の値を、増加/減少/リセット
CurrentValue = param switch
{
"+" => (sbyte)Math.Min(CurrentValue + 1, sbyte.MaxValue),
"-" => (sbyte)Math.Max(CurrentValue - 1, sbyte.MinValue),
_ => 0x00
};
// バリエーション処理
ValidateAllProperties();
// ログ出力
logger.LogDebug("現在値: {CurrentValue}", CurrentValue);
if (HasErrors) logger.LogWarning("バリデーションエラー: {ErrorMessage}", ErrorMessage);
}
}

<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="MvvmToolkitWinUI3AppSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MvvmToolkitWinUI3AppSample"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="WinUI 3 アプリ w/ MVVM Toolkit">
<Window.SystemBackdrop>
<MicaBackdrop Kind="BaseAlt" />
</Window.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="46" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- タイトルバー -->
<TitleBar x:Name="TitleBar"
Title="WinUI 3 アプリ w/ MVVM Toolkit"
IsBackButtonVisible="False"
IsPaneToggleButtonVisible="False"
IsTabStop="False"
Height="46"
Grid.Row="0" />
<!-- コンテンツ -->
<StackPanel Margin="25,40,25,0"
Orientation="Vertical"
HorizontalAlignment="Center"
Spacing="25"
Grid.Row="1">
<!-- タイトル -->
<TextBlock Text="Hello, World!"
HorizontalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}" />
<!-- 現在の値 -->
<TextBlock Text="{x:Bind MainWindowViewModel.CurrentValue, Mode=OneWay}"
TextAlignment="Center"
Style="{StaticResource BodyLargeStrongTextBlockStyle}" />
<!-- 操作ボタン -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="10">
<!-- -1ボタン -->
<Button Content="-1"
Style="{StaticResource AccentButtonStyle}"
Command="{x:Bind MainWindowViewModel.ChangeCurrentValueCommand}"
CommandParameter="-" />
<!-- +1ボタン -->
<Button Content="+1"
Style="{StaticResource AccentButtonStyle}"
Command="{x:Bind MainWindowViewModel.ChangeCurrentValueCommand}"
CommandParameter="+" />
<!-- クリアボタン -->
<Button Content="クリア"
Style="{StaticResource ButtonRevealStyle}"
Command="{x:Bind MainWindowViewModel.ChangeCurrentValueCommand}"
CommandParameter="0" />
</StackPanel>
<!-- エラーメッセージ -->
<InfoBar Severity="Error"
IsClosable="False"
MaxWidth="186"
Message="{x:Bind MainWindowViewModel.ErrorMessage, Mode=OneWay}"
IsOpen="{x:Bind MainWindowViewModel.HasErrors, Mode=OneWay}" />
</StackPanel>
</Grid>
</Window>

using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Graphics;
namespace MvvmToolkitWinUI3AppSample;
/// <summary>
/// メイン画面ビュー
/// </summary>
public sealed partial class MainWindow : Window
{
/// <summary>
/// メイン画面ビューモデル
/// </summary>
public MainWindowViewModel MainWindowViewModel;
/// <summary>
/// メイン画面のコンストラクタ
/// </summary>
public MainWindow(MainWindowViewModel mainWindowViewModel)
{
InitializeComponent();
this.MainWindowViewModel = mainWindowViewModel;
// 画面サイズの設定
this.AppWindow.Resize(new SizeInt32(480, 360));
// タイトルバーの設定
this.AppWindow.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
this.ExtendsContentIntoTitleBar = true;
if (AppWindow.TitleBar.ExtendsContentIntoTitleBar)
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
this.SetTitleBar(this.TitleBar);
}
}
この WinUI 3 アプリの実行の様子は、こんな形です。

ObservableProperty 属性を付与するだけで、ソースジェネレータによって、通知プロパティに必要なコードが下図のように生成されます。

RelayCommand 属性を付与するだけで、ソースジェネレータによって、コマンドに必要なコードが下図のように生成されます。
