MvvmAIO.Prism.SourceGenerators
0.3.1
dotnet add package MvvmAIO.Prism.SourceGenerators --version 0.3.1
NuGet\Install-Package MvvmAIO.Prism.SourceGenerators -Version 0.3.1
<PackageReference Include="MvvmAIO.Prism.SourceGenerators" Version="0.3.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="MvvmAIO.Prism.SourceGenerators" Version="0.3.1" />
<PackageReference Include="MvvmAIO.Prism.SourceGenerators"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add MvvmAIO.Prism.SourceGenerators --version 0.3.1
#r "nuget: MvvmAIO.Prism.SourceGenerators, 0.3.1"
#:package MvvmAIO.Prism.SourceGenerators@0.3.1
#addin nuget:?package=MvvmAIO.Prism.SourceGenerators&version=0.3.1
#tool nuget:?package=MvvmAIO.Prism.SourceGenerators&version=0.3.1
Prism.SourceGenerators
参与贡献见 CONTRIBUTING.md。
Roslyn source generators for the Prism MVVM library.
CI Status
- Open the workflow page above to see the latest pipeline status.
- The
Testsbadge displays the latest passed/failed/skipped counts. - The run also uploads a
test-resultsartifact (.trx) for detailed test reports.
Project Structure
Prism.SourceGenerators/ # Shared project (.shproj/.projitems/.props + source code)
Prism.SourceGenerators.Roslyn4001/ # Roslyn 4.0.1
Prism.SourceGenerators.Roslyn4031/ # Roslyn 4.3.1
Prism.SourceGenerators.Roslyn4120/ # Roslyn 4.12.0
Prism.SourceGenerators.Roslyn5000/ # Roslyn 5.0.0
Prism.SourceGenerators.Core/ # MvvmAIO.Prism.Core (attributes), bundled in MvvmAIO.Prism.SourceGenerators
Prism.Bcl.Commands/ # MvvmAIO.Prism.Bcl.Commands (Prism 8 AsyncDelegateCommand package, install manually)
Samples (Avalonia): Prism.SourceGenerators.Samples — Prism 8 / Prism 9 demo apps consuming MvvmAIO.Prism.SourceGenerators from NuGet.
Generators
[ObservableProperty]
Generates observable properties for classes inheriting from BindableBase. Supports two usage modes depending on the C# language version.
Field target (all C# versions)
Annotate a private field with [ObservableProperty] to generate a property that calls SetProperty in the setter. By default the generated property is public. Pass PropertyAccess (positional or named PropertyAccess = …) to choose another accessibility (internal, protected, private, protected internal, private protected).
// C# 12 or earlier
using Prism.SourceGenerators;
public partial class MainViewModel : BindableBase
{
[ObservableProperty]
private string _title = "Hello";
[ObservableProperty(PropertyAccess.Internal)]
// or: [ObservableProperty(PropertyAccess = PropertyAccess.Internal)]
private int _count;
// Generated: setter calls OnTitleChanging*, then BindableBase.SetProperty(ref _title, value, () => { OnTitleChanged*; }),
// then optional RaisePropertyChanged for [NotifyPropertyChangedFor] / command refresh attributes.
}
For partial property targets, the accessibility on the property declaration is used; PropertyAccess is ignored.
Partial property target (C# 13+ with field keyword)
Annotate a partial property with [ObservableProperty] to generate the implementing declaration using the field keyword (semi-auto property).
// C# 13+ / .NET 9+ (requires LangVersion 13.0+ or preview)
using Prism.SourceGenerators;
public partial class MainViewModel : BindableBase
{
[ObservableProperty]
public partial string Title { get; set; } = "Hello";
// Generated: same SetProperty(ref field, value, onChanged) pattern with OnChanging/OnChanged hooks.
}
The partial property approach eliminates the need for a separate backing field and provides a cleaner API surface. Both modes can coexist in the same project.
OnChanging / OnChanged partial methods
For every [ObservableProperty], the generator emits four partial method declarations that you can optionally implement to react to changes. OnXxxChanging hooks run before the backing field is updated; OnXxxChanged hooks run after:
public partial class MainViewModel : BindableBase
{
[ObservableProperty]
public partial int Age { get; set; }
// Generated declarations (implement any subset):
// partial void OnAgeChanging(int value);
// partial void OnAgeChanging(int oldValue, int newValue);
// partial void OnAgeChanged(int value);
// partial void OnAgeChanged(int oldValue, int newValue);
partial void OnAgeChanging(int oldValue, int newValue)
{
Debug.WriteLine($"Age about to change from {oldValue} to {newValue}");
}
partial void OnAgeChanged(int oldValue, int newValue)
{
Debug.WriteLine($"Age changed from {oldValue} to {newValue}");
}
}
The generated setter uses EqualityComparer<T>.Default.Equals for an early-out. When the value differs, it calls both OnChanging overloads, then SetProperty(ref storage, value, onChanged) so overrides of SetProperty run on the same path as hand-written Prism properties. The onChanged callback invokes both OnChanged overloads, then SetProperty raises PropertyChanged for the generated property. Any [NotifyPropertyChangedFor] names and [NotifyCanExecuteChangedFor] command refreshes are emitted after that call.
[NotifyPropertyChangedFor]
Apply to a field or partial property alongside [ObservableProperty] to automatically raise PropertyChanged for additional dependent properties when the annotated property changes.
public partial class MainViewModel : BindableBase
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName = "";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _lastName = "";
public string FullName => $"{FirstName} {LastName}";
}
Supports multiple property names via [NotifyPropertyChangedFor(nameof(A), nameof(B))] or multiple attribute instances.
[NotifyCanExecuteChangedFor]
Apply to a field or partial property alongside [ObservableProperty] to automatically invoke RaiseCanExecuteChanged() on the named commands when the property value changes.
public partial class EditorViewModel : BindableBase
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string _name = "";
[DelegateCommand(CanExecute = nameof(CanSave))]
private void Save() { /* ... */ }
private bool CanSave() => !string.IsNullOrEmpty(Name);
}
The generated setter calls SaveCommand?.RaiseCanExecuteChanged() after RaisePropertyChanged. Multiple commands are supported via [NotifyCanExecuteChangedFor(nameof(A), nameof(B))] or repeated attributes. Names may reference either an existing member on the type or the generated command of a method annotated with [DelegateCommand] / [AsyncDelegateCommand] (e.g. method Save yields SaveCommand). Unresolved names are reported as PSG2005 (warning) and the setter is still emitted.
Forwarding attributes to the generated property
For field targets, attributes you write with the explicit [property: Xxx] target are forwarded onto the generated property:
public partial class Vm : BindableBase
{
[ObservableProperty]
[property: System.Text.Json.Serialization.JsonIgnore]
[property: System.ComponentModel.DataAnnotations.Required]
private string _password = "";
}
becomes
[global::System.Text.Json.Serialization.JsonIgnoreAttribute]
[global::System.ComponentModel.DataAnnotations.RequiredAttribute]
public string Password { get { ... } set { ... } }
For partial property targets, every attribute you put on the partial declaration is forwarded onto the implementing declaration (with the exception of generator-owned attributes — [ObservableProperty], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor] — which are stripped). Forwarded attributes are emitted with fully-qualified type names so they do not depend on using directives present in the generated file.
Argument expressions inside the forwarded attribute are emitted verbatim. Use literal/
nameof/typeofarguments or fully-qualified type references in argument positions if yourusingdirectives aren't visible to the generated file.
[DelegateCommand]
Generates DelegateCommand or AsyncDelegateCommand properties from methods.
- Synchronous methods (
void) generateDelegateCommand/DelegateCommand<T> - Async methods returning non-generic
Task,ValueTask, orValueTask<TResult>generateAsyncDelegateCommand/AsyncDelegateCommand<T>.ValueTask/ValueTask<TResult>are wired via.AsTask()in the generated delegate so Prism’sFunc<Task>/Func<T, Task>constructors are used.Task<TResult>is not supported for execute methods (unchanged). Execute methods that take aCancellationTokencannot returnValueTask/ValueTask<TResult>with the current emission shape (PSG1001). - For Prism < 9.0, use NuGet
MvvmAIO.Prism.SourceGenerators, which addsMvvmAIO.Prism.Corefor source-generator attributes. For Prism.Core 8.1.97 async commands, installMvvmAIO.Prism.Bcl.Commandsmanually soAsyncDelegateCommandexists. If those assemblies are missing while async commands are used, PSG3002 is reported. - C# 14+: Command properties use the
fieldkeyword (no separate backing field) - C# 13 and earlier: Command properties use a traditional backing field
using Prism.SourceGenerators;
public partial class MainViewModel : BindableBase
{
// Generates: DelegateCommand IncrementCommand
[DelegateCommand]
private void Increment() { /* ... */ }
// Generates: AsyncDelegateCommand LoadDataCommand
[DelegateCommand]
private async Task LoadDataAsync() { /* ... */ }
// With CanExecute support
[DelegateCommand(CanExecute = nameof(CanSubmit))]
private void Submit() { /* ... */ }
private bool CanSubmit() => true;
}
Generated output comparison
C# 14+ (LangVersion >= 14) — uses field keyword:
// No backing field needed
public DelegateCommand IncrementCommand => field ??= new DelegateCommand(Increment);
C# 13 and earlier — traditional backing field:
private DelegateCommand? _incrementCommand;
public DelegateCommand IncrementCommand => _incrementCommand ??= new DelegateCommand(Increment);
[AsyncDelegateCommand]
Dedicated attribute for async methods with advanced Prism-style features.
On Prism 9+, uses the framework types; on Prism 8.1.97, install MvvmAIO.Prism.Bcl.Commands for the same fluent surface: EnableParallelExecution, CancelAfter, Catch, CancellationTokenSourceFactory, and ObservesCanExecute.
using Prism.SourceGenerators;
public partial class MainViewModel : BindableBase
{
// Parallel execution enabled
[AsyncDelegateCommand(EnableParallelExecution = true)]
private async Task FetchDataAsync() { /* ... */ }
// With error handling and CanExecute
[AsyncDelegateCommand(CanExecute = nameof(CanSave), Catch = nameof(HandleError))]
private async Task SaveAsync() { /* ... */ }
private bool CanSave() => true;
private void HandleError(Exception ex) { /* ... */ }
}
[ObservesProperty]
Automatically re-evaluates CanExecute when the specified properties change.
Works with both [DelegateCommand] and [AsyncDelegateCommand].
using Prism.SourceGenerators;
public partial class MainViewModel : BindableBase
{
[ObservableProperty]
private bool _isValid;
[DelegateCommand(CanExecute = nameof(CanSubmit))]
[ObservesProperty(nameof(IsValid))]
private void Submit() { /* ... */ }
// Multiple properties
[AsyncDelegateCommand(CanExecute = nameof(CanSave))]
[ObservesProperty(nameof(Counter), nameof(IsActive))]
private async Task SaveAsync() { /* ... */ }
}
[BindableBase]
Apply to a class that does not inherit from Prism.Mvvm.BindableBase to automatically generate an INotifyPropertyChanged implementation. The generated code includes PropertyChanged event, SetProperty<T>, RaisePropertyChanged, and OnPropertyChanged methods.
using Prism.SourceGenerators;
[BindableBase]
public partial class SimpleViewModel
{
private string _message = "Hello!";
public string Message
{
get => _message;
set => SetProperty(ref _message, value);
}
}
If the class already inherits from BindableBase or a base class that implements INotifyPropertyChanged, no code is generated.
Diagnostics
| ID | Description |
|---|---|
| PSG0001 | Class with [ObservableProperty] members must be partial |
| PSG0002 | Class with [DelegateCommand] / [AsyncDelegateCommand] method must be partial |
| PSG0003 | Property with [ObservableProperty] must be declared as partial |
| PSG0004 | Class with [BindableBase] must be partial |
| PSG1001 | Method signature is invalid for [DelegateCommand] |
| PSG1002 | Method signature is invalid for [AsyncDelegateCommand] |
| PSG2001 | Catch handler member was not found |
| PSG2002 | Catch handler signature is not compatible |
| PSG2003 | CanExecute member was not found |
| PSG2004 | Observed property was not found |
| PSG2005 | [NotifyCanExecuteChangedFor] references a command that was not found |
| PSG2006 | CanExecute names a member whose signature is not compatible with the command |
| PSG3002 | AsyncDelegateCommand not found; install MvvmAIO.Prism.SourceGenerators and, on Prism.Core 8.1.97, MvvmAIO.Prism.Bcl.Commands (or upgrade to Prism 9+) |
Quick fix: PSG0001–PSG0004 all have an IDE code fix that inserts the missing
partialmodifier (Ctrl+. / Alt+Enter on the squiggle, or "Fix all in document/project/solution" to apply across the whole codebase).
Installation
<PackageReference Include="MvvmAIO.Prism.SourceGenerators" Version="0.2.0" />
Or:
dotnet add package MvvmAIO.Prism.SourceGenerators
Building
dotnet build Prism.SourceGenerators.slnx
Nuke Build
This repository uses Nuke as the build orchestration layer for local automation and CI.
- Main source solution:
Prism.SourceGenerators.slnx - Build automation solution:
build.slnx(contains onlybuild/_build.csproj)
Common commands:
# CI pipeline locally (clean + restore + compile + test)
dotnet run --project build/_build.csproj -- --target Ci --configuration Release
# Pack NuGet package (optionally override version)
dotnet run --project build/_build.csproj -- --target Pack --configuration Release --version 0.2.0
# Publish NuGet packages (MvvmAIO.Prism.SourceGenerators + MvvmAIO.Prism.Bcl.Commands)
dotnet run --project build/_build.csproj -- --target Publish --configuration Release --version 0.2.0 --nuget-api-key <NUGET_API_KEY>
Requirements
- .NET 10 SDK
- Visual Studio 2022 17.13+ / Rider / VS Code with C# Dev Kit (for
.slnxsupport)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.