This project expands upon the blazor-mvvm repository by Kelly Adams, implementing full MVVM support via the CommunityToolkit.Mvvm. Enhancements include preventing cross-thread exceptions, adding extra base class types, MVVM-style navigation, and converting the project into a usable library.
- Blazor Extension for the MVVM CommunityToolkit
Add the Blazing.Mvvm NuGet package to your project.
Install the package via .NET CLI or the NuGet Package Manager.
dotnet add package Blazing.Mvvm
Install-Package Blazing.Mvvm
Configure the library in your Program.cs
file. The AddMvvm
method will add the required services for the library and automatically register ViewModels that inherit from the ViewModelBase
, RecipientViewModelBase
, or ValidatorViewModelBase
class in the calling assembly.
using Blazing.Mvvm;
builder.Services.AddMvvm(options =>
{
options.HostingModelType = BlazorHostingModelType.WebApp;
});
If you are using a different hosting model, set the HostingModelType
property to the appropriate value. The available options are:
BlazorHostingModelType.Hybrid
BlazorHostingModelType.Server
BlazorHostingModelType.WebApp
BlazorHostingModelType.WebAssembly
BlazorHostingModelType.HybridMaui
If the ViewModels are in a different assembly, configure the library to scan that assembly for the ViewModels.
using Blazing.Mvvm;
builder.Services.AddMvvm(options =>
{
options.RegisterViewModelsFromAssemblyContaining<MyViewModel>();
});
// OR
var vmAssembly = typeof(MyViewModel).Assembly;
builder.Services.AddMvvm(options =>
{
options.RegisterViewModelsFromAssembly(vmAssembly);
});
public partial class FetchDataViewModel : ViewModelBase
{
private static readonly string[] Summaries = [
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
[ObservableProperty]
private ObservableCollection<WeatherForecast> _weatherForecasts = new();
public string Title => "Weather forecast";
public override void OnInitialized()
=> WeatherForecasts = new ObservableCollection<WeatherForecast>(Get());
private IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
});
}
}
NOTE: If working with repositories, database services, etc, that require a scope, then use
MvvmOwningComponentBase<TViewModel>
instead.
@page "/fetchdata"
@inherits MvvmOwningComponentBase<FetchDataViewModel>
<PageTitle>@ViewModel.Title</PageTitle>
<h1>@ViewModel.Title</h1>
@if (!ViewModel.WeatherForecasts.Any())
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in ViewModel.WeatherForecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
If you like or are using this project to learn or start your solution, please give it a star. Thanks!
Also, if you find this library useful, and you're feeling really generous, then please consider buying me a coffee ☕.
The Library supports the following hosting models:
- Blazor Server App
- Blazor WebAssembly App (WASM)
- Blazor Web App (.NET 8.0+)
- Blazor Hybrid - Wpf, WinForms, MAUI, and Avalonia (Windows only)
The library package includes:
MvvmComponentBase
,MvvmOwningComponentBase
(Scoped service support), &MvvmLayoutComponentBase
for quick and easy wiring up ViewModels.ViewModelBase
,RecipientViewModelBase
, &ValidatorViewModelBase
wrappers for the CommunityToolkit.Mvvm.MvvmNavigationManager
class,MvvmNavLink
, andMvvmKeyNavLink
component for MVVM-style navigation, no more hard-coded paths.- Sample applications for getting started quickly with all hosting models.
There are two additional sample projects in separate GitHub repositories:
- Blazor MVVM Sample - takes Microsoft's Xamarin Sample project for the CommunityToolkit.Mvvm and converts it to: Blazor Wasm & Blazor Hybrid for Wpf & Avalonia. Minimal changes were made.
- Dynamic Parent and Child - demonstrates loose coupling of a parent component/page and an unknown number of child components using Messenger for interactivity.
The library offers several base classes that extend the CommunityToolkit.Mvvm base classes:
ViewModelBase
: Inherits from theObservableObject
class.RecipientViewModelBase
: Inherits from theObservableRecipient
class.ValidatorViewModelBase
: Inherits from theObservableValidator
class and supports theEditForm
component.
The ViewModelBase
, RecipientViewModelBase
, and ValidatorViewModelBase
classes support the ComponentBase
lifecycle methods, which are invoked when the corresponding ComponentBase
method is called:
OnAfterRender
OnAfterRenderAsync
OnInitialized
OnInitializedAsync
OnParametersSet
OnParametersSetAsync
ShouldRender
ViewModels are registered as Transient
services by default. If you need to register a ViewModel with a different service lifetime (Scoped, Singleton, Transient), use the ViewModelDefinition
attribute:
[ViewModelDefinition(Lifetime = ServiceLifetime.Scoped)]
public partial class FetchDataViewModel : ViewModelBase
{
// ViewModel code
}
In the View
component, inherit the MvvmComponentBase
type and set the generic argument to the ViewModel
:
@page "/fetchdata"
@inherits MvvmComponentBase<FetchDataViewModel>
To register the ViewModel
with a specific interface or abstract class, use the ViewModelDefinition
generic attribute:
[ViewModelDefinition<IFetchDataViewModel>]
public partial class FetchDataViewModel : ViewModelBase, IFetchDataViewModel
{
// ViewModel code
}
In the View
component, inherit the MvvmComponentBase
type and set the generic argument to the interface or abstract class:
@page "/fetchdata"
@inherits MvvmComponentBase<IFetchDataViewModel>
To register the ViewModel
as a keyed service, use the ViewModelDefinition
attribute (this also applies to generic variant) and set the Key
property:
[ViewModelDefinition(Key = "FetchDataViewModel")]
public partial class FetchDataViewModel : ViewModelBase
{
// ViewModel code
}
In the View
component, use the ViewModelKey
attribute to specify the key of the ViewModel
:
@page "/fetchdata"
@attribute [ViewModelKey("FetchDataViewModel")]
@inherits MvvmComponentBase<FetchDataViewModel>
The library supports passing parameter values to the ViewModel
which are defined in the View
.
This feature is opt-in. To enable it, set the ParameterResolutionMode
property to ViewAndViewModel
in the AddMvvm
method. This will resolve parameters in both the View
component and the ViewModel
.
builder.Services.AddMvvm(options =>
{
options.ParameterResolutionMode = ParameterResolutionMode.ViewAndViewModel;
});
To resolve parameters in the ViewModel
only, set the ParameterResolutionMode
property value to ViewModel
.
Properties in the ViewModel
that should be set must be marked with the ViewParameter
attribute.
public partial class SampleViewModel : ViewModelBase
{
[ObservableProperty]
[property: ViewParameter]
private string _title;
[ViewParameter]
public int Count { get; set; }
[ViewParameter("Content")]
private string Body { get; set; }
}
In the View
component, the parameters should be defined as properties with the Parameter
attribute:
@inherits MvvmComponentBase<SampleViewModel>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public int Count { get; set; }
[Parameter]
public string Content { get; set; }
}
No more magic strings! Strongly-typed navigation is now possible. If the page URI changes, you no longer need to search through your source code to make updates. It is auto-magically resolved at runtime for you!
When the MvvmNavigationManager
is initialized by the IOC container as a Singleton, the class examines all assemblies and internally caches all ViewModels (classes and interfaces) along with their associated pages.
When navigation is required, a quick lookup is performed, and the Blazor NavigationManager
is used to navigate to the correct page. Any relative URI or query string passed via the NavigateTo
method call is also included.
Note: The
MvvmNavigationManager
class is not a complete replacement for the BlazorNavigationManager
class; it only adds support for MVVM.
Modify the NavMenu.razor
to use MvvmNavLink
:
<div class="nav-item px-3">
<MvvmNavLink class="nav-link" TViewModel="FetchDataViewModel">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</MvvmNavLink>
</div>
The
MvvmNavLink
component is based on the BlazorNavLink
component and includes additionalTViewModel
andRelativeUri
properties. Internally, it uses theMvvmNavigationManager
for navigation.
Navigate by ViewModel using the MvvmNavigationManager
from code:
Inject the MvvmNavigationManager
class into your page or ViewModel, then use the NavigateTo
method:
mvvmNavigationManager.NavigateTo<FetchDataViewModel>();
The NavigateTo
method works the same as the standard Blazor NavigationManager
and also supports passing a relative URL and/or query string.
If you prefer abstraction, you can also navigate by interface as shown below:
mvvmNavigationManager.NavigateTo<ITestNavigationViewModel>();
The same principle works with the MvvmNavLink
component:
<div class="nav-item px-3">
<MvvmNavLink class="nav-link"
TViewModel=ITestNavigationViewModel
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span>Test
</MvvmNavLink>
</div>
<div class="nav-item px-3">
<MvvmNavLink class="nav-link"
TViewModel=ITestNavigationViewModel
RelativeUri="this is a MvvmNavLink test"
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span>Test + Params
</MvvmNavLink>
</div>
<div class="nav-item px-3">
<MvvmNavLink class="nav-link"
TViewModel=ITestNavigationViewModel
RelativeUri="?test=this%20is%20a%20MvvmNavLink%20querystring%20test"
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span>Test + QueryString
</MvvmNavLink>
</div>
<div class="nav-item px-3">
<MvvmNavLink class="nav-link"
TViewModel=ITestNavigationViewModel
RelativeUri="this is a MvvmNvLink test/?test=this%20is%20a%20MvvmNavLink%20querystring%20test"
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span>Test + Both
</MvvmNavLink>
</div>
Navigate by ViewModel Key using the MvvmNavigationManager
from code:
Inject the MvvmNavigationManager
class into your page or ViewModel, then use the NavigateTo
method:
MvvmNavigationManager.NavigateTo("FetchDataViewModel");
The same principle works with the MvvmKeyNavLink
component:
<div class="nav-item px-3">
<MvvmKeyNavLink class="nav-link"
NavigationKey="@nameof(TestKeyedNavigationViewModel)"
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span> Keyed Test
</MvvmKeyNavLink>
</div>
<div class="nav-item px-3">
<MvvmKeyNavLink class="nav-link"
NavigationKey="@nameof(TestKeyedNavigationViewModel)"
RelativeUri="this is a MvvmKeyNavLink test"
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span> Keyed + Params
</MvvmKeyNavLink>
</div>
<div class="nav-item px-3">
<MvvmKeyNavLink class="nav-link"
NavigationKey="@nameof(TestKeyedNavigationViewModel)"
RelativeUri="?test=this%20is%20a%20MvvmKeyNavLink%20querystring%20test"
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span> Keyed + QueryString
</MvvmKeyNavLink>
</div>
<div class="nav-item px-3">
<MvvmKeyNavLink class="nav-link"
NavigationKey="@nameof(TestKeyedNavigationViewModel)"
RelativeUri="this is a MvvmKeyNavLink test/?test=this%20is%20a%20MvvmKeyNavLink%20querystring%20test"
Match="NavLinkMatch.All">
<span class="oi oi-calculator" aria-hidden="true"></span> Keyed + Both
</MvvmKeyNavLink>
</div>
The library provides an MvvmObservableValidator
component that works with the EditForm
component to enable validation using the ObservableValidator
class from the CommunityToolkit.Mvvm library.
The following example demonstrates how to use the MvvmObservableValidator
component with the EditForm
component to perform validation.
First, define a class that inherits from the ObservableValidator
class and contains properties with validation attributes:
public class ContactInfo : ObservableValidator
{
private string? _name;
[Required]
[StringLength(100, MinimumLength = 2, ErrorMessage = "The {0} field must have a length between {2} and {1}.")]
[RegularExpression(@"^[a-zA-Z\s'-]+$", ErrorMessage = "The {0} field contains invalid characters. Only letters, spaces, apostrophes, and hyphens are allowed.")]
public string? Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
private string? _email;
[Required]
[EmailAddress]
public string? Email
{
get => _email;
set => SetProperty(ref _email, value, true);
}
private string? _phoneNumber;
[Required]
[Phone]
[Display(Name = "Phone Number")]
public string? PhoneNumber
{
get => _phoneNumber;
set => SetProperty(ref _phoneNumber, value, true);
}
}
Next, in the ViewModel
component, define the property that will hold the object to be validated and the methods that will be called when the form is submitted:
public sealed partial class EditContactViewModel : ViewModelBase, IDisposable
{
private readonly ILogger<EditContactViewModel> _logger;
[ObservableProperty]
private ContactInfo _contact = new();
public EditContactViewModel(ILogger<EditContactViewModel> logger)
{
_logger = logger;
Contact.PropertyChanged += ContactOnPropertyChanged;
}
public void Dispose()
=> Contact.PropertyChanged -= ContactOnPropertyChanged;
[RelayCommand]
private void ClearForm()
=> Contact = new ContactInfo();
[RelayCommand]
private void Save()
=> _logger.LogInformation("Form is valid and submitted!");
private void ContactOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
=> NotifyStateChanged();
}
Finally, in the View
component, use the EditForm
component with the MvvmObservableValidator
component to enable validation:
@page "/form"
@inherits MvvmComponentBase<EditContactViewModel>
<EditForm Model="ViewModel.Contact" FormName="EditContact" OnValidSubmit="ViewModel.SaveCommand.Execute">
<MvvmObservableValidator />
<ValidationSummary />
<div class="row g-3">
<div class="col-12">
<label class="form-label">Name:</label>
<InputText aria-label="name" @bind-Value="ViewModel.Contact.Name" class="form-control" placeholder="Some Name"/>
<ValidationMessage For="() => ViewModel.Contact.Name" />
</div>
<div class="col-12">
<label class="form-label">Email:</label>
<InputText aria-label="email" @bind-Value="ViewModel.Contact.Email" class="form-control" placeholder="[email protected]"/>
<ValidationMessage For="() => ViewModel.Contact.Email" />
</div>
<div class="col-12">
<label class="form-label">Phone Number:</label>
<InputText aria-label="phone number" @bind-Value="ViewModel.Contact.PhoneNumber" class="form-control" placeholder="555-1212"/>
<ValidationMessage For="() => ViewModel.Contact.PhoneNumber" />
</div>
</div>
<hr class="my-4">
<div class="row">
<button class="btn btn-primary btn-lg col"
type="submit"
disabled="@ViewModel.Contact.HasErrors">
Save
</button>
<button class="btn btn-secondary btn-lg col"
type="button"
@onclick="ViewModel.ClearFormCommand.Execute">
Clear Form
</button>
</div>
</EditForm>
- Added support for
ObservableRecipient
being set to inactive when disposing theMvvmComponentBase
,MvvmOwningComponentBase
,MvvmLayoutComponentBase
, andRecipientViewModelBase
. @gragra33 & @teunlielu
- Version bump to fix a nuget release issue
- Added MAUI Blazor Hybrid App support + sample HybridMaui app. @hakakou
This is a major release with breaking changes, migration notes can be found here.
- Added auto registration and discovery of view models. @mishael-o
- Added support for keyed view models. @mishael-o
- Added support for keyed view models to
MvvmNavLink
,MvvmKeyNavLink
(new component),MvvmNavigationManager
,MvvmComponentBase
,MvvmOwningComponentBase
, &MvvmLayoutComponentBase
. @gragra33 - Added a
MvvmObservableValidator
component which provides support forObservableValidator
. @mishael-o - Added parameter resolution in the ViewModel. @mishael-o
- Added new
TestKeyedNavigation
samples for Keyed Navigation. @gragra33 - Added & Updated tests for all changes made. @mishael-o & @gragra33
- Added support for .NET 9. @gragra33
- Dropped support for .NET 7. @mishael-o
- Documentation updates. @mishael-o & @gragra33
BREAKING CHANGES:
- Renamed
BlazorHostingModel
toBlazorHostingModelType
to avoid confusion
The full history can be found in the Version Tracking documentation.