NuExt.Minimal.Mvvm.SourceGenerator
0.7.2
Prefix Reserved
dotnet add package NuExt.Minimal.Mvvm.SourceGenerator --version 0.7.2
NuGet\Install-Package NuExt.Minimal.Mvvm.SourceGenerator -Version 0.7.2
<PackageReference Include="NuExt.Minimal.Mvvm.SourceGenerator" Version="0.7.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="NuExt.Minimal.Mvvm.SourceGenerator" Version="0.7.2" />
<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.7.2
#r "nuget: NuExt.Minimal.Mvvm.SourceGenerator, 0.7.2"
#:package NuExt.Minimal.Mvvm.SourceGenerator@0.7.2
#addin nuget:?package=NuExt.Minimal.Mvvm.SourceGenerator&version=0.7.2
#tool nuget:?package=NuExt.Minimal.Mvvm.SourceGenerator&version=0.7.2
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
- .NET Standard 2.0+, .NET 8+, .NET Framework 4.6.2+
- Language: C# 12+
- Works with:
NuExt.Minimal.Mvvm(core) and optional UI integrations (…Wpf,…MahApps.Metro).
Features
- Properties & commands
- Generates properties (change notification) from backing fields / partial properties
- Generates command properties from methods (sync/async)
- Emits cached event args for zero‑alloc hot paths
- Validation (
INotifyDataErrorInfo)- Thread‑safe store (per‑scope lists in
ConcurrentDictionary) - Copy‑on‑write updates + CAS; safe for UI enumeration
- Scopes: property‑level and entity‑level (entity is
nullor""across all APIs) - Helpers:
HasErrorsFor(scope),GetErrors(scope),GetErrorsSnapshot()
- Thread‑safe store (per‑scope lists in
- WPF command requery
[UseCommandManager]wires generated command toCommandManager.RequerySuggested
- Localization
[Localize]populates a static class from a JSON file (provided viaAdditionalFiles)
- Custom attributes
[AlsoNotify]to raise extraPropertyChanged[CustomAttribute]to apply an attribute to a generated member
Attributes (summary)
Minimal.Mvvm.NotifyAttribute: Generates a property (from a field or partial property) or a command property (from a method).
Options:PropertyName,CallbackName,PreferCallbackWithParameter,Getter,Setter.Minimal.Mvvm.AlsoNotifyAttribute: Notifies additional properties when the annotated property changes.Minimal.Mvvm.CustomAttributeAttribute: Specifies a fully qualified attribute name to be applied to a generated property.Minimal.Mvvm.UseCommandManagerAttribute: Enables automaticCanExecutereevaluation for the generated command property (WPF only).Minimal.Mvvm.NotifyDataErrorInfoAttribute: Generates validation infrastructure forINotifyDataErrorInfo.Minimal.Mvvm.LocalizeAttribute: Localizes the target class using the provided JSON file (MSBuildAdditionalFiles).
Install
dotnet add package NuExt.Minimal.Mvvm.SourceGenerator
# and one of:
dotnet add package NuExt.Minimal.Mvvm
# or
dotnet add package NuExt.Minimal.Mvvm.Wpf
# or
dotnet add package NuExt.Minimal.Mvvm.MahApps.Metro
Quick Start
using Minimal.Mvvm;
using System.Threading.Tasks;
public partial class PersonModel : BindableBase
{
[Notify, AlsoNotify(nameof(FullName))]
private string? _name;
[Notify, AlsoNotify(nameof(FullName))]
private string? _surname;
public string FullName => $"{_surname} {_name}";
// Generates IAsyncCommand<string?>? ShowInfoCommand
[Notify("ShowInfoCommand"), UseCommandManager]
[CustomAttribute("System.Text.Json.Serialization.JsonIgnore")]
private async Task ShowInfoAsync(string? text)
{
await Task.Delay(100);
}
}
What you get (conceptually):
Name/Surnameproperties withPropertyChangedandFullNamenotifications.IAsyncCommand<string?>? ShowInfoCommandproperty with WPF requery wiring (via[UseCommandManager]).- Cached PropertyChangedEventArgs etc. for zero‑alloc notifications.
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 withpropertyName, cancelling and disposing any previous task for that scope (lock‑free CAS under the hood).CancelValidationTask(string propertyName = null)
Cancels/disposes the tracked task forpropertyName; withnullcancels all tasks.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 to a command field or method, enables automatic CanExecute reevaluation for the generated command property by subscribing to the WPF CommandManager.RequerySuggested event. This attribute is used together with [Notify] for commands that should react to global UI state changes.
using Minimal.Mvvm;
public partial class MyViewModel : ViewModelBase
{
[Notify, UseCommandManager]
private IRelayCommand? _saveCommand;
}
Localization (AdditionalFiles)
Example csproj snippet for [Localize("local.en.json")]:
<ItemGroup>
<AdditionalFiles Include="Resources\local.en.json" />
</ItemGroup>
The generator will create a static class with string properties populated from that JSON. At runtime, localization can also be loaded from the specified file (see samples).
Ecosystem
- NuExt.Minimal.Mvvm
- NuExt.Minimal.Behaviors.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.