この記事は 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へ渡す場合は、ICommand や RelayCommand も出てきます。
これらは、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.Mvvm と CommunityToolkit.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から見える名前は同じです。
CustomerName、Message、SaveCommand を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 を付け忘れると、CustomerName や SaveCommand が生成されません。
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 なしでも問題ありません。
ボタンの有効/無効まで制御したくなったら、CanExecute と NotifyCanExecuteChangedFor を使います。
非同期処理の場合
保存処理が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