1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CommunityToolkit.Mvvm入門|ObservableObjectとRelayCommandでMVVMの定型コードを減らす【外伝G31】

1
Posted at

この記事は CommunityToolkit.Mvvm入門 です。

MVVMの考え方を確認するなら G27: MVVM共通編
XAMLの読み方を確認するなら G28: XAML読み方辞典
WPFのMVVM実装を見るなら G29: WPF実装編
.NET MAUIのMVVM実装を見るなら G30: MAUI実装編 から読むとつながりやすいです。

WPFや.NET MAUIでMVVMを書くと、ViewModelには同じようなコードが増えやすくなります。

たとえば、画面に表示するメッセージを1つ持つだけでも、手書きでは次のようなコードになります。

private string _message = "";

public string Message
{
    get => _message;
    set
    {
        if (_message == value)
        {
            return;
        }

        _message = value;
        OnPropertyChanged();
    }
}

さらに、画面更新のために INotifyPropertyChanged も必要になります。

public event PropertyChangedEventHandler? PropertyChanged;

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

ボタン操作をViewModelへ渡す場合は、ICommandRelayCommand も出てきます。

これらは、MVVMの仕組みを理解するには大事です。
ただ、画面やViewModelが増えてくると、毎回同じようなコードを書くことになります。

そこで使えるのが CommunityToolkit.Mvvm です。

CommunityToolkit.Mvvm を使うと、ViewModelでよく出る定型コードをかなり減らせます。

CommunityToolkit.Mvvm は、C#やWPF、.NET MAUIの標準機能そのものではありません。
NuGetで追加して使うライブラリです。

ただし、WPFや.NET MAUIでMVVMを書くときの有力な選択肢です。


CommunityToolkit.Mvvmとは

CommunityToolkit.Mvvm は、MVVMでよく出るコードを短く書けるようにするライブラリです。

主に次のようなものを使います。

名前 役割
ObservableObject 変更通知を持つViewModelの土台
[ObservableProperty] 変更通知つきプロパティを生成する
[RelayCommand] メソッドからCommandを生成する
RelayCommand 同期処理用のCommand
AsyncRelayCommand 非同期処理用のCommand

まずは、次の3つを押さえると使い始めやすいです。

  • ObservableObject
  • [ObservableProperty]
  • [RelayCommand]

最初から全部を覚える必要はありません。
まずは「プロパティ通知」と「Commandの定型コードを減らすもの」と見ると分かりやすいです。


どこが短くなるのか

手書きのViewModelでは、次のようなコードを書いていました。

CustomerViewModel
  ├ INotifyPropertyChanged
  ├ CustomerName プロパティ
  ├ Message プロパティ
  ├ SaveCommand
  ├ Save()
  └ OnPropertyChanged()

CommunityToolkit.Mvvm を使うと、同じようなViewModelを次のように書けます。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SampleApp.Services;

namespace SampleApp.ViewModels;

public partial class CustomerViewModel : ObservableObject
{
    private readonly CustomerService _customerService = new();

    [ObservableProperty]
    private string customerName = "";

    [ObservableProperty]
    private string message = "";

    [RelayCommand]
    private void Save()
    {
        var result = _customerService.Save(CustomerName);

        Message = result.Success
            ? "保存しました。"
            : result.Message;
    }
}

このコードから、次のようなメンバーが使えるようになります。

書いたもの 使えるようになるもの
[ObservableProperty] private string customerName CustomerName プロパティ
[ObservableProperty] private string message Message プロパティ
[RelayCommand] private void Save() SaveCommand
ObservableObject 変更通知の仕組み

XAML側は、基本的に変わりません。

<Entry Text="{Binding CustomerName}" />
<Button Command="{Binding SaveCommand}" />
<Label Text="{Binding Message}" />

WPFなら、次のようにBindingできます。

<TextBox Text="{Binding CustomerName}" />
<Button Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding Message}" />

CommunityToolkit.Mvvm を使っても、MVVMの考え方は変わりません。
ViewModelに画面状態を置き、XAMLからBindingし、操作はCommandでViewModelへ渡します。


Visual Studioで追加する

CommunityToolkit.Mvvm は、NuGetパッケージとして追加します。

Visual Studioでは、次の流れです。

プロジェクトを右クリック
  ↓
NuGet パッケージの管理
  ↓
参照
  ↓
CommunityToolkit.Mvvm を検索
  ↓
インストール

CLIで追加する場合は、次のコマンドです。

dotnet add package CommunityToolkit.Mvvm

追加できたら、ViewModel側で次の using を使います。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

CommunityToolkit.MvvmCommunityToolkit.Maui は別物です。
この記事で使うのは、ViewModelやCommandを助ける CommunityToolkit.Mvvm です。


手書き版を確認する

先に、置き換え前のViewModelを確認します。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using SampleApp.Commands;
using SampleApp.Services;

namespace SampleApp.ViewModels;

public class CustomerViewModel : INotifyPropertyChanged
{
    private readonly CustomerService _customerService = new();

    private string _customerName = "";
    private string _message = "";

    public CustomerViewModel()
    {
        SaveCommand = new RelayCommand(Save);
    }

    public string CustomerName
    {
        get => _customerName;
        set
        {
            if (_customerName == value)
            {
                return;
            }

            _customerName = value;
            OnPropertyChanged();
        }
    }

    public string Message
    {
        get => _message;
        set
        {
            if (_message == value)
            {
                return;
            }

            _message = value;
            OnPropertyChanged();
        }
    }

    public ICommand SaveCommand { get; }

    private void Save()
    {
        var result = _customerService.Save(CustomerName);

        Message = result.Success
            ? "保存しました。"
            : result.Message;
    }

    public event PropertyChangedEventHandler? PropertyChanged;

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

この書き方は、MVVMの仕組みを理解するには分かりやすいです。

ただ、ViewModelが増えると、次のようなコードも増えていきます。

手書きで増えやすいもの 内容
backing field _customerName など
getter / setter 値の取得と設定
OnPropertyChanged() 画面更新の通知
ICommand プロパティ SaveCommand など
RelayCommand クラス Command用の共通実装

CommunityToolkit.Mvvm は、この定型部分を減らすために使います。


ObservableObjectを使う

まず、ObservableObject を使います。

ObservableObject は、ViewModelに変更通知の仕組みを持たせるための基底クラスです。

手書き版では、次のように INotifyPropertyChanged を実装していました。

public class CustomerViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

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

ObservableObject を使うと、この部分を自分で書かずに済みます。

using CommunityToolkit.Mvvm.ComponentModel;

namespace SampleApp.ViewModels;

public partial class CustomerViewModel : ObservableObject
{
}

ObservableObject は、ViewModelで変更通知を扱うための土台です。
手書きしていた INotifyPropertyChanged 周りを任せられます。


[ObservableProperty] を使う

次に、[ObservableProperty] を使います。

手書き版では、CustomerName を次のように書いていました。

private string _customerName = "";

public string CustomerName
{
    get => _customerName;
    set
    {
        if (_customerName == value)
        {
            return;
        }

        _customerName = value;
        OnPropertyChanged();
    }
}

[ObservableProperty] を使うと、次のように書けます。

[ObservableProperty]
private string customerName = "";

これで、CustomerName プロパティが生成されます。

XAML側では、生成された CustomerName にBindingします。

<Entry Text="{Binding CustomerName}" />

WPFなら、次のようにBindingできます。

<TextBox Text="{Binding CustomerName}" />

[ObservableProperty] を付けるのは、基本的にフィールド側です。
XAMLからBindingする名前は、生成されたプロパティ名の CustomerName です。


[RelayCommand] を使う

次に、[RelayCommand] を使います。

手書き版では、次のように SaveCommand を作っていました。

public ICommand SaveCommand { get; }

public CustomerViewModel()
{
    SaveCommand = new RelayCommand(Save);
}

private void Save()
{
    // 保存処理
}

[RelayCommand] を使うと、次のように書けます。

[RelayCommand]
private void Save()
{
    // 保存処理
}

これで、SaveCommand が生成されます。

XAML側では、生成された SaveCommand にBindingします。

<Button Command="{Binding SaveCommand}" />

流れはこうです。

Button.Command
  ↓ Binding
SaveCommand
  ↓
Save()

[RelayCommand] を付けたメソッド名が Save の場合、XAMLからは SaveCommand としてBindingできます。


Toolkit版のViewModel

ここまでをまとめると、ViewModelは次のようになります。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SampleApp.Services;

namespace SampleApp.ViewModels;

public partial class CustomerViewModel : ObservableObject
{
    private readonly CustomerService _customerService = new();

    [ObservableProperty]
    private string customerName = "";

    [ObservableProperty]
    private string message = "";

    [RelayCommand]
    private void Save()
    {
        var result = _customerService.Save(CustomerName);

        Message = result.Success
            ? "保存しました。"
            : result.Message;
    }
}

手書き版と比べると、かなり短くなります。

手書き版 Toolkit版
INotifyPropertyChanged を自分で実装 ObservableObject に任せる
OnPropertyChanged() を自分で呼ぶ [ObservableProperty] で生成される
RelayCommand クラスを書く [RelayCommand] で生成される
SaveCommand を自分で作る Save() から生成される

短くなっても、XAMLから見える名前は同じです。
CustomerNameMessageSaveCommand をBindingできます。


XAML側はほとんど変わらない

CommunityToolkit.Mvvm を使っても、XAML側はほとんど変わりません。

.NET MAUIなら、次のように書けます。

<VerticalStackLayout
    Padding="20"
    Spacing="12">

    <Label Text="顧客名" />

    <Entry
        Text="{Binding CustomerName}"
        Placeholder="顧客名を入力" />

    <Button
        Text="保存"
        Command="{Binding SaveCommand}" />

    <Label Text="{Binding Message}" />

</VerticalStackLayout>

WPFなら、次のように書けます。

<StackPanel Margin="20">
    <TextBlock Text="顧客名" />

    <TextBox
        Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}"
        Width="240" />

    <Button
        Content="保存"
        Command="{Binding SaveCommand}"
        Width="120"
        Margin="0,12,0,0" />

    <TextBlock
        Text="{Binding Message}"
        Margin="0,12,0,0" />
</StackPanel>

対応関係は同じです。

XAML ViewModel
Text="{Binding CustomerName}" CustomerName
Command="{Binding SaveCommand}" SaveCommand
Text="{Binding Message}" Message

Toolkitを入れても、XAMLのBinding名を変える必要はありません。
主に変わるのは、ViewModel側の書き方です。


partial を忘れない

CommunityToolkit.Mvvm の属性を使うViewModelでは、クラスに partial を付けます。

public partial class CustomerViewModel : ObservableObject
{
}

[ObservableProperty][RelayCommand] は、ソースジェネレーターによってコードを生成します。
生成されたコードは、同じクラスの一部として扱われます。

そのため、ViewModelクラスには partial が必要です。

partial を付け忘れると、CustomerNameSaveCommand が生成されません。
Toolkitを使うViewModelでは、まず partial class になっているか確認します。


生成される名前を意識する

[ObservableProperty] を使うと、フィールド名からプロパティ名が生成されます。

[ObservableProperty]
private string customerName = "";

この場合、生成されるプロパティ名は CustomerName です。

XAML側では、生成後のプロパティ名にBindingします。

<Entry Text="{Binding CustomerName}" />

[RelayCommand] も同じです。

[RelayCommand]
private void Save()
{
}

この場合、生成されるCommand名は SaveCommand です。

XAML側では、次のようにBindingします。

<Button Command="{Binding SaveCommand}" />

対応関係はこうです。

書いたもの XAMLから見る名前
customerName CustomerName
message Message
Save() SaveCommand

Bindingするのは、生成後の名前です。
フィールド名ではなく、XAMLから見えるプロパティ名・Command名を意識します。


ボタンの有効/無効も扱える

保存ボタンを、入力状態に応じて有効/無効にしたい場合もあります。

たとえば、顧客名が空なら保存できないようにする場合です。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SampleApp.Services;

namespace SampleApp.ViewModels;

public partial class CustomerViewModel : ObservableObject
{
    private readonly CustomerService _customerService = new();

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private string customerName = "";

    [ObservableProperty]
    private string message = "";

    [RelayCommand(CanExecute = nameof(CanSave))]
    private void Save()
    {
        var result = _customerService.Save(CustomerName);

        Message = result.Success
            ? "保存しました。"
            : result.Message;
    }

    private bool CanSave()
    {
        return !string.IsNullOrWhiteSpace(CustomerName);
    }
}

ここでは、次の2つを使っています。

書き方 役割
CanExecute = nameof(CanSave) CanSave()true のときだけ実行できる
NotifyCanExecuteChangedFor(nameof(SaveCommand)) CustomerName が変わったら SaveCommand の有効状態も更新する

この形にすると、CustomerName が空の間は保存ボタンを押せない状態にできます。

最初は CanExecute なしでも問題ありません。
ボタンの有効/無効まで制御したくなったら、CanExecuteNotifyCanExecuteChangedFor を使います。


非同期処理の場合

保存処理がAPI通信やDBアクセスになると、非同期処理が必要になることがあります。

CommunityToolkit.Mvvm では、非同期Commandも扱えます。

[RelayCommand]
private async Task SaveAsync()
{
    await Task.Delay(500);

    Message = "保存しました。";
}

この場合も、XAML側ではCommandとしてBindingできます。

<Button Command="{Binding SaveCommand}" />

メソッド名が SaveAsync の場合でも、Command名は SaveCommand になります。

非同期Commandは便利です。
ただし、最初は同期処理の RelayCommand を理解してから進むと追いやすいです。


使うかどうかの判断

CommunityToolkit.Mvvm は便利ですが、すべてのMVVMコードで必ず使う必要はありません。

判断基準は次のようになります。

状況 判断
MVVMの仕組みを初めて学ぶ まず手書きで流れを見る
ViewModelが1つだけの小さいサンプル 手書きでもよい
ViewModelが増えてきた Toolkitを使う価値が大きい
OnPropertyChanged() が大量に出てきた Toolkitを検討する
Commandを毎回手書きしている Toolkitを検討する
チームでMVVMを書く 書き方を決めてから使う

Toolkitを使うとコードは短くなります。
ただし、生成される名前や仕組みを知らないまま使うと、逆に追いにくくなることがあります。


よくある詰まりどころ

CustomerName が見つからない

[ObservableProperty] を付けたフィールド名を確認します。

[ObservableProperty]
private string customerName = "";

この場合、XAML側は CustomerName です。

<Entry Text="{Binding CustomerName}" />

Bindingするのはフィールド名の customerName ではなく、生成されたプロパティ名の CustomerName です。

SaveCommand が見つからない

[RelayCommand] を付けたメソッド名を確認します。

[RelayCommand]
private void Save()
{
}

この場合、XAML側は SaveCommand です。

<Button Command="{Binding SaveCommand}" />

生成されない

まず見るのは次です。

  • CommunityToolkit.Mvvm がNuGetで追加されているか
  • using CommunityToolkit.Mvvm.ComponentModel; があるか
  • using CommunityToolkit.Mvvm.Input; があるか
  • ViewModelクラスが partial になっているか
  • 一度ビルドしているか
public partial class CustomerViewModel : ObservableObject

ソースジェネレーターで生成されるコードは、元の .cs ファイルに直接書き足されるわけではありません。
IntelliSenseが追いつかない場合は、一度ビルドすると見えることがあります。


手書き版とToolkit版の比較

最後に、手書き版とToolkit版を比べます。

手書き版です。

public class CustomerViewModel : INotifyPropertyChanged
{
    private string _message = "";

    public string Message
    {
        get => _message;
        set
        {
            if (_message == value)
            {
                return;
            }

            _message = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

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

Toolkit版です。

public partial class CustomerViewModel : ObservableObject
{
    [ObservableProperty]
    private string message = "";
}

どちらも目的は同じです。

Messageが変わったら、Bindingしている画面に通知する。

違うのは、定型コードを自分で書くか、Toolkitに任せるかです。

手書き版は仕組みの理解に向いています。
Toolkit版は実装量を減らすのに向いています。
目的に合わせて使い分けます。


まとめ

CommunityToolkit.Mvvm は、MVVMでよく出てくる定型コードを減らすためのライブラリです。

特に次の3つを押さえると使い始めやすいです。

機能 役割
ObservableObject 変更通知の土台
[ObservableProperty] 変更通知つきプロパティを生成する
[RelayCommand] メソッドからCommandを生成する

手書きしていたプロパティは、

public string Message
{
    get => _message;
    set
    {
        _message = value;
        OnPropertyChanged();
    }
}

Toolkitを使うと、次のように短くできます。

[ObservableProperty]
private string message = "";

Commandも同じです。

[RelayCommand]
private void Save()
{
}

これで、XAML側からは次のようにBindingできます。

<Button Command="{Binding SaveCommand}" />

CommunityToolkit.Mvvm は必須ではありません。

ただ、ViewModelが増えて OnPropertyChanged()RelayCommand の定型コードが増えてきたら、導入する価値が大きいです。

MVVMの理解には、まず手書き版が役立ちます。
実装量を減らしたい段階では、CommunityToolkit.Mvvm が役立ちます。


関連記事


次に読む

次は、ViewModelから画面依存を分ける方向へ進めます。

  • G32: MVVMのService分割入門|ViewModelから保存処理・ダイアログ・画面遷移を分ける

連載Index(読む順・公開済リンクが最新)
S00: 総合Index

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?