![]() |
VOOZH | about |
dotnet add package NuExt.Minimal.Mvvm --version 0.7.3
NuGet\Install-Package NuExt.Minimal.Mvvm -Version 0.7.3
<PackageReference Include="NuExt.Minimal.Mvvm" Version="0.7.3" />
<PackageVersion Include="NuExt.Minimal.Mvvm" Version="0.7.3" />Directory.Packages.props
<PackageReference Include="NuExt.Minimal.Mvvm" />Project file
paket add NuExt.Minimal.Mvvm --version 0.7.3
#r "nuget: NuExt.Minimal.Mvvm, 0.7.3"
#:package NuExt.Minimal.Mvvm@0.7.3
#addin nuget:?package=NuExt.Minimal.Mvvm&version=0.7.3Install as a Cake Addin
#tool nuget:?package=NuExt.Minimal.Mvvm&version=0.7.3Install as a Cake Tool
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
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)
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.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.ViewModelBase.Services is created lazily and disposed via your cleanup.ServiceProvider.Default acts as the application-level container. Prefer registering app-wide services there; ViewModels may register local overrides in their own scope.Use the NuExt.Minimal.Mvvm.SourceGenerator for compile‑time boilerplate generation in ViewModels.
AsyncCommand with concurrency and cancellationpublic 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);
}
}
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;
};
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;
}
}
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.
AsyncValueCommand for allocation‑sensitive hot pathspublic 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...
}
}
ServiceProvider.Default at startup.ViewModelBase.Services (scope = the ViewModel).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)).name overloads to keep side‑by‑side implementations.ServiceProvider.Default.RegisterService<T>(...)Services.RegisterService<T>(...)RegisterTransient<T>(...)[UseCommandManager] exampleIn WPF, [UseCommandManager] wires a generated command property to CommandManager.RequerySuggested, so CanExecute is reevaluated automatically on typical UI events (focus changes, keyboard, window activation). You don’t need to call RaiseCanExecuteChanged() manually.
using Minimal.Mvvm;
using System.Threading;
using System.Threading.Tasks;
public partial class LoginViewModel : ViewModelBase
{
[Notify] private string _userName = string.Empty;
[Notify] private string _password = string.Empty;
// Field-based pattern: generator creates the LoginCommand property.
// [UseCommandManager] auto-subscribes the property setter to WPF CommandManager.RequerySuggested.
[Notify, UseCommandManager]
private IAsyncCommand? _loginCommand;
public LoginViewModel()
{
LoginCommand = new AsyncCommand(LoginAsync, CanLogin);
}
private bool CanLogin() =>
!string.IsNullOrWhiteSpace(UserName) &&
!string.IsNullOrWhiteSpace(Password);
private async Task LoginAsync(CancellationToken ct)
{
await Task.Delay(250, ct);
// sign-in...
}
}
Alternatively, you can place
[Notify, UseCommandManager]on a method that should become a command; the generator will create the command property and wire WPF requery in the property setter as well.
<Window x:Class="MyApp.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
Title="Login" Width="360" Height="220">
<Window.DataContext>
<vm:LoginViewModel/>
</Window.DataContext>
<StackPanel Margin="16" VerticalAlignment="Center">
<TextBox Margin="0,0,0,8"
Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Margin="0,0,0,12"
Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="Sign in"
Command="{Binding LoginCommand}"
HorizontalAlignment="Right"
MinWidth="96" Padding="12,6"/>
</StackPanel>
</Window>
[UseCommandManager] is WPF‑only; Avalonia/WinUI don’t have CommandManager.CommandManager.InvalidateRequerySuggested().You have two options to enable AsyncValueCommand* on legacy TFMs:
Recommended: install the binary add‑on
dotnet add package NuExt.Minimal.Mvvm.Legacy
This adds AsyncValueCommand* for net462/netstandard2.0 and pulls System.Threading.Tasks.Extensions only on legacy.
Sources‑only workflow: opt‑in via .Sources and NUEXT_ENABLE_VALUETASK + System.Threading.Tasks.Extensions.
Q: How is this different from CommunityToolkit.Mvvm?
A: NuExt.Minimal.Mvvm focuses on a smaller, deterministic core with strict command semantics (no‑op Execute when CanExecute is false), explicit async error pipeline (local → global), and no external dependencies. CommunityToolkit.Mvvm provides a broader toolbox (messaging, DI helpers, attributes, etc.). If you need a minimal, performance‑oriented core with predictable async/command behavior, NuExt.Minimal.Mvvm is a good fit; if you want a feature‑rich toolkit, CommunityToolkit.Mvvm may be preferable.
Q: Do I have to wire WPF CommandManager.RequerySuggested myself?
A: No. With the source generator, mark the command with [UseCommandManager], and the generated property will auto‑subscribe/unsubscribe for requery.
Q: What is the command error flow?
A: Exceptions raised during AsyncCommand execution are first published to UnhandledException; if not handled there, they flow to AsyncCommand.GlobalUnhandledException.
Q: How do I integrate my existing DI container?
A: Wrap it as a parent via new ServiceProvider(System.IServiceProvider parent).
Use it in one of two ways:
ServiceProvider.Default = new ServiceProvider(parent), so all ViewModels can resolve app‑wide services via the default provider.: base(fallback)) when you need a VM to resolve “up” into the external container while still allowing local overrides in that VM scope.Q: Where are scoped services?
A: A ViewModel is the scope. Register app‑wide singletons in ServiceProvider.Default (application scope).
Register per‑VM overrides in ViewModelBase.Services (the VM’s local scope). Transients are created per resolution.
Q: What is the “application scope”?
A: ServiceProvider.Default. Register app-wide singletons there; ViewModels resolve with the order local → fallback → parent → default.
Via NuGet:
dotnet add package NuExt.Minimal.Mvvm
Or via Visual Studio:
Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution....NuExt.Minimal.Mvvm.A source package is also available: NuExt.Minimal.Mvvm.Sources. This package allows you to embed the entire framework directly into your application, enabling easier source code exploring and debugging.
See ValueTask commands on legacy targets for legacy opt‑in details.
To install the source code package, use the following command:
dotnet add package NuExt.Minimal.Mvvm.Sources
Or via Visual Studio:
Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution....NuExt.Minimal.Mvvm.Sources.Legacy only: to use
AsyncValueCommand*on .NET Framework 4.6.2 / .NET Standard 2.0, see ValueTask commands on legacy targets (binary add‑on or.Sourcesopt‑in withNUEXT_ENABLE_VALUETASKandSystem.Threading.Tasks.Extensions).
Issues and PRs are welcome. Keep changes minimal and performance-conscious.
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 netcoreapp2.0 was computed. netcoreapp2.1 netcoreapp2.1 was computed. netcoreapp2.2 netcoreapp2.2 was computed. netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 netstandard2.0 is compatible. netstandard2.1 netstandard2.1 is compatible. |
| .NET Framework | net461 net461 was computed. net462 net462 is compatible. net463 net463 was computed. net47 net47 was computed. net471 net471 was computed. net472 net472 was computed. net48 net48 was computed. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen40 tizen40 was computed. tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
Showing the top 2 NuGet packages that depend on NuExt.Minimal.Mvvm:
| Package | Downloads |
|---|---|
|
NuExt.Minimal.Mvvm.Wpf
NuExt.Minimal.Mvvm.Wpf is an extension for the lightweight MVVM framework NuExt.Minimal.Mvvm, specifically designed for WPF applications. Commonly Used Types: Minimal.Mvvm.ModelBase Minimal.Mvvm.Wpf.ControlViewModel Minimal.Mvvm.Wpf.DocumentContentViewModelBase Minimal.Mvvm.Wpf.WindowViewModel Minimal.Mvvm.Wpf.IAsyncDialogService Minimal.Mvvm.Wpf.IAsyncDocument Minimal.Mvvm.Wpf.IAsyncDocumentContent Minimal.Mvvm.Wpf.IAsyncDocumentManagerService Minimal.Mvvm.Wpf.InputDialogService Minimal.Mvvm.Wpf.OpenWindowsService Minimal.Mvvm.Wpf.SettingsService Minimal.Mvvm.Wpf.TabbedDocumentService Minimal.Mvvm.Wpf.ViewLocator Minimal.Mvvm.Wpf.WindowedDocumentService Minimal.Mvvm.Wpf.WindowPlacementService |
|
|
NuExt.Minimal.Mvvm.Legacy
Legacy-only add-on for NuExt.Minimal.Mvvm (net462/netstandard2.0). Provides backports (e.g., AsyncValueCommand). Not deprecated; NOT for modern .NET. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.7.3 | 294 | 3/16/2026 |
| 0.7.2 | 156 | 2/26/2026 |
| 0.7.1 | 151 | 2/23/2026 |
| 0.7.0 | 147 | 2/12/2026 |
| 0.6.0 | 144 | 1/11/2026 |
| 0.5.2 | 331 | 12/15/2025 |
| 0.5.1 | 253 | 12/14/2025 |
| 0.4.1 | 481 | 12/10/2025 |
| 0.4.0 | 201 | 12/5/2025 |
| 0.3.4 | 240 | 2/21/2025 |
| 0.3.3 | 361 | 1/26/2025 |
| 0.3.2 | 327 | 1/22/2025 |
| 0.3.1 | 333 | 1/19/2025 |
| 0.3.0 | 333 | 1/13/2025 |
| 0.2.0 | 275 | 11/14/2024 |