NuExt.Minimal.Mvvm.SourceGenerator
0.8.3
Prefix Reserved
dotnet add package NuExt.Minimal.Mvvm.SourceGenerator --version 0.8.3
NuGet\Install-Package NuExt.Minimal.Mvvm.SourceGenerator -Version 0.8.3
<PackageReference Include="NuExt.Minimal.Mvvm.SourceGenerator" Version="0.8.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="NuExt.Minimal.Mvvm.SourceGenerator" Version="0.8.3" />
<PackageReference Include="NuExt.Minimal.Mvvm.SourceGenerator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add NuExt.Minimal.Mvvm.SourceGenerator --version 0.8.3
#r "nuget: NuExt.Minimal.Mvvm.SourceGenerator, 0.8.3"
#:package NuExt.Minimal.Mvvm.SourceGenerator@0.8.3
#addin nuget:?package=NuExt.Minimal.Mvvm.SourceGenerator&version=0.8.3
#tool nuget:?package=NuExt.Minimal.Mvvm.SourceGenerator&version=0.8.3
NuExt.Minimal.Mvvm.SourceGenerator
NuExt.Minimal.Mvvm.SourceGenerator is a Roslyn source generator for the lightweight MVVM framework NuExt.Minimal.Mvvm. It emits boilerplate for properties, commands, validation, and localization at compile time, so you can focus on app logic.
Requirements
- Consumer target frameworks: .NET Standard 2.0/2.1, .NET 8/9/10, and .NET Framework 4.6.2+
- Language: C# 12+
- Core package:
NuExt.Minimal.Mvvmor a compatible UI integration package
Features
- Properties
- Generates observable properties from backing fields and partial properties.
- Emits cached event arguments for allocation-free notification paths.
- Initializes generated backing fields for non-nullable partial
stringproperties withstring.Emptyin nullable-enabled code. - Supports related-property notifications through
[AlsoNotify]. - Supports attributes on generated members through
[CustomAttribute].
- Commands
- Generates command properties from synchronous and asynchronous methods.
- Supports complete command generation through
[GenerateCommands]and[Command]. - Generates command creation, initialization, lookup, enumeration, and cleanup overrides.
- Discovers
CanExecutecallbacks in the current type and its accessible base types. - Supports explicit command names,
CanExecutecallback names, command-property accessibility, and WPFCommandManagerintegration.
- Validation (
INotifyDataErrorInfo)- Uses thread-safe, copy-on-write error storage.
- Supports property-level and entity-level validation scopes.
- Provides synchronous and asynchronous validation helpers.
- Localization
- Generates localization properties, nested sections, runtime JSON loading, and optional WPF markup extensions from JSON files supplied through MSBuild
AdditionalFiles.
- Generates localization properties, nested sections, runtime JSON loading, and optional WPF markup extensions from JSON files supplied through MSBuild
Attributes
[GenerateCommands]: Enables complete command generation for a partial class derived from a compatible command ViewModel base class.[Command]: Generates a command property and the code that creates its command instance from the annotated method, and includes the command in the generated lifecycle infrastructure.- Options:
PropertyName,CanExecute,Getter,Setter.
- Options:
[Notify]: Generates an observable property from a field or partial property. When applied to a method, it provides command-property-only generation.- Options:
PropertyName,CallbackName,Getter,Setter.
- Options:
[AlsoNotify]: Raises notifications for additional properties when the generated property changes.[CustomAttribute]: Applies an attribute to a generated member.[UseCommandManager]: Enables automaticCanExecutereevaluation through WPFCommandManager.RequerySuggested. Can be applied together with[Command]for complete command generation or with[Notify]for command-property-only generation.[NotifyDataErrorInfo]: GeneratesINotifyDataErrorInfoinfrastructure.[Localize]: Generates localization properties, nested sections, and runtime JSON loading from a file supplied through MSBuildAdditionalFiles.- Option:
MarkupExtensiongenerates an optional WPF markup extension with the specified name.
- Option:
Install
dotnet add package NuExt.Minimal.Mvvm.SourceGenerator
Install the core package or a compatible UI integration package:
dotnet add package NuExt.Minimal.Mvvm
dotnet add package NuExt.Minimal.Mvvm.Wpf
dotnet add package NuExt.Minimal.Mvvm.MahApps.Metro
Quick Start
using Minimal.Mvvm;
using Minimal.Mvvm.Wpf;
[GenerateCommands]
public partial class PersonViewModel : CommandViewModelBase
{
[Notify, AlsoNotify(nameof(FullName))]
private string? _name;
[Notify, AlsoNotify(nameof(FullName))]
private string? _surname;
public string FullName => $"{_surname} {_name}";
private bool CanShowInfo(string? text)
{
return !string.IsNullOrWhiteSpace(text);
}
[Command]
private async Task ShowInfoAsync(string? text)
{
await Task.Delay(100);
}
}
[GenerateCommands] enables complete command generation for the class. [Command] generates ShowInfoCommand and the code that creates its command instance, binds ShowInfoAsync to the compatible CanShowInfo callback, and integrates the command into the ViewModel command lifecycle.
Conceptually, the generator produces:
NameandSurnameproperties withPropertyChangednotifications.FullNamenotifications when either generated property changes.- An asynchronous
ShowInfoCommandbound toShowInfoAsync. - Automatic discovery and binding of
CanShowInfo. - Command initialization, lookup, enumeration, and cleanup overrides.
- Cached event arguments for allocation-free notification paths.
Command generation
Complete command generation
Apply [GenerateCommands] to a partial class and [Command] to its command methods:
using Minimal.Mvvm;
using Minimal.Mvvm.Wpf;
[GenerateCommands]
public partial class EditorViewModel : CommandViewModelBase
{
private bool CanSave()
{
return HasChanges;
}
[Command(Setter = AccessModifier.Private)]
private Task SaveAsync(CancellationToken cancellationToken)
{
return SaveCoreAsync(cancellationToken);
}
}
For every [Command] method, the generator:
- Generates the command property.
- Generates creation of the appropriate synchronous,
Task-based, orValueTask-based command instance. - Resolves and binds a
CanExecutecallback when one is available. - Generates overrides for command creation, lookup, enumeration, and cleanup.
- Preserves commands declared by base ViewModels through generated
basecalls.
The annotated class must be partial and derive from a compatible base class that supplies the command infrastructure, such as CommandViewModelBase, ControlViewModelBase, or WindowViewModelBase from NuExt.Minimal.Mvvm.Wpf.
Command methods
A command method may be synchronous or asynchronous and may have one command parameter. Asynchronous methods may additionally accept a CancellationToken when supported by the corresponding command type.
The property name is derived from the method name. A trailing Async is removed before appending Command:
[Command]
private void Refresh()
{
}
[Command]
private Task SaveAsync(CancellationToken cancellationToken)
{
return SaveCoreAsync(cancellationToken);
}
This generates RefreshCommand and SaveCommand.
Use PropertyName when a different command-property name is required:
[Command(PropertyName = "CommitCommand")]
private Task SaveAsync(CancellationToken cancellationToken)
{
return SaveCoreAsync(cancellationToken);
}
CanExecute resolution
When CanExecute is not specified, the generator looks for an accessible method named Can{CommandMethodName}. For an asynchronous command method, the trailing Async suffix is removed first.
private bool CanSave()
{
return HasChanges;
}
[Command]
private Task SaveAsync(CancellationToken cancellationToken)
{
return SaveCoreAsync(cancellationToken);
}
Specify an explicit callback when the conventional name is not appropriate:
private bool CanModify()
{
return IsUsable;
}
[Command(CanExecute = nameof(CanModify))]
private void ExpandOrCollapse(bool expand)
{
}
Explicit callback lookup includes accessible methods declared by the current class and its base classes. The callback name is emitted as a method group, allowing the C# compiler to perform the final conversion check against the selected command constructor.
Generated command lifecycle
Complete generation implements the command infrastructure required by the base class:
CreateCommands()creates commands declared by the current class after calling the base implementation.GetCurrentCommand(...)maps a command method to its generated command property and delegates unknown methods to the base implementation.GetAllCommands(...)includes base commands and appends commands declared by the current class.NullifyCommands()clears generated command properties and then invokes the base implementation.
This infrastructure allows compatible ViewModel bases to cancel executing asynchronous commands, wait for command completion, raise CanExecuteChanged, and release command instances without runtime reflection.
Command-property-only generation
Apply [Notify] to a method when the command property should be generated but command construction and lifecycle integration must remain explicit:
using Minimal.Mvvm;
public partial class EditorViewModel : ViewModelBase
{
public EditorViewModel()
{
SaveCommand = new AsyncCommand(SaveAsync, CanSave);
}
[Notify(Setter = AccessModifier.Private)]
private Task SaveAsync(CancellationToken cancellationToken)
{
return SaveCoreAsync(cancellationToken);
}
private bool CanSave()
{
return HasChanges;
}
}
This mode generates the command property only. The application remains responsible for constructing the command and integrating it into any command lifecycle.
Validation semantics (concise)
- Scopes:
null/""= entity‑level;"PropertyName"= property‑level. - Clearing:
ClearErrors(null)orClearErrors("")→ clears entity‑level only.ClearErrors(nameof(Property))→ clears that property only.ClearAllErrors()→ full reset (raisesErrorsChangedper affected scope + updatesHasErrors).
- Updates:
SetError,SetErrors(merge),ReplaceErrors(replace),RemoveError.- Copy‑on‑write lists + CAS; early‑return on no‑op (no redundant
ErrorsChanged).
- Copy‑on‑write lists + CAS; early‑return on no‑op (no redundant
Threading: notifications are marshaled to UI via a lazily captured
SynchronizationContext.
Async validation helpers (SetValidationTask / CancelValidationTask)
When a class is annotated with [NotifyDataErrorInfo], the generator exposes helpers to manage asynchronous per‑property validation tasks:
SetValidationTask(Task task, CancellationTokenSource cts, string propertyName)
Associates the task and itsCancellationTokenSourcewithpropertyName, cancelling and disposing the previous token source for that scope before replacing the tracked task.CancelValidationTask(string propertyName = null)
Cancels and disposes the trackedCancellationTokenSourceforpropertyName; withnull, cancels all tracked validation operations.CancelAllValidationTasks()
Convenience wrapper to cancel every tracked task (use in teardown).HasErrorsFor(scope)/SetError/SetErrors/ReplaceErrors/RemoveError
Same semantics as in the synchronous flow (entity =null/"").
Usage pattern (per property):
[NotifyDataErrorInfo]
public partial class LoginViewModel : ViewModelBase
{
private readonly CancellationTokenSource _cts = new();
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(UserName))
{
// Cancel previous async validation for this property
CancelValidationTask(nameof(UserName));
// Run sync validation first (one notification, via ReplaceErrors)
ValidateUserNameSync();
// Start async validation with a linked CTS
var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
var task = ValidateUserNameAsync(UserName, cts.Token);
// Observe faults to avoid UnobservedTaskException (no UI marshal)
task.ContinueWith(
t => { _ = t.Exception; },
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
// Publish/track the task for this property
SetValidationTask(task, cts, nameof(UserName));
}
}
private async Task ValidateUserNameAsync(string userName, CancellationToken ct)
{
try
{
// Debounce
await Task.Delay(250, ct);
// If sync errors still exist — skip async
if (HasErrorsFor(nameof(UserName)))
return;
// Ensure value is still current
if (!string.Equals(UserName, userName, StringComparison.Ordinal))
return;
// Do the actual async check (no UI context capture)
var locked = await _auth.IsUserLockedAsync(userName, ct).ConfigureAwait(false);
if (locked) SetError("This user is locked.", nameof(UserName));
else ClearErrors(nameof(UserName));
}
catch (OperationCanceledException) { /* ignore */ }
}
// Teardown: cancel all pending validations
protected override async Task UninitializeAsyncCore(CancellationToken ct)
{
CancelAllValidationTasks();
await base.UninitializeAsyncCore(ct);
}
}
Notes
- Always cancel the previous task for the same property before starting a new one:
CancelValidationTask(nameof(Property)). - Prefer a linked
CancellationTokenSourcetied to the VM lifetime. - Mark faults as observed (via
ContinueWith(OnlyOnFaulted|ExecuteSynchronously)) or catch inside the async validator. - Use
ConfigureAwait(false)inside validators; the generator marshals notifications to the UI via a lazily capturedSynchronizationContext. - Keep
CanExecute/UI logic driven by property‑level errors (e.g.,HasErrorsFor(nameof(UserName))), while entity‑level banners are cleared at the start of a new attempt (ClearErrors("")).
WPF notes (displaying errors)
- Field‑level errors bind with
Validation.HasErrorand delayed index access to(Validation.Errors)[0](avoid warnings). - Entity‑level errors can be shown by a hidden binding host (bind to VM with
ValidatesOnNotifyDataErrors=True, read itsValidation.Errors).
See the WpfAppSample in the repository for a minimal, production‑style pattern (entity banner + field messages).
UseCommandManagerAttribute
When applied together with [Command] or [Notify], enables automatic CanExecute reevaluation for the generated command property by subscribing to the WPF CommandManager.RequerySuggested event.
Complete command generation
using Minimal.Mvvm;
using Minimal.Mvvm.Wpf;
[GenerateCommands]
public partial class EditorViewModel : CommandViewModelBase
{
private bool CanSave()
{
return HasChanges;
}
[Command(Setter = AccessModifier.Private), UseCommandManager]
private void Save()
{
}
}
Command-property-only generation
using Minimal.Mvvm;
public partial class EditorViewModel : ViewModelBase
{
public EditorViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}
[Notify, UseCommandManager]
private IRelayCommand? _saveCommand;
private void Save()
{
}
private bool CanSave()
{
return true;
}
}
Localization (AdditionalFiles)
The localization generator provides a lightweight compile-time localization model for desktop MVVM applications. It generates a strongly typed API from JSON, supports nested sections for structured access, and allows localization values to be overridden from another JSON file at runtime without reflection.
Add the default localization JSON file through MSBuild AdditionalFiles:
<ItemGroup>
<AdditionalFiles Include="Resources\local.en.json" />
</ItemGroup>
Apply [Localize] to a static partial class:
using Minimal.Mvvm;
[Localize("local.en.json")]
public static partial class Loc
{
}
The JSON root may contain both string values and nested sections:
{
"Cancel": "Cancel",
"Close": "Close",
"Login": {
"Title": "Login",
"Password": "Password",
"RememberMe": "Remember me",
"SignIn": "Sign in",
"UserName": "User name"
},
"Validation": {
"PasswordRequired": "Password is required.",
"PasswordTooShort": "Password must be at least {0} characters."
},
"MainWindow": {
"Title": "Sample application"
}
}
Semantic keys are recommended because they produce a clear and stable generated API. Non-semantic keys are also supported and are converted to valid C# property names. Localization files should normally have the same keys and section structure, with only their values differing.
String values are generated as static properties. JSON objects are generated as nested static classes:
var cancel = Loc.Cancel;
var title = Loc.MainWindow.Title;
var message = Loc.Validation.PasswordTooShort;
Sections may be nested to any depth and may coexist with string values at the same level.
Loading localization at runtime
The generated class provides LoadFromJsonFile for loading another localization file without reflection:
var fileName = Path.Combine(configDirectory, $"local.{culture}.json");
if (File.Exists(fileName))
{
Loc.LoadFromJsonFile(fileName);
}
Values missing from the runtime file retain their compile-time defaults. Unknown keys in the runtime file are ignored. Invalid JSON and localization values other than strings or objects cause an exception.
Runtime loading requires a reference to System.Text.Json.
Using localization in WPF XAML
Flat localization properties can be referenced directly through x:Static:
<Button Content="{x:Static local:Loc.Cancel}" />
This is the preferred syntax for flat properties because the referenced property is checked by the XAML compiler.
Nested localization sections cannot be referenced directly through x:Static. To access them from WPF XAML, enable the generated markup extension:
using Minimal.Mvvm;
[Localize("local.en.json", MarkupExtension = "Loc")]
internal static partial class Loc
{
}
The value "Loc" generates LocExtension. The standard XAML markup-extension naming convention allows it to be used without the Extension suffix:
<Window
xmlns:local="clr-namespace:SampleApp"
Title="{local:Loc MainWindow.Title}">
<StackPanel>
<Button Content="{x:Static local:Loc.Cancel}" />
<TextBlock Text="{local:Loc Login.Title}" />
<TextBlock Text="{local:Loc Login.UserName}" />
</StackPanel>
</Window>
Flat properties may also be accessed through the generated markup extension:
<Button Content="{local:Loc Cancel}" />
An unknown markup-extension path is displayed as the path itself, making incorrect keys visible in the application and XAML Designer instead of failing window creation.
MarkupExtension is optional and requires WPF. When it is omitted or set to null, no markup extension is generated.
Ecosystem
- NuExt.Minimal.Mvvm
- NuExt.Minimal.Behaviors.Wpf
- NuExt.Presentation.Wpf
- NuExt.Minimal.Mvvm.Wpf
- NuExt.Minimal.Mvvm.MahApps.Metro
- NuExt.System
- NuExt.System.Data
- NuExt.System.Data.SQLite
Contributing
Issues and PRs are welcome. Keep changes minimal and performance-conscious.
License
MIT. See LICENSE.
Learn more about Target Frameworks and .NET Standard.
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.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.3 | 0 | 10/29/2026 |
| 0.8.2 | 52 | 10/27/2026 |
| 0.8.1 | 64 | 10/26/2026 |
| 0.8.0 | 71 | 10/23/2026 |
| 0.7.4 | 121 | 10/2/2026 |
| 0.7.3 | 175 | 6/29/2026 |
| 0.7.2 | 157 | 3/16/2026 |
| 0.7.1 | 144 | 2/23/2026 |
| 0.7.0 | 146 | 2/13/2026 |
| 0.6.0 | 147 | 1/11/2026 |
| 0.5.2 | 339 | 12/15/2025 |
| 0.5.1 | 274 | 12/14/2025 |
| 0.4.1 | 487 | 12/10/2025 |
| 0.4.0 | 214 | 12/5/2025 |
| 0.3.2 | 203 | 1/26/2025 |
| 0.3.1 | 194 | 1/22/2025 |
| 0.3.0 | 189 | 1/21/2025 |