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.MvvmREADME
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.
Features
-
Core
Minimal.Mvvm.BindableBase— lightweightINotifyPropertyChangedbase.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: ifCanExecute(parameter)isfalse,Execute(parameter)does nothing. This guarantees consistent behavior for both UI‑bound and programmatic calls.
Semantics
AsyncCommandprovides cancellation and reentrancy control (AllowConcurrentExecution, default: false).AsyncValueCommand/AsyncValueCommand<T>areValueTask-based async commands.- Exceptions bubble to
UnhandledException(per‑command) first; if not handled, toAsyncCommand.GlobalUnhandledException. Cancel()signals the current operation viaCancellationToken; if nothing is executing, it’s a no‑op.
- All commands (
-
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 onValueTaskfor allocation‑sensitive scenarios.CompositeCommand— aggregates multiple commands and executes them sequentially; awaitsExecuteAsync(...)and callsExecute(...)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.Servicesis created lazily and disposed via your cleanup. - Application scope
ServiceProvider.Defaultacts as the application-level container. Prefer registering app-wide services there; ViewModels may register local overrides in their own scope.
Integrations & Companion Packages
- Building WPF? Use NuExt.Minimal.Mvvm.Wpf for document services, async dialogs, explicit view composition, and dispatcher‑safe APIs.
- MahApps.Metro integration: NuExt.Minimal.Mvvm.MahApps.Metro
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.Defaultat 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
nameoverloads to keep side‑by‑side implementations. - Concept mapping:
- App “Singleton” →
ServiceProvider.Default.RegisterService<T>(...) - Per‑View/ViewModel “Scoped” →
Services.RegisterService<T>(...) - “Transient” →
RegisterTransient<T>(...)
- App “Singleton” →
WPF + `[UseCom
Related Skills
node-connect
348.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
108.9kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
348.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
348.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
