MVVM is a development pattern that has been around for a while now. It was designed to facilitate the development of WPF applications for Windows and is still used for the likes of Maui apps. I reckon it has a place in Blazor apps as a neat way to separate view from logic, and as a sort of volatile state management for when you don’t want state to persist between pages.
Goals
I won’t go into detail of what MVVM is or what it can offer, that’s already been explained better than I could hope to. This article assumes you know what you’re doing with your IDE and .NET in general, it also helps to have some familiarity with Blazor.
In summary, MVVM is short for Model, View, ViewModel. From a Blazor application perspective:
- The Model would be a DTO, either stored in persistent state management or fetched fresh from a back-end API
- The View is the
.razor
file
- The ViewModel handles the state and logic of a view, be that a component or a page
Some key benefits of MVVM with regards to a Blazor application:
- Negates (or minimises) the need for code-behind on components and pages
- Complete separation of views from logic; VMs are view agnostic and could, for example, be ported to a Maui app quite easily
- Light state management, e.g. for submission forms that are destroyed when submitted or navigating away
- Use of
INotifyPropertyChanged
and INotifyCollectionChanged
interfaces streamline component refreshes
For this example I will be using the default Blazor WASM template provided with .NET 6.0. Later versions of .NET should be able to follow the same principles outlined here. I will also be using the following packages:
This demonstration won’t do anything particularly fancy but should serve as a good introduction to how MVVM can work effectively in a Blazor app.
Getting Started
Start by creating a Blazor WASM project:
dotnet new blazorwasm -n BlazorWithMvvm
Go into the new directory and add the three packages mentioned previously:
dotnet add package MvvmBlazor
dotnet add package CommunityToolkit.Mvvm
dotnet add package MudBlazor
MvvmBlazor and MudBlazor both require additional setup.
Setting up MvvmBlazor
MvvmBlazor’s setup documentation can be found here.
In Program.cs
, register the MvvmBlazor service:
builder.Services.AddMvvm();
Add the relevant MvvmBlazor namespaces to _Imports.razor
:
@using MvvmBlazor
@using MvvmBlazor.Components
Setting up MudBlazor
MudBlazor’s setup documentation can be found here.
In Program.cs
, register the MudBlazor service:
builder.Services.AddMudServices();
Add the MudBlazor namespace to _Imports.razor
:
Add font and stylesheet references inside the <head>
tag in index.html
:
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
Add a script reference alongside the existing Blazor script reference near the bottom of <body>
in index.html
:
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
Add the components you need to MainLayout.razor
; only MudThemeProvider
is required. This can go underneath the @inherits...
line.
<MudThemeProvider/>
<MudDialogProvider/>
<MudSnackbarProvider/>
Creating an MVVM Component
Create the ViewModel
MvvmBlazor relies on code generators to do a lot of the heavy lifting so VMs are pretty easy to put together. Here’s an example (using
statements and namespace removed for brevity):
public partial class MyButtonViewModel : ViewModelBase
{
[Notify]
private string _myMessage = default!;
[Parameter]
public int CurrentCount { get; set; }
private bool _canSetCount => CurrentCount > 3;
[RelayCommand(CanExecute = nameof(_canSetCount))]
private void SetCount()
{
MyMessage = "S'cool!";
}
}
Here’s what you need to know about this ViewModel:
- Source generators require this class to have a namespace
- It must be declared as a
partial
for source generators to work
- It must inherit from
ViewModelBase
- The
[Notify]
attribute is applied to a member variable, source generators will handle the INotify
goodies and expose it as a publicly accessible property
- The
[Parameter]
property will tie up with the component itself to pass parameters straight through to the ViewModel
SetCount
has a RelayCommand
attribute from MVVM Toolkit which uses source generators to build an IRelayCommand
object around the SetCount()
method called SetCountCommand
. It also references the _canSetCount
variable to determine whether or not the command can be run with CanExecute
. The method updates the source generated property MyMessage
mentioned before, triggering notification updates automatically
Add the ViewModel to the services registration in Program.cs
. In this case I’m using AddTransient
so that it is disposed of when the component is destroyed; navigating away and back again will reset it.
builder.Services.AddTransient<MyButtonViewModel>();
You could set this up so that ViewModels can be picked up automatically by using reflection to find all classes with the suffix `ViewModel`, for example.
Create the Component
The following code demonstrates how to take advantage of the ViewModel for a component:
@inherits MvvmComponentBase<MyButtonViewModel>
<MudButton Command="@BindingContext.SetCountCommand" Disabled="@(!BindingContext.SetCountCommand.CanExecute(null))">Is it cool?</MudButton>
<MudText>
@Bind(x => x.MyMessage)
</MudText>
@code {
[Parameter]
public int CurrentCount { get; set; }
}
Here’s what you need to know about this Component:
- It inherits from
MvvmComponentBase<ComponentViewModel>
- The concrete VM can be accessed with the
BindingContext
property
- In the case of
MudButton
, the Command
property is not sufficient to disable it when CanExecute()
is false
so I’ve set it up manually here
- Properties are bound with the pattern
@Bind(x => x.BindingPropertyName)
- Code-behind is unavoidable but minimal, other components still need to know what parameters this component can accept, hence the
@code
block
Using the Component
To demonstrate how this component works I plugged it in to the Counter.razor
page by adding the following just above the @code
block:
<MyButton CurrentCount="currentCount" />
currentCount
is a property on that component already and we’re passing it down. The idea is that the user can increment that count, but we’ll only see our button enable when the count is higher than 3 (see the predicate in the ViewModel).
Try it out!
Run the app, go to the Counter
page and the button should be disabled. Increment the counter a few times and the button should be enabled, clicking it should show a wee message.