gbastecki.BlazorMvvm
1.1.0
See the version list below for details.
dotnet add package gbastecki.BlazorMvvm --version 1.1.0
NuGet\Install-Package gbastecki.BlazorMvvm -Version 1.1.0
<PackageReference Include="gbastecki.BlazorMvvm" Version="1.1.0" />
<PackageVersion Include="gbastecki.BlazorMvvm" Version="1.1.0" />
<PackageReference Include="gbastecki.BlazorMvvm" />
paket add gbastecki.BlazorMvvm --version 1.1.0
#r "nuget: gbastecki.BlazorMvvm, 1.1.0"
#:package gbastecki.BlazorMvvm@1.1.0
#addin nuget:?package=gbastecki.BlazorMvvm&version=1.1.0
#tool nuget:?package=gbastecki.BlazorMvvm&version=1.1.0
BlazorMvvm
Use the MVVM pattern for Blazor with a simple and lightweight library.
Quick start
This library is distributed via NuGet.
Check Live Demo.
Usage
BlazorMvvm provides a lightweight set of base classes and components to implement the Model-View-ViewModel (MVVM) pattern in Blazor applications.
This guide outlines the core components and their usage.
ViewModel
Viewmodels encapsulate the application's presentation logic and state. In BlazorMvvm, your viewmodels must inherit from the BlazorViewModel base class.
This base class implements IBlazorViewModel, which is essential for notifying the UI when a property's value has changed.
Example: HomeViewModel.cs
using BlazorMvvm;
namespace YourNamespace;
public class HomeViewModel : BlazorViewModel
{
// --- Option 1: Manual Property Notification ---
private int _counter;
public int Counter
{
get => _counter;
set
{
// Manual equality check
if (_counter == value) return;
_counter = value;
// Manually raise the OnPropertyChanged event
// This will trigger the UI to refresh
base.OnPropertyChanged();
}
}
// --- Option 2: Using the Set<T> Helper ---
private int _counter2;
public int Counter2
{
get => _counter2;
// The Set() helper method simplifies this pattern:
// 1. It performs an equality check.
// 2. If the value is new, it updates the backing field.
// 3. It raises the OnPropertyChanged event.
set => Set(ref _counter2, value);
}
// --- Command example ---
private void IncrementCounter()
{
Counter++;
}
private IBlazorCommand _incrementCounterCommand;
public IBlazorCommand IncrementCounterCommand => _incrementCounterCommand ??= new BlazorCommand(IncrementCounter);
}
Example: HomeViewModel.cs with Source Generation
using BlazorMvvm;
namespace YourNamespace;
public partial class HomeViewModel : BlazorViewModel //class must be partial
{
[BlazorObservableProperty]
private int _counter;
// Generates:
// public int Counter
// {
// get => _counter;
// set => Set(ref _counter, value);
// }
[BlazorObservableProperty]
private int _counter2;
// Generates:
// public int Counter2
// {
// get => _counter2;
// set => Set(ref _counter2, value);
// }
[BlazorCommand]
private void IncrementCounter()
{
Counter++;
}
// Generates:
// private BlazorMvvm.IBlazorCommand _incrementCounterCommand;
// public BlazorMvvm.IBlazorCommand IncrementCounterCommand => _incrementCounterCommand ??= new BlazorMvvm.BlazorCommand(IncrementCounter);
}
ComponentBase
To bind a view (Blazor Component) to a viewmodel, your components should inherit from BlazorMvvmComponentBase<T>, where T is the type of your viewmodel.
The final step is to connect your viewmodel instance to the component by calling SetDataContext() in the component's OnInitialized lifecycle method. This subscribes the component to the viewmodel's PropertyChanged events, automatically triggering StateHasChanged() to re-render the component when a property is updated.
Example: Home.razor
@using BlazorMvvm
@inherits BlazorMvvmComponentBase<HomeViewModel>
Example: Home.razor.cs
using BlazorMvvm;
namespace YourNamespace;
public partial class Home : BlazorMvvmComponentBase<HomeViewModel>
{
// Instantiate the ViewModel
HomeViewModel ViewModel = new();
protected override void OnInitialized()
{
// Set the DataContext to link the View and ViewModel.
// This is the essential step for enabling data binding.
SetDataContext(ViewModel);
base.OnInitialized();
}
}
ObservableComponent
By default, when a viewmodel property changes, the entire component bound via SetDataContext is re-rendered. For complex UIs, this can be inefficient.
The ObservableComponent allows you to define fine-grained "observable" fragments within your component. These fragments can be bound to a viewmodel and will only re-render when specific properties change, isolating the UI update.
Usage
Full Update: Pass a
ViewModelinstance. TheObservableComponent's child content will re-render for any property change on that viewmodel.Selective Update: Pass a
ViewModeland aPropertyNamesarray. The child content will only re-render when one of the specified properties raises itsOnPropertyChangedevent.
Example: Home.razor
@using BlazorMvvm
@inherits BlazorMvvmComponentBase<HomeViewModel>
<ObservableComponent ViewModel="ObservablePartViewModel">
<div>ObservableComponent current counter: @ObservablePartViewModel.Counter</div>
</ObservableComponent>
<ObservableComponent ViewModel="SharedObservableViewModel" PropertyNames="[nameof(SharedObservableViewModel.Counter1)]">
<div>SharedObservableViewModel current counter 1: @SharedObservableViewModel.Counter1</div>
</ObservableComponent>
<ObservableComponent ViewModel="SharedObservableViewModel" PropertyNames="[nameof(SharedObservableViewModel.Counter2), nameof(SharedObservableViewModel.Counter3)]">
<div>SharedObservableViewModel current counter 2: @SharedObservableViewModel.Counter2</div>
<div>SharedObservableViewModel current counter 3: @SharedObservableViewModel.Counter3</div>
</ObservableComponent>
Example: Home.razor.cs
using BlazorMvvm;
using Microsoft.AspNetCore.Components;
namespace YourNamespace;
public class ObservablePartViewModel : BlazorViewModel { /* ... */ }
public class SharedObservableViewModel : BlazorViewModel { /* ... */ }
public partial class Home : BlazorMvvmComponentBase<HomeViewModel>
{
// Main ViewModel for the component
HomeViewModel ViewModel = new();
// ViewModels for the observable fragments
ObservablePartViewModel ObservablePartViewModel = new();
SharedObservableViewModel SharedObservableViewModel = new();
protected override void OnInitialized()
{
// The main viewmodel is set as the primary DataContext
SetDataContext(ViewModel);
base.OnInitialized();
}
}
Commands
BlazorMvvm provides Command implementations that allow you to bind UI actions (like @onclick) to methods on your viewmodel, while also managing execution state (e.g., disabling a button while an async task is running).
Available Implementations
Parameterless:
BlazorCommand(Action execute, Func<bool>? canExecute = null)BlazorAsyncCommand(Func<Task> execute, Func<Task<bool>>? canExecute = null, bool allowConcurrentExecutions = false)
Generic (Type-Safe Parameter):
BlazorRelayCommand<T>(Action<T> execute, Func<T, bool>? canExecute = null)BlazorAsyncRelayCommand<T>(Func<T, Task> execute, Func<T, Task<bool>>? canExecute = null, bool allowConcurrentExecutions = false)
Object-based Parameter:
BlazorRelayCommand(Action<object[]?> execute, Func<object[]?, bool>? canExecute = null)BlazorAsyncRelayCommand(Func<object[]?, Task> execute, Func<object[]?, Task<bool>>? canExecute = null, bool allowConcurrentExecutions = false)
Key Features
canExecute: An optional delegate that determines if the command is allowed to run.IsExecuting(Async only): Aboolproperty that istruewhile theexecutetask is running.allowConcurrentExecutions(Async only): Iffalse(the default), prevents the command from executing if itIsExecuting.OnIsExecutingChanged(Async only): An event raised whenIsExecutingchanges. You must subscribe to this and callOnPropertyChanged()to notify the UI to update.
Example: ButtonExampleViewModel.cs
This example demonstrates an async command that disables a button for 5 seconds. It implements IDisposable to safely unsubscribe from the event handler.
using BlazorMvvm;
using System;
using System.Threading.Tasks;
namespace YourNamespace;
public class ButtonExampleViewModel : BlazorViewModel, IDisposable
{
public IBlazorAsyncCommand DisableButtonCommand { get; }
public ButtonExampleViewModel()
{
// Initialize the command, passing the method to execute
DisableButtonCommand = new BlazorAsyncCommand(DisableButton);
// Subscribe to the event to update the UI
DisableButtonCommand.OnIsExecutingChanged += DisableButtonCommand_OnIsExecutingChanged;
}
private async Task DisableButton()
{
// Simulate a long-running operation
await Task.Delay(5000);
}
private void DisableButtonCommand_OnIsExecutingChanged(bool isExecuting)
{
// Notify the UI that the command's state has changed
// This allows the button's 'disabled' attribute to update
base.OnPropertyChanged(nameof(DisableButtonCommand));
}
// Implement IDisposable to clean up event subscriptions
public void Dispose()
{
DisableButtonCommand.OnIsExecutingChanged -= DisableButtonCommand_OnIsExecutingChanged;
}
}
Example: .razor Component
This component hosts the ButtonExampleViewModel inside an ObservableComponent to ensure the button state updates correctly.
@using BlazorMvvm
@inherits BlazorMvvmComponentBase<HomeViewModel>
<ObservableComponent ViewModel="ButtonExampleViewModel">
<button
@onclick="ButtonExampleViewModel.DisableButtonCommand.Execute"
disabled="@ButtonExampleViewModel.DisableButtonCommand.IsExecuting">
Disable button for 5 seconds
</button>
</ObservableComponent>
@code {
ButtonExampleViewModel ButtonExampleViewModel = new();
protected override void OnDispose()
{
ButtonExampleViewModel.Dispose();
base.OnDispose();
}
}
ViewModelFactory
The BlazorMvvmViewModelFactory is responsible for resolving and providing instances of ViewModels with support for different lifetimes.
Features
- Automatic Dependency Injection: ViewModels can be automatically registered and injected.
- Lazy Loading support: Works seamlessly with lazy-loaded assemblies.
- AOT & Trimming compatible: Uses Source Generators and Module Initializers to avoid runtime reflection, making it fully compatible with AOT and Trimming.
- Flexible lifetimes: Support for
Transient,Scoped, andSingletonViewModels. - Constructor Selection: Ability to specify which constructor to use when multiple constructors are present.
- Service Injection: Supports constructor injection for services registered in the DI container.
- Integration with BlazorMvvmComponentBase: Automatically resolves and injects ViewModels into components.
Setup
To use automatic dependency injection for registered ViewModels, in your main Program.cs, add the following line to register the BlazorMvvm ViewModelFactory:
builder.Services.UseBlazorMvvmViewModelFactory();
Registering ViewModels
To register a ViewModel with the BlazorMvvmViewModelFactory, decorate the ViewModel class with the [BlazorMvvmViewModel] attribute, specifying the desired lifetime.
[BlazorMvvmViewModel(ViewModelLifetime.Singleton)]
public class HomeViewModel : BlazorViewModel { ... }
ViewModelFactory parameters injection
You can also register services that your ViewModels depend on in the DI container as usual, to make them available for constructor injection.
builder.Services.AddSingleton<IService, ServiceImplementation>();
Then, you can declare constructor parameters in your ViewModel, and they will be automatically injected when the ViewModel is resolved.
[BlazorMvvmViewModel(ViewModelLifetime.Singleton)]
public class HomeViewModel : BlazorViewModel
{
private readonly IService _service;
public HomeViewModel(IService service)
{
_service = service;
}
}
ViewModelFactory constructor selection
If your ViewModel has multiple constructors, you can specify which constructor should be used by decorating it with the [BlazorMvvmViewModelFactoryConstructor] attribute.
[BlazorMvvmViewModel(ViewModelLifetime.Singleton)]
public class HomeViewModel : BlazorViewModel
{
private readonly IService _service;
public HomeViewModel()
{
}
// This constructor will be used by the ViewModelFactory
[BlazorMvvmViewModelFactoryConstructor]
public HomeViewModel(IService service)
{
_service = service;
}
}
ViewModel lifetimes initialized via ViewModelFactory
You can control the lifetime of your ViewModels using the attribute:
[BlazorMvvmViewModel(ViewModelLifetime.Transient)]: Created every time it's requested (Default).[BlazorMvvmViewModel(ViewModelLifetime.Scoped)]: Created once per scope.[BlazorMvvmViewModel(ViewModelLifetime.Singleton)]: Created once per application.
Using ViewModelFactory in components
When using the BlazorMvvmViewModelFactory, you can retrieve ViewModel instances via dependency injection.
There is no need to call SetDataContext() manually, as the base class will do it for you.
Example: Home.razor.cs with ViewModelFactory registration
using BlazorMvvm;
namespace YourNamespace;
public partial class Home : BlazorMvvmComponentBase<HomeViewModel>
{
// ViewModel will be resolved and injected automatically if registered with [BlazorMvvmViewModelAttribute].
// No need to instantiate it manually.
// It can be accessed via BaseViewModel property inherited from BlazorMvvmComponentBase<T>.
// OnInitialized will call SetDataContext automatically.
// Just make sure to call base.OnInitialized() if overriding OnInitialized.
protected override void OnInitialized()
{
base.OnInitialized();
}
}
Source generation for properties and commands
Features
- Automatic property generation: Convert fields into observable properties with
[BlazorObservableProperty]. - Commands generation: Generate
Commandsimplementations from methods with[BlazorCommand]. - Async support: Support for both
TaskandValueTaskin commands. - Flexible CanExecute: Support for synchronous and asynchronous
CanExecutemethods. - Parameter support: Handle parameterless methods, single parameters, and multiple parameters (using Tuples).
- Concurrency control: Configure concurrent execution behavior for async commands.
Observable Properties
Decorate a private field with [BlazorObservableProperty] to generate a public property with change notification.
ViewModel classes must be declared as partial to use this feature.
The generator assumes the field is named either lowerCamel, _lowerCamel or m_lowerCamel, and it will transform that to be UpperCamel.
public partial class CounterViewModel : BlazorViewModel
{
[BlazorObservableProperty]
private int _count;
// Generates:
// public int Count
// {
// get => _count;
// set => Set(ref _count, value);
// }
}
Commands
Decorate a method with [BlazorCommand] to generate a proper Command.
Synchronous Command
[BlazorCommand]
private void Increment()
{
Count++;
}
// Generates: public IBlazorCommand IncrementCommand { get; }
Asynchronous Command
Supports both Task and ValueTask.
[BlazorCommand]
private async Task LoadDataAsync()
{
await Task.Delay(1000);
// ...
}
// Generates: public IBlazorAsyncCommand LoadDataAsyncCommand { get; }
Command with Parameter
[BlazorCommand]
private void UpdateMessage(string message)
{
Message = message;
}
// Generates: public IBlazorRelayCommand<string> UpdateMessageCommand { get; }
Command with Multiple Parameters
Methods with multiple parameters are wrapped using a Tuple.
[BlazorCommand]
private void Add(int a, int b)
{
Count = a + b;
}
// Generates: public IBlazorRelayCommand<(int, int)> AddCommand { get; }
// Usage in View: <button @onclick="() => AddCommand.Execute((5, 10))"></button>
CanExecute Logic
You can specify a method to control command execution using the CanExecute parameter (via constructor argument or property).
Synchronous CanExecute
[BlazorCommand(CanExecute = nameof(CanIncrement))]
private void Increment() => Count++;
private bool CanIncrement() => Count < 10;
Asynchronous CanExecute
The source generator automatically handles wrapping synchronous predicates for async commands, or you can use async predicates.
[BlazorCommand(nameof(CanDoAsync))]
private async Task DoAsync() { ... }
// Can be synchronous bool
// private bool CanDoAsync() => true;
// Or asynchronous
private async Task<bool> CanDoAsync()
{
// ...
return true;
}
Concurrency Control
For async commands, you can control whether multiple executions are allowed simultaneously.
// Prevent concurrent executions (default is false)
[BlazorCommand(AllowConcurrentExecutions = false)]
private async Task SubmitAsync()
{
// ...
}
| 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.