mvvm-toolkit について
mvvm-toolkit は、CommunityToolkit.Mvvm 8.x を使うアプリの ViewModel、プロパティ、コマンド、検証を作成・レビューする開発者向けのスキルです。ソースジェネレーターの利用法、基底クラスの選択、よくある問題を解説します。
主な機能
- 01属性による監視可能プロパティの生成
- 02同期・非同期コマンドの生成
- 03関連プロパティとコマンドへの通知
- 04フォーム入力の検証に対応
- 05診断情報と一般的な問題の解説
活用シーン
- 01ViewModel への監視可能プロパティの追加
- 02同期または非同期コマンドの作成
- 03入力検証付きフォームの構築
- 04CommunityToolkit.Mvvm 実装のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ github/ awesome-copilot --skill mvvm-toolkitインストール後、Claude Code で/mvvm-toolkitを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/end-to-end-walkthrough.mdrelaycommand-cookbook.mdsource-generators.mdtroubleshooting.mdvalidation.md詳細
CommunityToolkit.Mvvm(コア)
CommunityToolkit.Mvvm 8.x を使うアプリで、ViewModel、プロパティ、コマンド、または検証を作成またはレビューする際に、このスキルを使用してください。
関連スキル。
IMessengerのパブリッシュ/サブスクライブパターンにはmvvm-toolkit-messengerを読み込みます。Microsoft.Extensions.DependencyInjectionとの統合にはmvvm-toolkit-diを読み込みます。
要点のおさらい。
partialクラスのプライベートフィールドに[ObservableProperty]を適用します。インスタンスメソッドに[RelayCommand]を適用します。ObservableObjectを継承します(入力フォームではObservableValidator、IMessengerを使う場合はObservableRecipientを継承します)。
パッケージとセットアップ
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
</ItemGroup>
対象:netstandard2.0、netstandard2.1、net6.0 以降。.NET、.NET Framework、Mono で動作します。ソースジェネレーターは同じ NuGet パッケージに含まれているため、追加のアナライザー参照は不要です。
名前空間:
using CommunityToolkit.Mvvm.ComponentModel; // ObservableObject, [ObservableProperty]
using CommunityToolkit.Mvvm.Input; // [RelayCommand], RelayCommand, AsyncRelayCommand
共通ルール。
[ObservableProperty]または[RelayCommand]を使うすべての型と、ネストされている場合はそれを囲むすべての型をpartialとして宣言する必要があります。そうしないと、ジェネレーターはMVVMTK0008/MVVMTK0042を出力します。
ソースジェネレーター早見表
| 属性 | 適用対象 | 生成されるもの |
|---|---|---|
[ObservableProperty] | プライベートフィールド | パブリックな INotifyPropertyChanged プロパティと OnXxxChanging/OnXxxChanged 部分メソッドフック |
[NotifyPropertyChangedFor(nameof(Other))] | オブザーバブルフィールド | 指定されたプロパティについても PropertyChanged を発生させます |
[NotifyCanExecuteChangedFor(nameof(MyCommand))] | オブザーバブルフィールド | 変更時に MyCommand.NotifyCanExecuteChanged() を呼び出します |
[NotifyDataErrorInfo] | ObservableValidator のオブザーバブルフィールド | セッターから ValidateProperty(value) を呼び出します |
[NotifyPropertyChangedRecipients] | ObservableRecipient のオブザーバブルフィールド | 変更後に Broadcast(old, new) を呼び出します |
[RelayCommand] | インスタンスメソッド | IRelayCommand/IAsyncRelayCommand として公開される遅延初期化の RelayCommand/AsyncRelayCommand |
[RelayCommand(CanExecute = nameof(CanX))] | インスタンスメソッド | CanExecute をメソッドまたはプロパティに接続します |
[RelayCommand(IncludeCancelCommand = true)] | CancellationToken を受け取る非同期メソッド | XxxCancelCommand も生成します |
[RelayCommand(AllowConcurrentExecutions = true)] | 非同期メソッド | キュー処理/並列呼び出しを許可します(デフォルトでは実行中に無効になります) |
[RelayCommand(FlowExceptionsToTaskScheduler = true)] | 非同期メソッド | 例外を待機して再スローする代わりに、ExecutionTask を通じて表面化させます |
[property: SomeAttr] | オブザーバブルフィールドまたは [RelayCommand] メソッド | SomeAttr を生成されるプロパティに転送します(例:[JsonIgnore]) |
命名規則。 フィールド name/_name/m_name → Name。メソッド LoadAsync → LoadCommand(Async 接尾辞が削除され、先頭の On も削除されます)。
生成コードのサンプルを含む属性の詳細については、references/source-generators.md を参照してください。
ViewModel のパターン
シンプルなオブザーバブルプロパティ
public partial class ContactViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}
フック:OnXxxChanging/OnXxxChanged
[ObservableProperty]
private string? name;
partial void OnNameChanged(string? value) =>
Logger.LogInformation("Name changed to {Name}", value);
単一引数 (value) と 2 引数 (oldValue, newValue) の両方のオーバーロードを使用できます。必要なものだけを実装してください。未実装のフックはコンパイラーによって省略されるため、実行時コストはゼロです。
依存プロパティと依存コマンド
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? lastName;
public string FullName => $"{FirstName} {LastName}".Trim();
オブザーバブルではないモデルのラップ
public sealed class ObservableUser(User user) : ObservableObject
{
public string Name
{
get => user.Name;
set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
}
}
呼び出し時の割り当てをなくすには、静的ラムダ(キャプチャする状態がないもの)を渡してください。
コマンド
[RelayCommand]
private void Refresh() => Items.Reset();
[RelayCommand]
private async Task LoadAsync()
{
foreach (var item in await service.GetItemsAsync())
Items.Add(item);
}
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
await using var stream = await http.GetStreamAsync(url, token);
// ...
}
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync() => repo.SaveAsync(Name!);
private bool CanSave() => !string.IsNullOrWhiteSpace(Name);
コマンドのライフタイムを明示的に管理する必要がある場合、または複雑なソースからコマンドを合成する場合に限り、RelayCommand/AsyncRelayCommand のコンストラクターを直接使用してください。属性を使う方法で、約 95% のケースに対応できます。
同期/非同期/キャンセル可能/並行実行/エラーの表面化に関するレシピについては、references/relaycommand-cookbook.md を参照してください。
基底クラスの選択
| 基底クラス | 使用する場面 |
|---|---|
ObservableObject | デフォルトです。INotifyPropertyChanged + INotifyPropertyChanging + SetProperty オーバーロード + Task プロパティ用の SetPropertyAndNotifyOnCompletion を提供します |
ObservableValidator | VM で INotifyDataErrorInfo が必要な場合(フォーム、設定入力など) |
ObservableRecipient | VM が IMessenger メッセージを送受信する場合。mvvm-toolkit-messenger スキルを参照してください |
C# は単一継承です。ObservableValidator と ObservableRecipient
はいずれも ObservableObject を継承しているため、組み合わせるには
コンポジションが必要です(例:ObservableValidator に IMessenger を
注入します)。
検証
using System.ComponentModel.DataAnnotations;
public sealed partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
// submit...
}
}
その他のエントリーポイント:TrySetProperty、ValidateProperty(value, name)、
ClearAllErrors()、GetErrors(propertyName)。カスタムルールでは
[CustomValidation] メソッドとカスタム ValidationAttribute サブクラスを
サポートしています。
バリデーターの全機能については、references/validation.mdを
参照してください。
主な注意点
partialの付け忘れ。 クラス(およびすべての外側の型)をpartialにする必要があります。コンパイルエラーMVVMTK0008/MVVMTK0042が発生します。- フィールド名が PascalCase。
[ObservableProperty] private string Name;は生成されるプロパティと衝突します。name、_name、またはm_nameを使用してください。 [RelayCommand]でのasync void。 ジェネレーターがIAsyncRelayCommandとして ラップするのは、Taskを返すメソッドのみです。async voidは同期的なRelayCommandになり、例外が監視されません。常にTaskを返してください。[NotifyCanExecuteChangedFor]の付け忘れ。CanSave()がtrueを返すようになっても、 Save ボタンは無効のままです。[ObservableProperty]フィールドが保持する同じ参照を変更する。EqualityComparer<T>.Defaultはtrueを返すため、通知が発生しません。インスタンスを 変更するのではなく、置き換えてください。
診断の一覧(MVVMTK0xxx)やその他の注意点については、
references/troubleshooting.mdを参照してください。
エンドツーエンドの簡単なウォークスルー
ジェネレーター + コマンド +
[NotifyCanExecuteChangedFor] を示す、2 ペイン構成の Notes アプリです:
public sealed partial class NoteViewModel(INotesService notes,
IMessenger messenger) : ObservableRecipient(messenger)
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
private string? filename;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? text;
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync()
{
Messenger.Send(new NoteSavedMessage(Filename!));
return notes.SaveAsync(Filename!, Text!);
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private Task DeleteAsync() => notes.DeleteAsync(Filename!);
private bool CanSave() =>
!string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text);
private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename);
}
サンプル全体(DI の設定、View のコードビハインド、XAML、単体テスト)については、
references/end-to-end-walkthrough.mdを参照してください。
リファレンスと関連スキル
| トピック | 参照先 |
|---|---|
| ソースジェネレーター属性のリファレンス | references/source-generators.md |
| RelayCommand のレシピ | references/relaycommand-cookbook.md |
| 検証の詳細 | references/validation.md |
| Notes アプリのウォークスルー全体 | references/end-to-end-walkthrough.md |
MVVMTK0xxx 診断と注意点 | references/troubleshooting.md |
| Messenger の pub/sub | 関連スキル:mvvm-toolkit-messenger |
Microsoft.Extensions.DependencyInjection の設定 | 関連スキル:mvvm-toolkit-di |
外部ソース: