This package allows you to generate boilerplate code at compile time specifically for ViewModels using the Minimal MVVM Framework, thereby streamlining your development process and reducing repetitive tasks.
$ dotnet add package NuExt.Minimal.Mvvm.SourceGeneratorNuExt.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.
NuExt.Minimal.Mvvm (core) and optional UI integrations (…Wpf, …MahApps.Metro).INotifyDataErrorInfo)
ConcurrentDictionary)null or "" across all APIs)HasErrorsFor(scope), GetErrors(scope), GetErrorsSnapshot()[UseCommandManager] wires generated command to CommandManager.RequerySuggested[Localize] populates a static class from a JSON file (provided via AdditionalFiles)[AlsoNotify] to raise extra PropertyChanged[CustomAttribute] to apply an attribute to a generated memberMinimal.Mvvm.NotifyAttribute: Generates a property (from a field or partial property) or a command property (from a method).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 automatic CanExecute reevaluation for the generated command property (WPF only).Minimal.Mvvm.NotifyDataErrorInfoAttribute: Generates validation infrastructure for INotifyDataErrorInfo.Minimal.Mvvm.LocalizeAttribute: Localizes the target class using the provided JSON file (MSBuild AdditionalFiles).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
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/Surname properties with PropertyChanged and FullName notifications.IAsyncCommand<string?>? ShowInfoCommand property with WPF requery wiring (via [UseCommandManager]).null/"" = entity‑level; "PropertyName" = property‑level.ClearErrors(null) or ClearErrors("") → clears entity‑level only.ClearErrors(nameof(Property)) → clears that property only.ClearAllErrors() → full reset (raises ErrorsChanged per affected scope + updates HasErrors).SetError, SetErrors (merge), ReplaceErrors (replace), RemoveError.
ErrorsChanged).Threading: notifications are marshaled to UI via a lazily captured
SynchronizationContext.
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)propertyName, cancelling and disposing any previous task for that scope (lock‑free CAS under the hood).CancelValidationTask(string propertyName = null)propertyName; with null cancels all tasks.CancelAllValidationTasks()HasErrorsFor(scope) / SetError / SetErrors / ReplaceErrors / RemoveErrornull/"").Usage pattern (per property):
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);
}
}
CancelValidationTask(nameof(Property)).CancellationTokenSource tied to the VM lifetime.ContinueWith(OnlyOnFaulted|ExecuteSynchronously)) or catch inside the async validator.ConfigureAwait(false) inside validators; the generator marshals notifications to the UI via a lazily captured SynchronizationContext.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("")).Validation.HasError and delayed index access to (Validation.Errors)[0] (avoid warnings).ValidatesOnNotifyDataErrors=True, read its Validation.Errors).See the WpfAppSample in the repository for a minimal, production‑style pattern (entity banner + field messages).
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;
}
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).
Issues and PRs are welcome. Keep changes minimal and performance-conscious.
MIT. See LICENSE.