跳到主要内容
知仓学习社ZHICANG

mvvm-toolkit

CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecut…

不碰外部(只输出文字)无严重或高危命中github/awesome-copilot

它会碰到什么

扫了多少6 个文本文件,50 KB
它会碰到什么不碰外部(只输出文字)
命中总数0 处
命中统计严重 0 · 高 0 · 中 0 · 低 0

这一栏是扫描器报的事实,不是结论。命中多不等于有毒(安全工具、规则库、示例脚本本来就会包含危险写法),命中少也不等于干净。它和你手上的凭据、文件、网络有什么关系,需要你自己看。

技能内容

CommunityToolkit.Mvvm (core)

Use this skill when authoring or reviewing ViewModels, properties,

commands, or validation in apps that use CommunityToolkit.Mvvm 8.x.

> Companion skills. Load mvvm-toolkit-messenger for IMessenger

> pub/sub patterns. Load mvvm-toolkit-di for

> Microsoft.Extensions.DependencyInjection integration.

> Quick recap. [ObservableProperty] on private fields in partial

> classes; [RelayCommand] on instance methods; inherit from

> ObservableObject (or ObservableValidator for input forms,

> ObservableRecipient when using IMessenger).


Package & setup

<ItemGroup>
  <PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
</ItemGroup>

Targets: netstandard2.0, netstandard2.1, net6.0+. Works on .NET, .NET

Framework, Mono. Source generators ship in the same NuGet — no extra

analyzer reference required.

Namespaces:

using CommunityToolkit.Mvvm.ComponentModel;   // ObservableObject, [ObservableProperty]
using CommunityToolkit.Mvvm.Input;             // [RelayCommand], RelayCommand, AsyncRelayCommand

> Universal rule. Every type that uses [ObservableProperty] or

> [RelayCommand] — and every enclosing type, if nested — must be

> declared partial. Without it, the generators emit

> MVVMTK0008 / MVVMTK0042.


Source generators cheat sheet

| Attribute | Applied to | Generates |

|-----------|-----------|-----------|

| [ObservableProperty] | private field | Public INotifyPropertyChanged property + OnXxxChanging/OnXxxChanged partial-method hooks |

| [NotifyPropertyChangedFor(nameof(Other))] | observable field | Also raises PropertyChanged for the listed property |

| [NotifyCanExecuteChangedFor(nameof(MyCommand))] | observable field | Calls MyCommand.NotifyCanExecuteChanged() on change |

| [NotifyDataErrorInfo] | observable field on ObservableValidator | Calls ValidateProperty(value) from the setter |

| [NotifyPropertyChangedRecipients] | observable field on ObservableRecipient | Broadcast(old, new) after the change |

| [RelayCommand] | instance method | Lazy RelayCommand / AsyncRelayCommand exposed as IRelayCommand / IAsyncRelayCommand |

| [RelayCommand(CanExecute = nameof(CanX))] | instance method | Wires CanExecute to a method or property |

| [RelayCommand(IncludeCancelCommand = true)] | async method with CancellationToken | Also generates XxxCancelCommand |

| [RelayCommand(AllowConcurrentExecutions = true)] | async method | Allows queued/parallel invocations (default disables while running) |

| [RelayCommand(FlowExceptionsToTaskScheduler = true)] | async method | Surfaces exceptions via ExecutionTask instead of awaiting and rethrowing |

| [property: SomeAttr] | observable field or [RelayCommand] method | Forwards SomeAttr onto the generated property (e.g., [JsonIgnore]) |

Naming. Field name / _name / m_nameName. Method LoadAsync

LoadCommand (the Async suffix is stripped; a leading On is also

stripped).

See [references/source-generators.md](references/source-generators.md) for

the full attribute reference with generated-code samples.


ViewModel patterns

Simple observable property

public partial class ContactViewModel : ObservableObject
{
    [ObservableProperty]
    private string? name;
}

Hooks: OnXxxChanging / OnXxxChanged

[ObservableProperty]
private string? name;

partial void OnNameChanged(string? value) =>
    Logger.LogInformation("Name changed to {Name}", value);

Both single-arg (value) and two-arg (oldValue, newValue) overloads

are available. Implement only the ones you need; unimplemented hooks are

elided by the compiler (zero runtime cost).

Dependent properties + dependent commands

[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();

Wrapping a non-observable model

public sealed class ObservableUser(User user) : ObservableObject
{
    public string Name
    {
        get => user.Name;
        set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
    }
}

Pass a static lambda (no captured state) to keep the call allocation-free.


Commands

[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);

Reach for manual RelayCommand / AsyncRelayCommand constructors only

when you must own the command's lifetime explicitly or compose it from

non-trivial sources. The attribute style covers ~95% of cases.

See [references/relaycommand-cookbook.md](references/relaycommand-cookbook.md)

for sync / async / cancellable / concurrency / error-surfacing recipes.


Base class selection

| Base class | Use when |

|------------|---------|

| ObservableObject | Default. INotifyPropertyChanged + INotifyPropertyChanging + SetProperty overloads + SetPropertyAndNotifyOnCompletion for Task properties |

| ObservableValidator | The VM needs INotifyDataErrorInfo (forms, settings input) |

| ObservableRecipient | The VM sends or receives IMessenger messages — see the mvvm-toolkit-messenger skill |

C# is single-inheritance: ObservableValidator and ObservableRecipient

both extend ObservableObject, so combining them requires composition

(e.g., inject IMessenger into an ObservableValidator).


Validation

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...
    }
}

Other entry points: TrySetProperty, ValidateProperty(value, name),

ClearAllErrors(), GetErrors(propertyName). Custom rules support

[CustomValidation] methods and custom ValidationAttribute subclasses.

See [references/validation.md](references/validation.md) for the full

validator surface area.


Top pitfalls

  1. Forgetting partial. Class (and every enclosing type) must be

partial. Compile error MVVMTK0008 / MVVMTK0042.

  1. PascalCase field name. [ObservableProperty] private string Name;

collides with the generated property. Use name, _name, or m_name.

  1. async void on [RelayCommand]. The generator only wraps

Task-returning methods as IAsyncRelayCommand. async void becomes

a sync RelayCommand and exceptions are unobserved. Always return

Task.

  1. Forgetting [NotifyCanExecuteChangedFor]. The Save button stays

disabled even though CanSave() would now return true.

  1. **Mutating the same reference held by an [ObservableProperty]

field.** EqualityComparer<T>.Default returns true, no notification

fires. Replace the instance instead of mutating it.

For the full diagnostic table (MVVMTK0xxx) and more pitfalls, see

[references/troubleshooting.md](references/troubleshooting.md).


End-to-end mini walkthrough

A two-pane Notes app demonstrating generators + commands +

[NotifyCanExecuteChangedFor]:

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);
}

For the full sample (DI wiring, View code-behind, XAML, unit tests), see

[references/end-to-end-walkthrough.md](references/end-to-end-walkthrough.md).


References & companion skills

| Topic | Where |

|-------|-------|

| Source generator attribute reference | [references/source-generators.md](references/source-generators.md) |

| RelayCommand recipes | [references/relaycommand-cookbook.md](references/relaycommand-cookbook.md) |

| Validation deep dive | [references/validation.md](references/validation.md) |

| Full Notes-app walkthrough | [references/end-to-end-walkthrough.md](references/end-to-end-walkthrough.md) |

| MVVMTK0xxx diagnostics & pitfalls | [references/troubleshooting.md](references/troubleshooting.md) |

| Messenger pub/sub | Companion skill: mvvm-toolkit-messenger |

| Microsoft.Extensions.DependencyInjection wiring | Companion skill: mvvm-toolkit-di |

External sources:

  • Toolkit overview: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/>
  • WinUI MVVM Toolkit tutorial: <https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro>
  • Source: <https://github.com/CommunityToolkit/dotnet>
  • Samples: <https://github.com/CommunityToolkit/MVVM-Samples>

想直接用这个技能?

本站把开放许可(MIT / Apache 等)的技能按仓库打包整理到网盘,点一下转存到你自己的网盘,不用一个个从 GitHub 拉。许可未声明的技能只给原始仓库链接,不打包。