SkillAgentSearch skills...

NuExt.Minimal.Mvvm

High‑performance, dependency‑free MVVM core for .NET — deterministic commands, robust async lifecycle, and a clear minimal API with a lightweight service provider.

Install / Use

/learn @nu-ext/NuExt.Minimal.Mvvm

README

NuExt.Minimal.Mvvm

NuExt.Minimal.Mvvm is a high‑performance, dependency‑free MVVM core for .NET focused on robust async flows and deterministic command execution. It provides a minimal, clear API with Bindable/ViewModel base types, a self‑validating command model (Relay/Async/Composite), and a lightweight service provider.

NuGet Build License Downloads

Features

  • Core

    • Minimal.Mvvm.BindableBase — lightweight INotifyPropertyChanged base.
    • Minimal.Mvvm.ViewModelBase — lean ViewModel foundation with simple service access.
    • Minimal.Mvvm.WeakEvent — lightweight weak‑event storage for (object sender, TEventArgs) handlers.
  • Command model (self‑validating)

    • All commands (RelayCommand, RelayCommand<T>, AsyncCommand, AsyncCommand<T>, AsyncValueCommand, AsyncValueCommand<T>, CompositeCommand) validate their state internally: if CanExecute(parameter) is false, Execute(parameter) does nothing. This guarantees consistent behavior for both UI‑bound and programmatic calls.

    Semantics

    • AsyncCommand provides cancellation and reentrancy control (AllowConcurrentExecution, default: false).
    • AsyncValueCommand / AsyncValueCommand<T> are ValueTask-based async commands.
    • Exceptions bubble to UnhandledException (per‑command) first; if not handled, to AsyncCommand.GlobalUnhandledException.
    • Cancel() signals the current operation via CancellationToken; if nothing is executing, it’s a no‑op.
  • Command implementations

    • RelayCommand / RelayCommand<T> — classic synchronous delegate‑based commands (can be invoked concurrently from multiple threads).
    • AsyncCommand / AsyncCommand<T>asynchronous commands with predictable error propagation and cancellation.
    • AsyncValueCommand / AsyncValueCommand<T>asynchronous commands built on ValueTask for allocation‑sensitive scenarios.
    • CompositeCommand — aggregates multiple commands and executes them sequentially; awaits ExecuteAsync(...) and calls Execute(...) for non‑async commands.
  • Service Provider Integration

    • Minimal.Mvvm.ServiceProvider: lightweight service registration/resolution in UI scenarios.
    • Hierarchical lookup order for ViewModels: local → fallback → parent → default (no hidden magic, no reflection).
    • Lifetime basics for UI apps:
      • Singleton (lazy) — stored per container, created on first resolution.
      • Transient — new instance per resolution, not cached.
      • Scope — a ViewModel acts as a scope; ViewModelBase.Services is created lazily and disposed via your cleanup.
      • Application scope
        • ServiceProvider.Default acts as the application-level container. Prefer registering app-wide services there; ViewModels may register local overrides in their own scope.

Integrations & Companion Packages

Recommended Companion

Use the NuExt.Minimal.Mvvm.SourceGenerator for compile‑time boilerplate generation in ViewModels.

Quick examples

1) Advanced AsyncCommand with concurrency and cancellation

public class SearchViewModel : ViewModelBase
{
    public IAsyncCommand<string> SearchCommand { get; }
    public ICommand CancelCommand { get; }

    public SearchViewModel()
    {
        SearchCommand = new AsyncCommand<string>(SearchAsync, CanSearch)
        {
            AllowConcurrentExecution = true
        };

        CancelCommand = new RelayCommand(() => SearchCommand.Cancel());
    }

    private async Task SearchAsync(string query, CancellationToken cancellationToken)
    {
        await Task.Delay(1000, cancellationToken);
        Results = $"Results for: {query}";
    }

    private bool CanSearch(string query) => !string.IsNullOrWhiteSpace(query);

    private string _results = string.Empty;
    public string Results
    {
        get => _results;
        private set => SetProperty(ref _results, value);
    }
}

2) Two‑tier exception handling

public class DataViewModel : ViewModelBase
{
    public IAsyncCommand LoadDataCommand { get; }

    public DataViewModel()
    {
        LoadDataCommand = new AsyncCommand(LoadDataAsync);

        LoadDataCommand.UnhandledException += (sender, e) =>
        {
            if (e.Exception is HttpRequestException httpEx)
            {
                ShowError($"Network error: {httpEx.Message}");
                e.Handled = true; // local tier handled
            }
        };
    }

    private async Task LoadDataAsync(CancellationToken cancellationToken)
    {
        throw new HttpRequestException("Connection failed");
    }

    private void ShowError(string message) { /* UI */ }
}

Global fallback: subscribe to AsyncCommand.GlobalUnhandledException once at app startup (composition root) for logging/telemetry.

AsyncCommand.GlobalUnhandledException += (sender, e) =>
{
    Logger.LogError(e.Exception, "Global command error");
    e.Handled = true;
};

3) Using Source Generator

To further simplify your ViewModel development, consider using the source generator provided by the NuExt.Minimal.Mvvm.SourceGenerator package. Here's an example:

using Minimal.Mvvm;

public partial class ProductViewModel : ViewModelBase
{
    [Notify]
    private string _name = string.Empty;

    [Notify(Setter = AccessModifier.Private)]
    private decimal _price;

    public ProductViewModel()
    {
        SaveCommand = new AsyncCommand(SaveAsync);
    }

    [Notify]
    private async Task SaveAsync(CancellationToken token)
    {
        await Task.Delay(500, token);
        Price = 99.99m;
    }
}

4) Service registration for ViewModels

public sealed class MyViewModel : ViewModelBase
{
    public MyViewModel(IServiceContainer fallback) : base(fallback)
    {
        // Application scope (global): ServiceProvider.Default for app-level services.
        ServiceProvider.Default.RegisterService<IMyService, MyService>();
        // ViewModel scope (local container); override per-VM if needed.
        Services.RegisterService<IMyService, MyVmSpecificService>();  // singleton
        Services.RegisterTransient<INotification>(() => new Toast()); // transient
    }

    public void Use()
    {
        // Resolution order: local → fallback → parent → default
        var svc = Services.GetService<IMyService>();
        var toast = Services.GetService<INotification>();
    }
}

Notes (UI lifetimes):

  • Scope = the ViewModel instance (local container). Local services live as long as the VM does.
  • Application scope = ServiceProvider.Default.
  • Singleton (lazy) = created on first successful resolution and cached in the owning container.
  • Transient = new instance per resolution (not cached).
  • Cleanup: call Services.CleanupAsync(...) during uninitialization if you need deterministic disposal of local singletons.

5) AsyncValueCommand for allocation‑sensitive hot paths

public sealed class ValidateViewModel : ViewModelBase
{
    public IAsyncCommand ValidateCommand { get; }

    public ValidateViewModel()
    {
        // Often completes synchronously; ValueTask avoids allocations in that case.
        ValidateCommand = new AsyncValueCommand(ValidateAsync);
    }

    private ValueTask ValidateAsync(CancellationToken ct)
    {
        // synchronous fast path
        if (IsValidFast()) return ValueTask.CompletedTask;

        // fallback async path
        return SlowValidateAsync(ct);
    }

    private bool IsValidFast() => /* lightweight checks */;
    private async ValueTask SlowValidateAsync(CancellationToken ct)
    {
        await Task.Delay(50, ct);
        // heavy checks...
    }
}

UI‑focused DI: how it compares (at a glance)

  • Minimal, explicit registrations (parameterless ctor or factory).
  • Hierarchical resolution that matches ViewModel trees.
  • No hidden conventions, no auto‑discovery; behavior is deterministic.
  • Easy to migrate from other DI tools for UI: register what you need locally per ViewModel, use a fallback container for app‑level services or unit testing, and rely on parent resolution for context‑specific overrides.

Migrating from other DI containers (UI context)

  • Application services: register into ServiceProvider.Default at startup.
  • Per‑VM overrides: register in ViewModelBase.Services (scope = the ViewModel).
  • External DI as parent: wrap your existing container as a parent for this provider: var app = new ServiceProvider((System.IServiceProvider)existing); Then either set it as application scope (ServiceProvider.Default = app) or pass as a ViewModel fallback (: base(app)).
  • Named registrations: use name overloads to keep side‑by‑side implementations.
  • Concept mapping:
    • App “Singleton” → ServiceProvider.Default.RegisterService<T>(...)
    • Per‑View/ViewModel “Scoped” → Services.RegisterService<T>(...)
    • “Transient” → RegisterTransient<T>(...)

WPF + `[UseCom

Related Skills

View on GitHub
GitHub Stars5
CategoryDevelopment
Updated20d ago
Forks0

Languages

C#

Security Score

90/100

Audited on Mar 16, 2026

No findings