gbastecki.BlazorMvvm
1.2.0
dotnet add package gbastecki.BlazorMvvm --version 1.2.0
NuGet\Install-Package gbastecki.BlazorMvvm -Version 1.2.0
<PackageReference Include="gbastecki.BlazorMvvm" Version="1.2.0" />
<PackageVersion Include="gbastecki.BlazorMvvm" Version="1.2.0" />
<PackageReference Include="gbastecki.BlazorMvvm" />
paket add gbastecki.BlazorMvvm --version 1.2.0
#r "nuget: gbastecki.BlazorMvvm, 1.2.0"
#:package gbastecki.BlazorMvvm@1.2.0
#addin nuget:?package=gbastecki.BlazorMvvm&version=1.2.0
#tool nuget:?package=gbastecki.BlazorMvvm&version=1.2.0
BlazorMvvm
A lightweight, AOT-compatible MVVM toolkit for Blazor with powerful source generation capabilities.
Table of Contents
- Package requirements
- Quick Start
- Core Features
- Key Highlights
- Command Attribute Quick Reference
- License
- Third-Party Libraries
- See it in action
Package requirements
This package uses Microsoft.AspNetCore.Components.Web package. The required version depends on your target .NET version:
| .NET Version | Required Package |
|---|---|
| .NET 10.0 | Microsoft.AspNetCore.Components.Web ≥ 10.0.0 |
| .NET 9.0 | Microsoft.AspNetCore.Components.Web ≥ 9.0.0 |
| .NET 8.0 | Microsoft.AspNetCore.Components.Web ≥ 8.0.0 |
| .NET 7.0 | Microsoft.AspNetCore.Components.Web ≥ 7.0.0 |
| .NET 6.0 | Microsoft.AspNetCore.Components.Web ≥ 6.0.0 |
Quick Start
1. Install the package
dotnet add package gbastecki.BlazorMvvm
2. Add package reference
<PackageReference Include="gbastecki.BlazorMvvm" Version="*" />
3. Register ViewModelFactory (optional, for ViewModel DI)
// Program.cs
builder.Services.UseBlazorMvvmViewModelFactory();
4. Create a ViewModel
[BlazorMvvmViewModel(ViewModelLifetime.Transient)]
public partial class CounterViewModel : BlazorViewModel
{
[BlazorObservableProperty]
private int _count;
[BlazorCommand]
private void Increment() => Count++;
}
5. Create a Component
@inherits BlazorMvvmComponentBase<CounterViewModel>
@page "/counter"
<h1>Count: @BaseViewModel.Count</h1>
<button @onclick="BaseViewModel.IncrementCommand.Execute">+1</button>
Core Features
Observable Properties
Convert private fields into observable properties with automatic change notification.
Source Generation (Recommended)
public partial class MyViewModel : BlazorViewModel
{
[BlazorObservableProperty]
private string _name;
[BlazorObservableProperty]
private int _counter;
[BlazorObservableProperty(Name = "CustomName")]
private string _internalField;
}
Generates:
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public int Counter
{
get => _counter;
set => Set(ref _counter, value);
}
public string CustomName
{
get => _internalField;
set => Set(ref _internalField, value);
}
Manual Implementation
public class MyViewModel : BlazorViewModel
{
private int _counter;
public int Counter
{
get => _counter;
set => Set(ref _counter, value); // Automatically notifies UI
// or use manual setter:
//set
//{
// if (_counter == value) return;
// _counter = value;
// OnPropertyChanged(nameof(Counter));
//}
}
}
Commands
Bind UI actions to ViewModel methods with built-in execution state management.
Synchronous Commands
[BlazorCommand]
private void Save() => SaveData();
// Generated: public IBlazorCommand SaveCommand { get; }
Async Commands
[BlazorCommand]
private async Task LoadDataAsync()
{
await Task.Delay(1000);
}
// Generated: public IBlazorAsyncCommand LoadDataAsyncCommand { get; }
Commands with Parameters
// Single parameter
[BlazorCommand]
private void UpdateName(string name) => Name = name;
// Generated: public IBlazorRelayCommand<string> UpdateNameCommand { get; }
// Multiple parameters (uses tuple)
[BlazorCommand]
private void Add(int a, int b) => Total = a + b;
// Generated: public IBlazorRelayCommand<(int, int)> AddCommand { get; }
// Usage: AddCommand.Execute((5, 10))
CanExecute Logic
[BlazorCommand(CanExecute = nameof(CanSave))]
private void Save() => SaveData();
private bool CanSave() => !string.IsNullOrEmpty(Name);
Concurrency Control
[BlazorCommand(AllowConcurrentExecutions = false)]
private async Task SubmitAsync()
{
// Prevents multiple concurrent executions
}
Auto-Refresh on IsExecuting Changed
Automatically trigger UI refresh when IsExecuting state changes
[BlazorCommand(autoRefreshOnIsExecutingChanged: true)]
private async Task LoadDataAsync()
{
await Task.Delay(5000);
}
<button @onclick="ViewModel.LoadDataAsyncCommand.Execute"
disabled="@ViewModel.LoadDataAsyncCommand.IsExecuting">
@if (ViewModel.LoadDataAsyncCommand.IsExecuting)
{
<span>Loading...</span>
}
else
{
<span>Loaded Data</span>
}
</button>
Custom Callback on IsExecuting Changed
[BlazorObservableProperty]
private bool _isLoading;
[BlazorCommand(OnIsExecutingChangedCallback = nameof(OnLoadingChanged))]
private async Task SaveAsync() => await SaveDataAsync();
private void OnLoadingChanged(bool isExecuting)
{
IsLoading = isExecuting;
// Additional logic here
}
Combined: Auto-Refresh + Custom Callback
[BlazorCommand(autoRefreshOnIsExecutingChanged: true, OnIsExecutingChangedCallback = nameof(OnSaving))]
private async Task SaveAsync() => await SaveDataAsync();
private void OnSaving(bool isExecuting)
{
// Custom logic runs AFTER OnPropertyChanged() is called
_logger.Log($"Saving state: {isExecuting}");
}
ObservableComponent
Optimize UI updates by isolating which parts of your component re-render.
Full Update Mode
Re-renders when ANY property on the ViewModel changes:
<ObservableComponent ViewModel="MyViewModel">
<p>Name: @MyViewModel.Name</p>
<p>Count: @MyViewModel.Count</p>
</ObservableComponent>
Selective Update Mode
Re-renders ONLY when specified properties change:
<ObservableComponent ViewModel="SharedVM"
PropertyNames="[nameof(SharedVM.Counter1)]">
<p>Counter1: @SharedVM.Counter1</p>
</ObservableComponent>
<ObservableComponent ViewModel="SharedVM"
PropertyNames="[nameof(SharedVM.Counter2), nameof(SharedVM.Counter3)]">
<p>Counter2: @SharedVM.Counter2</p>
<p>Counter3: @SharedVM.Counter3</p>
</ObservableComponent>
BlazorMessenger
Cross-ViewModel communication with weak reference support for automatic cleanup.
Messenger Setup
Choose between Dependency Injection or static singleton access:
// Option 1: Dependency Injection (Recommended)
// Program.cs
builder.Services.AddSingleton<IBlazorMessenger, BlazorMessenger>();
// ViewModel - Inject via constructor
public class MyViewModel : BlazorViewModel
{
private readonly IBlazorMessenger _messenger;
public MyViewModel(IBlazorMessenger messenger)
{
_messenger = messenger;
}
}
// Option 2: Static Singleton
// No registration needed - access directly
BlazorMessenger.Default.Send(new MyMessage());
Define Messages
// Simple value message
public class CounterChangedMessage : ValueChangedMessage<int>
{
public CounterChangedMessage(int value) : base(value) { }
}
// Request/Response message
public class GetUserRequest : RequestMessage<User> { }
Send Messages
// Via injected messenger
_messenger.Send(new CounterChangedMessage(42));
// Via static default instance
BlazorMessenger.Default.Send(new CounterChangedMessage(42));
Complete Receiver Pattern (Recommended)
Register in constructor, unregister in Dispose to prevent memory leaks:
[BlazorMessenger]
public partial class ReceiverViewModel : BlazorViewModel,
IBlazorRecipient<CounterChangedMessage>, IDisposable
{
private readonly IBlazorMessenger _messenger;
[BlazorObservableProperty]
private int _counter;
// Register in constructor - starts listening
public ReceiverViewModel(IBlazorMessenger messenger)
{
_messenger = messenger;
RegisterMessenger(_messenger); // Generated method
}
// Unregister in Dispose - stops listening, prevents leaks
public void Dispose()
{
UnregisterMessenger(_messenger); // Generated method
}
public void Receive(CounterChangedMessage message)
{
Counter = message.Value;
}
}
Manual Registration (Without Source Generation)
public class ReceiverViewModel : BlazorViewModel,
IBlazorRecipient<CounterChangedMessage>, IDisposable
{
private readonly IBlazorMessenger _messenger;
public ReceiverViewModel(IBlazorMessenger messenger)
{
_messenger = messenger;
_messenger.Register<CounterChangedMessage>(this);
}
public void Dispose()
{
_messenger.Unregister<CounterChangedMessage>(this);
}
public void Receive(CounterChangedMessage message) { }
}
Strong vs Weak Reference Messenger
| Feature | BlazorMessenger |
BlazorStrongMessenger |
|---|---|---|
| Reference Type | Weak | Strong |
| GC Behavior | Recipients auto-collected | Recipients kept alive |
| Memory Leaks | Forgiving if you forget Dispose | Must unregister manually |
| Use Case | Most scenarios (default) | Guaranteed delivery |
// Use weak messenger (default) - safer for most cases
builder.Services.AddSingleton<IBlazorMessenger, BlazorMessenger>();
// Use strong messenger - guaranteed delivery, explicit lifecycle
builder.Services.AddSingleton<IBlazorMessenger, BlazorStrongMessenger>();
Channel Tokens
Isolate communication using channel tokens (any IEquatable<T> type):
String Channels
// Register on specific channel
messenger.Register<MyMessage, string>(this, "channel-a",
(r, m) => ((MyReceiver)r).OnMessage(m));
// Send to that channel only
messenger.Send(new MyMessage(), "channel-a");
// Unregister from channel
messenger.Unregister<MyMessage, string>(this, "channel-a");
Enum Channels (via int)
// Note: Enums don't implement IEquatable<T>, so cast to int
public enum NotificationChannel { System = 0, User = 1, Debug = 2 }
// Register on channel
messenger.Register<LogMessage, int>(this, (int)NotificationChannel.Debug,
(r, m) => ((LogReceiver)r).OnLog(m));
// Send to channel
messenger.Send(new LogMessage("msg"), (int)NotificationChannel.Debug);
Request/Response Pattern
// Register handler
messenger.Register<GetUserRequest>(this, (r, m) =>
{
m.Reply(new User("John"));
});
// Send and get response
User user = messenger.Send(new GetUserRequest());
ViewModelFactory
Automatic dependency injection for ViewModels with configurable lifetimes.
Setup
// Program.cs
builder.Services.UseBlazorMvvmViewModelFactory();
Register ViewModels
[BlazorMvvmViewModel(ViewModelLifetime.Singleton)]
public partial class AppViewModel : BlazorViewModel { }
[BlazorMvvmViewModel(ViewModelLifetime.Scoped)]
public partial class PageViewModel : BlazorViewModel { }
[BlazorMvvmViewModel(ViewModelLifetime.Transient)]
public partial class DialogViewModel : BlazorViewModel { }
Constructor Injection
[BlazorMvvmViewModel(ViewModelLifetime.Scoped)]
public partial class HomeViewModel : BlazorViewModel
{
private readonly IApiService _api;
private readonly ILogger _logger;
public HomeViewModel(IApiService api, ILogger logger)
{
_api = api;
_logger = logger;
}
}
Constructor Selection
[BlazorMvvmViewModel(ViewModelLifetime.Scoped)]
public partial class MyViewModel : BlazorViewModel
{
public MyViewModel() { }
[BlazorMvvmViewModelFactoryConstructor] // Use this constructor
public MyViewModel(IService service) { }
}
Component Usage
public partial class Home : BlazorMvvmComponentBase<HomeViewModel>
{
// ViewModel is automatically resolved and injected via BaseViewModel
// No need for manual SetDataContext()
protected override void OnInitialized()
{
base.OnInitialized();
// BaseViewModel is ready to use!
}
}
Key Highlights
| Feature | Benefit |
|---|---|
| Source Generation | Zero runtime reflection, AOT-ready |
| Trimming Safe | Works with .NET trimmer |
| Weak References | Automatic memory cleanup in messenger |
| Selective Updates | Fine-grained UI re-rendering |
| Auto-Refresh | No boilerplate for loading states |
| Multiple Parameters | Tuple support for complex commands |
| Channel Tokens | Isolated message channels |
| Request/Response | Synchronous messaging pattern |
Command Attribute Quick Reference
[BlazorCommand] // Basic command
[BlazorCommand(CanExecute = nameof(CanExecute))] // With validation
[BlazorCommand(AllowConcurrentExecutions = true)] // Allow parallel execution
[BlazorCommand(autoRefreshOnIsExecutingChanged: true)] // Auto UI refresh
[BlazorCommand(OnIsExecutingChangedCallback = nameof(Callback))] // Custom callback
[BlazorCommand(autoRefreshOnIsExecutingChanged: true, OnIsExecutingChangedCallback = nameof(Callback))] // Both combined
License
This project is licensed under the MIT License - see the LICENSE file for details.
Third-Party Libraries
See the NOTICE file for attribution information.
See it in action
BarcodeTool | Demo — A production app built with BlazorMvvm, featuring barcode generation and scanning.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.0)
-
net6.0
- Microsoft.AspNetCore.Components.Web (>= 6.0.0)
-
net7.0
- Microsoft.AspNetCore.Components.Web (>= 7.0.0)
-
net8.0
- Microsoft.AspNetCore.Components.Web (>= 8.0.0)
-
net9.0
- Microsoft.AspNetCore.Components.Web (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.