WPF

【C#/WPF実践アプリ開発ラボ(】MVVMパターン/アーキテクチャの基礎⑤ ~CommunityToolkit.MvvmでINotifyPropertyChangedを自動化~【ObservableProperty、RelayCommandで簡潔に!】

MVVM は「動く」までの定型コード(ボイラープレートコード)が多く、更新通知・コマンド周りの重複が増えがちです。

CommunityToolkit.Mvvm はこの“繰り返し”をソースジェネレータで自動化します。

以下のような方に役立つ内容となっています。

  • MVVMパターンのボイラープレートコードを減らしたい
  • INotifyPropertyChangedの実装を簡略化したい
  • ICommandの実装を簡潔に書きたい
  • より保守性の高いViewModelコードを書きたい

この記事では、シンプルなカウンターアプリをベースに、CommunityToolkit.Mvvmを導入することで、どれだけコードが簡潔になるかを実践します。

MVVM解説の記事①~④から続けて見ていただくと、理解が深まるかと思います。

【C#/WPF実践入門編(9)】MVVMパターン/アーキテクチャの基礎①~Model・View・ViewModelの3層で役割分担~ WPFアプリ開発において重要なアーキテクチャパターンであるMVVM(Model-View-ViewModel)について学びます。 ...
【C#/WPF実践入門編(10)】MVVMパターン/アーキテクチャの基礎② ~VisualStudioでフォルダ構成とサービス層で実務向け設計~ 前回はMVVMパターンの基本概念を学びました。今回は、より実践的なMVVMアプリの設計について学習します。 適切なフォルダ構成で...
【C#/WPF実践入門編(11)】MVVMパターン/アーキテクチャの基礎③ ~複数ViewModelでModelを共有する設計と依存性注入(DI)の実践~ 前回はMVVMパターンの実践的なフォルダ構成とサービス層について学びました。今回は、複数のViewModelで1つのModelを共有す...
【C#/WPF実践入門編(12)】MVVMパターン/アーキテクチャの基礎④ ~データテンプレートとコンテンツコントロールで実現する宣言的なView-ViewModel紐付け~ 前回は複数のViewModelで1つのModelを共有する方法を学びました。 今回、データテンプレート(DataTemplate...
プロ太

CommunityToolkit.Mvvmは実務でもよく使われるライブラリで、定型的なコードを大幅に削減できます。

WPF だけでなく .NET MAUI、WinUI 3、AvaloniaUI などの XAML 系 UIフレームワークでも広く活用できます。

演習のコード一式はGitHubに置いてあります。

動画も作成しています。

講義:CommunityToolkit.Mvvmによるボイラープレート削減

CommunityToolkit.Mvvmにより、どのようにボイラープレートコードが削減できるかを、Before/Afterで確認してみましょう。

Beforeコード(MVVMの基本実装)

まず、CommunityToolkit.Mvvmを使う前のコード(Beforeコード)を確認しましょう。

これまで学んできたMVVMパターンの基本を実装したシンプルなカウンターアプリを例にします。以下の構成になっています。(全コードはGitHubにあります)

プロ太

前回演習のものよりシンプル(カウントするだけ)にしています。Model、ViewModel、Viewが1つずつです。

BeforeコードにおけるViewModelのコード(CounterViewModel.cs)をみてみましょう。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using WpfCounterApp.Commands;
using WpfCounterApp.Models;

namespace WpfCounterApp.ViewModels
{
    public class CounterViewModel : INotifyPropertyChanged
    {
        private readonly CounterModel _model;
        private readonly SimpleCommand _incrementCommand;
        private readonly SimpleCommand _decrementCommand;

        public CounterViewModel()
        {
            _model = new CounterModel();
            _model.ValueChanged += OnCountChanged;

            _incrementCommand = new SimpleCommand(_ => _model.Increment());
            _decrementCommand = new SimpleCommand(
                _ => _model.Decrement(),
                _ => _model.CanDecrement());
        }

        public int Count => _model.Value;

        public ICommand IncrementCommand => _incrementCommand;
        public ICommand DecrementCommand => _decrementCommand;

        private void OnCountChanged()
        {
            OnPropertyChanged(nameof(Count));
            _decrementCommand.RaiseCanExecuteChanged();
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

このコードは正しく動作しますが、MVVMパターンの実装には定型的なコード(ボイラープレート)が多く含まれています。

プロ太

次に、このViewModelコードの何が課題なのかを見ていきましょう。

Beforeコードの課題

Beforeコードには、以下のような課題があります。

1.INotifyPropertyChangedの実装が冗長

ViewModelでプロパティの変更を通知するために、毎回以下のようなコードを書く必要があります。

public event PropertyChangedEventHandler? PropertyChanged;

protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

すべてのViewModelで同じコードを繰り返し書くことになります。

2.コマンドの実装が煩雑

ICommandを実装したクラス(SimpleCommand)を自分で用意し、インスタンス化する必要があります。

private readonly SimpleCommand _incrementCommand;
private readonly SimpleCommand _decrementCommand;

_incrementCommand = new SimpleCommand(_ => _model.Increment());
_decrementCommand = new SimpleCommand(
    _ => _model.Decrement(),
    _ => _model.CanDecrement());

コマンドが増えるたびに、フィールド宣言とインスタンス化のコードが増えていきます。

3.CanExecuteの変更通知が手動

コマンドの実行可否が変わったときに、手動でRaiseCanExecuteChanged()を呼ぶ必要があります。

private void OnCountChanged()
{
    OnPropertyChanged(nameof(Count));
    _decrementCommand.RaiseCanExecuteChanged();  // ★手動で呼び出し
}

プロパティとコマンドの依存関係の管理が煩雑になりがちで、ミスが起きやすくなります。

プロ美

確かに、同じようなコードを何度も書くのは面倒だし、間違えやすそうだね。

プロ太

そうですね。これらの課題を解決するために、CommunityToolkit.Mvvmが用意されています。

CommunityToolkit.Mvvmとは?(Afterコード)

CommunityToolkit.Mvvm(旧名:Microsoft.Toolkit.Mvvm)は、Microsoftが提供するMVVMパターン実装を支援するライブラリです。

このライブラリには以下のような機能があります。

  • ObservableObject:INotifyPropertyChangedの実装を提供する基底クラス
  • ObservableProperty属性:プロパティの変更通知を自動生成
  • RelayCommand属性:コマンドの実装を自動生成
  • NotifyCanExecuteChangedFor属性:コマンドの実行可否の変更通知を自動化

CommunityToolkit.Mvvmは、C#のソースジェネレータ技術を使っていて、コンパイル時に定型コードを自動生成します。

プロ美

ソースジェネレータ、属性って何?なんか難しそう…。

プロ太

C#コードにちょっとした付加情報(属性)を記載すると、それをもとに定型コードを自動生成(ソースジェネレータが実行)する仕組みです。

ライブラリ自体は高度な仕組みで実装されていますが、使う分には属性の記述方法を把握していればOKです!一例をみてみましょう。

例えば、「ObservableObjectObservableProperty属性」の導入によって、INotifyPropertyChanged実装が不要になります。以下のようなイメージです。
(INotifyPropertyChanged関連に絞ったBefore/Afterです)

●Before(従来)のコード

public class CounterViewModel : INotifyPropertyChanged
{
    private readonly CounterModel _model;

    public int Count => _model.Value;

    public CounterViewModel()
    {
        _model = new CounterModel();
        _model.ValueChanged += OnCountChanged;
    }

    private void OnCountChanged()
    {
        OnPropertyChanged(nameof(Count));
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

●After(CommunityToolkit.Mvvm利用):

public partial class CounterViewModel : ObservableObject
{
    private readonly CounterModel _model;

    [ObservableProperty]
    private int count;

    public CounterViewModel()
    {
        _model = new CounterModel();
        _model.ValueChanged += OnCountChanged;
    }

    private void OnCountChanged()
    {
        Count = _model.Value;
    }
}
プロ美

Afterのコード、簡潔になってる!

プロ太

CommunityToolkit.Mvvmが裏側で、Afterの記述からBefore相当のコードを自動生成してくれているんですね。

それでは、演習でAfterのコード全体をつくっていきましょう!

CommunityToolkit.Mvvmについて詳しくはこちらも参考にしてください。

演習:CommunityToolkit.Mvvmの導入とコード修正

それでは、実際にCommunityToolkit.Mvvmを導入して、Beforeコードを修正してAfterコードを作成していきましょう。以下の手順で進めます。

  • 手順1:NuGetパッケージのインストール
  • 手順2:CounterViewModelの修正
  • 手順3:SimpleCommand.csの削除

手順1:NuGetパッケージのインストール

CommunityToolkit.Mvvmをインストールします。Visual Studio上でインストールする場合は、手順については以下を参考にしてください。

C#入門編(18)NuGetパッケージの使い方 ~CSVファイルを読み込む~【Visual Studio+nuget】 実践的なアプリ開発では、他人が作った既存部品をいかにうまく使うかがキモになります。 だいたいのアプリ開発は以下のイメージです。 ...

プロジェクトのあるフォルダで以下のコマンドを実行する方法もあります。

dotnet add package CommunityToolkit.Mvvm

手順2:CounterViewModelの修正

CounterViewModel.csを以下のように修正します。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using WpfCounterApp.Models;

namespace WpfCounterApp.ViewModels
{
    public partial class CounterViewModel : ObservableObject //★(1)
    {
        private readonly CounterModel _model;

        public CounterViewModel()
        {
            _model = new CounterModel();
            _model.ValueChanged += OnCountChanged;
        }

        [ObservableProperty] //★(2)
        [NotifyCanExecuteChangedFor(nameof(DecrementCommand))] //★(3)
        private int count;

        [RelayCommand] //★(4)
        private void Increment() => _model.Increment();

        [RelayCommand(CanExecute = nameof(CanDecrement))] //★(5)
        private void Decrement() => _model.Decrement();

        private bool CanDecrement() => Count > 0;

        private void OnCountChanged()
        {
            Count = _model.Value;
        }
    }
}

以下がポイントです。

  • ★(1)で、ObservableObjectを継承します。partialキーワードは、ソースジェネレータがクラス定義の残りを別ファイルで自動生成するため必須です。
  • ★(2)で、小文字のフィールド(count)を宣言するだけで、大文字のプロパティ(Count)とPropertyChanged通知が自動生成されます。
  • ★(3)で、Countプロパティが変更されると、指定したコマンド(DecrementCommand)のCanExecuteが自動的に再評価されます。
  • ★(4) で、メソッドに属性をつけるだけで、IncrementCommandプロパティが自動生成されます。SimpleCommandのインスタンス化は不要になります。
  • ★(5)で、CanExecuteパラメータで実行可否を判定するメソッドを指定できます。CanDecrementがfalseを返すと、ボタンが自動的に無効化されます。

ちなみに、自動生成されているコードを確認することもできます。例えば、Visual Studioで自動生成されたプロパティ(Count)を右クリックし「定義へ移動」をしてみましょう。

すると、以下の自動生成コードを確認できます。(もう1つのCounterViewModelクラスを自動生成しているため、partialキーワードが必要になっています。)

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace WpfCounterApp.ViewModels
{
    /// <inheritdoc/>
    partial class CounterViewModel
    {
        /// <inheritdoc cref="count"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.4.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        public int Count
        {
            get => count;
            set
            {
                if (!global::System.Collections.Generic.EqualityComparer<int>.Default.Equals(count, value))
                {
                    OnCountChanging(value);
                    OnCountChanging(default, value);
                    OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Count);
                    count = value;
                    OnCountChanged(value);
                    OnCountChanged(default, value);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Count);
                    DecrementCommand.NotifyCanExecuteChanged();
                }
            }
        }
...
プロ太

普段、自動生成コードを開発者が意識する必要はありません。

ただ、一度これをみておくと、CommunityToolkit.Mvvmが裏で行ってくれていることのイメージを掴めるでしょう。

手順3:SimpleCommand.csの削除

CommunityToolkit.Mvvmが提供するRelayCommandを使うため、自作のSimpleCommand.csは不要になります。

SimpleCommand.csはCommandsフォルダごと削除します。

プロ美

自分で作ったコマンドクラスが不要になるんだね!

アプリ実行

アプリを実行すると、Beforeコードと同じように動作します。(動作は変わりません)

コードの構造は大きく変わり、ボイラープレートコードが削減され、可読性が大幅に向上しています。

プロ美

繰り返し書いていたコードがなくなって、スッキリしたね!

プロ太

ViewModelのコード量も「44行→34行」(約23%)と削減できましたね。

プロパティやコマンドが増えるほど削減効果は大きくなるので、 規模の大きなアプリでは、より効果を発揮するでしょう。

まとめ

本記事では、CommunityToolkit.Mvvmを使ったMVVMパターンのボイラープレート削減について学びました。

以下のような主な機能を学びました。

  • ObservableObject:INotifyPropertyChangedの実装を提供する基底クラス
  • [ObservableProperty]:プロパティの変更通知を自動生成する属性
  • [RelayCommand]:コマンドの実装を自動生成する属性
  • [NotifyCanExecuteChangedFor]:コマンドの実行可否の変更通知を自動化する属性

CommunityToolkit.Mvvmソースジェネレータの仕組みによって、コンパイル時に定型コードを自動生成しています。

CommunityToolkit.Mvvmは実務でも(特に新規プロジェクトで)使われていて、 WPF/MVVMアプリ開発において学ぶ価値が高いものです。

MVVM向けライブラリは、目的に応じて以下の選択肢もあります。

  • Prism – 大規模アプリ開発向けの高機能MVVMライブラリ
  • ReactiveUI – データの流れをストリームとして扱い、複雑な連携処理を簡単に書けるライブラリ

入門としてはCommunityToolkit.MvvmでMVVMの基礎を学ぶのがおすすめです。

プロ太

引き続き、一緒にC# WPFアプリ開発を学んでいきましょう!

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。