Recently, I've been doing some work with Blazor and decided that using the MVVM pattern could be quite useful as it will allow the flexibility to have multiple frontends working from the same view model. This would allow for a web-based frontend and also a mobile app one, which could be MAUI (using XAML) or MAUI Blazor. There could also be a Windows Forms application using the same models, although in my case it don't think this will be necessary. Using the MVVM pattern also makes the frontend easier to unit test as the logic is loosely coupled to the view.

In this article I will show you a way to implement the MVVM pattern in Blazor.

What is MVVM?

The idea behind the MVVM (Model-View-ViewModel) pattern is separating the Model (your business logic and data), the View (presentation) and the ViewModel (presentation logic) which controls what is displayed by the view and how the view interacts with the Model.

The View and View Model are bound together by a Binder which automatically updates the View Model from the View and vice-versa. For this reason, Model-View-ViewModel is also known as Model-View-Binder, although this is more common outside of the Microsoft ecosystem.

In an approach such as Clean Architecture the Model would represent the domain/application, or in a data-centric approach it would represent the Data Access Layer (DAL).

The key thing to note is that the View should not have any dependency on the Model. The only thing that View should be interacting with is the ViewModel.

Isn't Blazor already based on the MVVM pattern?

It has a Model, a View, a Binder, and if you choose to use one, a ViewModel. However, it doesn't prevent you from calling the Model directly from your View, or from defining properties directly in your View instead of using a ViewModel. It also doesn't restrict how many view models you have in a single view.

This was apparently an architectural decision by Microsoft to not restrict you to any particular pattern, but to use it as you see fit.

The implementation

We'll be using Blazor Server in Interactive Auto mode for this implementation.

The first thing that we'll need is a base class for our view models. This class will implement `System.ComponentModel.INotifyPropertyChanged', meaning that it will need to raise an event every time that a property changes. By using this approach we should be compatible with other parts of the .NET ecosystem (e.g. MAUI using XAML).

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public virtual void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

I've marked the NotifyPropertyChanged method as virtual, this will help when it comes to unit testing. We will be able to mock our view models and to ensure that this method is being called.

The next step is to create a base component that contains a View Model property implementing INotifyPropertyChanged.

We'll inject the View Model and subscribe to its PropertyChanged event on initialization (disposing appropriately).

When the PropertyChanged event is triggered we'll call Blazor's StateHasChanged method, to inform the Binder that something has changed and that it needs to update the component.

public abstract class ComponentBaseWithViewModel<TViewModel> :
    ComponentBase,
    IDisposable
    where TViewModel : INotifyPropertyChanged
{
    [Inject]
    protected TViewModel ViewModel { get; init; }

    protected override void OnInitialized()
    {
        ViewModel.PropertyChanged += OnChangeHandler;
    }

    private async void OnChangeHandler(object? sender, PropertyChangedEventArgs e)
    {
        await InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        ViewModel.PropertyChanged -= OnChangeHandler;
    }
}

Now we can create an interface for our first View Model.

We'll be creating a very simple calculator that Adds and Subtracts 2 numbers, and displays the result. The actual calculation will be performed by the ISimpleCalculator in our Model.

public interface ICalculatorViewModel : INotifyPropertyChanged
{
    int? Number1 { get; set; }
    int? Number2 { get; set; }
    int? Result { get; }
    string Message { get; }

    void PerformAddition();
    void PerformSubtraction();
}

Next we can create our implementation of the View Model.

Any public properties that are updated need to call the NotifyPropertyChanged within their setter so that they trigger an update of the view.

public class CalculatorViewModel : ViewModelBase, ICalculatorViewModel
{
    private int? _result;
    private string _message;
    private readonly ISimpleCalculator _calculator;

    public CalculatorViewModel(ISimpleCalculator calculator)
    {
        _calculator = calculator;
        _message = string.Empty;
    }

    public int? Number1 { get; set; }
    public int? Number2 { get; set; }

    public int? Result { 
        get { return _result; } 
        private set { _result = value; NotifyPropertyChanged(); }
     }

    public string Message { 
        get { return _message; } 
        private set { _message = value; NotifyPropertyChanged(); } 
    }

    public void PerformAddition()
    {
        ClearMessage();

        if (EntryIsValid())
            Result = _calculator.Add(Number1.Value, Number2.Value);
        else
            SetError();
    }

    public void PerformSubtraction()
    {
        ClearMessage();

        if (EntryIsValid())
            Result = _calculator.Subtract(Number1.Value, Number2.Value);
        else
            SetError();
    }

    private void ClearMessage() => Message = string.Empty;
    private void SetError() => Message = "You must enter 2 numbers!";

    [MemberNotNullWhen(true, nameof(Number1), nameof(Number2))]
    private bool EntryIsValid()
    {
        return Number1 is not null && Number2 is not null;
    }
}

As we're injecting our View Model into the View, we'll need to register it as a service on startup.

I've set the scope here as transient. This means that a new instance will be used each time the component is created. If I were to set it as scoped then it's state would be kept each time the component was created, so if you navigated to another page and then back again, the calculator would still have the same values.

services.AddTransient<ICalculatorViewModel, CalculatorViewModel>() ;

Finally, we can create our component view which inherits from our base class with the type parameter of our view model.

@inherits ComponentBaseWithViewModel<ICalculatorViewModel>
<div>
    
    <div class="form-floating mb-3">
        <InputNumber @bind-Value="ViewModel.Number1" placeholder="Enter a number" class="form-control">
        </InputNumber>
        <label for="@nameof(ViewModel.Number1)" class="form-label">Enter a number</label>
    </div>
    <div class="form-floating mb-3">
        <InputNumber @bind-Value="ViewModel.Number2" placeholder="Enter another number" class="form-control">
        </InputNumber>
        <label for="@nameof(ViewModel.Number2)" class="form-label">Enter another number</label>
    </div>
    <div class="mt-3">
        <p>What should I do with these numbers?</p>
    </div>
    <div class="d-flex flex-row mb-3">
        <button @onclick="ViewModel.PerformAddition" class="btn btn-primary w-50 me-1">
            Add them together
        </button>
        <br />
        <button @onclick="ViewModel.PerformSubtraction" class="btn btn-primary w-50 ms-1">
            Subtract number 2 from number 1
        </button>
    </div>
    @if(string.IsNullOrEmpty(ViewModel.Message))
    {
        if (ViewModel.Result.HasValue)
        {
            <div class="alert alert-success">
                The result is: @ViewModel.Result.Value
            </div>
        }
    }
    else
    {
        <div class="alert alert-danger">@ViewModel.Message</div>
    }
</div>

The final result...

What about Blazor WASM and Blazor Hybrid?

We can extend this approach to Blazor WASM or Hybrid. The difference is what we use as the Model as were unlikely to want to be interacting will the domain or data access layer directly. We'll probably want to be calling an API.

Source Code

The source code for this article, along with an MVVM interpretation of the standard Visual Studio Blazor template (refactored into separate projects) can be found on Github.

Support me

If you liked this article then please consider supporting me.

An unhandled error has occurred. Reload 🗙