Use the MVVM pattern for Blazor with a simple and lightweight library.
This library is distributed via NuGet.
Check Live Demo.
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.
Viewmodels encapsulate the application's presentation logic and state. In BlazorMvvm, your viewmodels must inherit from the BlazorViewModel base class.
This base class implements INotifyPropertyChanged, which is essential for notifying the UI when a property's value has changed.
Example: HomeViewModel.cs
using BlazorMvvm;
using System.Runtime.CompilerServices;
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);
}
}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;
using Microsoft.AspNetCore.Components;
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();
}
}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.
-
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>
<!--
This fragment binds to ObservablePartViewModel and will
refresh when ANY property on it changes.
-->
<ObservableComponent ViewModel="ObservablePartViewModel">
<div>ObservableComponent current counter: @ObservablePartViewModel.Counter</div>
</ObservableComponent>
<!--
This fragment binds to SharedObservableViewModel but will ONLY
refresh when 'Counter1' changes.
-->
<ObservableComponent ViewModel="SharedObservableViewModel" PropertyNames="[nameof(SharedObservableViewModel.Counter1)]">
<div>SharedObservableViewModel current counter 1: @SharedObservableViewModel.Counter1</div>
</ObservableComponent>
<!--
This fragment also binds to SharedObservableViewModel but will ONLY
refresh when 'Counter2' or 'Counter3' changes.
A change to 'Counter1' will not affect this fragment.
-->
<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();
}
}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).
-
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)
-
-
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();
}
}