NuExt.Minimal.Mvvm.Sources 0.7.1

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package NuExt.Minimal.Mvvm.Sources --version 0.7.1
                    
NuGet\Install-Package NuExt.Minimal.Mvvm.Sources -Version 0.7.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NuExt.Minimal.Mvvm.Sources" Version="0.7.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NuExt.Minimal.Mvvm.Sources" Version="0.7.1" />
                    
Directory.Packages.props
<PackageReference Include="NuExt.Minimal.Mvvm.Sources" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NuExt.Minimal.Mvvm.Sources --version 0.7.1
                    
#r "nuget: NuExt.Minimal.Mvvm.Sources, 0.7.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NuExt.Minimal.Mvvm.Sources@0.7.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NuExt.Minimal.Mvvm.Sources&version=0.7.1
                    
Install as a Cake Addin
#tool nuget:?package=NuExt.Minimal.Mvvm.Sources&version=0.7.1
                    
Install as a Cake Tool

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.
  • 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 provider for registration and resolution of services.

Integrations & Companion Packages

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) Using ServiceProvider

public class MyService
{
    public string GetData() => "Hello from MyService!";
}

public class MyViewModel : ViewModelBase
{
    public IRelayCommand MyCommand { get; }

    public MyViewModel()
    {
        // Register services
        ServiceProvider.Default.RegisterService<MyService>();

        MyCommand = new RelayCommand(() =>
        {
            // Resolve and use services
            var myService = ServiceProvider.Default.GetService<MyService>();
            var data = myService.GetData();
            // Use the data
        });
    }
}

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...
    }
}

WPF + [UseCommandManager] example

In 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.

ViewModel

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.

XAML

<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>
Notes
  • [UseCommandManager] is WPF‑only; Avalonia/WinUI don’t have CommandManager.
  • If you still need a manual requery (rare), call CommandManager.InvalidateRequerySuggested().

ValueTask commands on legacy targets

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: if you consume .Sources, you can enable the commands explicitly on legacy:

<PropertyGroup>
  <DefineConstants>$(DefineConstants);NUEXT_ENABLE_VALUETASK</DefineConstants>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
</ItemGroup>

The same source files compile on modern automatically and on legacy only when NUEXT_ENABLE_VALUETASK is defined.

FAQ

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.

Installation

Via NuGet:

dotnet add package NuExt.Minimal.Mvvm

Or via Visual Studio:

  1. Go to Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution....
  2. Search for NuExt.Minimal.Mvvm.
  3. Click "Install".

Source Code Package

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:

  1. Go to Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution....
  2. Search for NuExt.Minimal.Mvvm.Sources.
  3. Click "Install".

Compatibility

  • .NET Standard 2.0+, .NET 8/9/10, .NET Framework 4.6.2+

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 .Sources opt‑in with NUEXT_ENABLE_VALUETASK and System.Threading.Tasks.Extensions).

Ecosystem

Contributing

Issues and PRs are welcome. Keep changes minimal and performance-conscious.

License

MIT. See LICENSE.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETFramework 4.6.2

    • No dependencies.
  • .NETStandard 2.0

    • No dependencies.
  • .NETStandard 2.1

    • No dependencies.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • 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.

Version Downloads Last Updated
0.7.3 87 3/16/2026
0.7.2 100 2/26/2026
0.7.1 103 2/23/2026
0.7.0 114 2/12/2026
0.6.0 154 1/11/2026
0.5.2 358 12/15/2025
0.5.1 304 12/14/2025
0.4.1 533 12/10/2025
0.4.0 260 12/5/2025
0.3.4 381 2/21/2025
0.3.3 351 1/26/2025
0.3.2 367 1/22/2025
0.3.1 343 1/19/2025
0.3.0 375 1/13/2025
0.2.0 697 11/14/2024
Loading failed