MVVM Support in Blazor
A solution for property change notification across components
I recently ported an Angular app to Blazor and wrote about it here:
Build a sample app in Blazor, aĀ .NET-based framework for building web apps that run in the browser and leverages C# and Razor templates to generate cross-platform, HTML5-compliant WebAssembly code.
I noticed immediately that Blazorās built in change detection works great inside of components (i.e. if you mutate a property on the model, dependent HTML will re-render). You donāt have to do anything special with your model. Updates simply refresh the component! The catch is that other components donāt automatically refresh, so you need to implement a mechanism for change detection across components.
Examples in this post/repo are for version
3.2.0-preview1.20073.1
I did some initial research, and the team is adamant this wonāt be baked into the framework. Here is the relevant quote from this issue:
Our goal with Blazor is target a broad audience with Web developers, so weāre specifically not targeting compatibility with WPF/UWP/Xamarin.Forms. We are also trying to give the you the flexibility to use the patterns you want without baking too much in the framework. So you should be able to implement MVVM patterns on top of Blazorās core concepts (much like you are doing already). It doesnāt sound like you are blocked with this, and we donāt plan to do more in this direction.
There are certainly other frameworks available, some that even support XAML. For example, take a look at Uno that is supported by MVVMLight. But what if you want to use HTML and expect change detection in your underlying models? I investigated a few solutions. In the issue thread, one suggestion was to make a base component. That felt like extra work to tag everything to inherit the component and then override a method to set the model that might change.
Blazor is brand new and it might make sense, as suggested by Tim Heuer and Oren Novotny, to take a different approach:
I feel like since #Blazor is soninfant right now we have a chance to better this pattern and instead of wrapping content letās push for attribute-based [Observable]?
— Tim Heuer (@timheuer) January 4, 2019
There's this little thing called Reactive Extensions for .NET that might fit quite well in here.
— Claire Novotny (@clairernovotny) January 4, 2019
However, an [Observable]
attribute would simplify change detection on the model but wouldnāt necessarily address communication between components. I wanted something that leverages the time-tested, baked in interface for change detection: INotifyPropertyChanged
. Then I can create a view model that simply raises an event when properties change:
public class NumberViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _number = 42;
public int Number
{
get => _number;
set
{
if (value != _number)
{
_number = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Number)));
}
}
}
}
In XAML, you can specify a DataSource
and it is scoped to the child elements until it is overridden later in the hierarchy. What if we could do something similar, only instead of providing scope for data-binding (this is already handled in Blazor and even supports having multiple models to reference in the same component, which is quite alright with me), it provides a scope for change detection?
From my Angular experience I immediately thought of interpolation. This the ability of a component to encapsulate child components. For example, instead of this:
<Component/>
You can do this:
<Component><ChildComponent/></Component>
It is important functionality and therefore no surprise that it is supported by Blazor. The easiest way to handle interpolation is to use the RenderFragment
type and expose a property named ChildComponent
by convention. In Blazor, state change detection is hierarchical so if a parent is notified of state changes, its children are notified as well. So, I simply need a component that
- Provides interpolation so I can embed child components
- Has a property I can use to set the view model we are watching
- Calls
StateHasChanged
whenever a property on the model changes
I decided to name this ViewModelRegion
because it indicates a region that watches for changes. Here is the full implementation (it was so simple I didnāt believe it would work the first time I tried it):
@ChildContent
@using System.ComponentModel
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public INotifyPropertyChanged ViewModel { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
ViewModel.PropertyChanged += (o, e) => StateHasChanged();
}
}
Given a component that binds to the number model:
@inject BlazorMVVM.Models.NumberViewModel Model
<div>
<p>Enter a number:</p>
<input type="number" @bind="Model.Number"/>
</div>
ā¦and another component that binds to it for display:
@inject BlazorMVVM.Models.NumberViewModel NumberModel
<div>
<h3>No VM Wrapper</h3>
<p>Number: @NumberModel.Number</p>
</div>
Without any changes, updates to the first component donāt reflect in the second component. However, wrapping it in the ViewModelRegion
fixes that.
@inject BlazorMVVM.Models.NumberViewModel NumberModel
<div>
<h3>Number Wrapper</h3>
<ViewModelRegion ViewModel="@NumberModel">
<p>Number: @NumberModel.Number</p>
</ViewModelRegion>
</div>
This solution also fully supports multiple models and regions:
@inject BlazorMVVM.Models.NumberViewModel NumberModel
@inject BlazorMVVM.Models.ToggleViewModel ToggleModel
<div>
<h3>Two Wrappers</h3>
<ViewModelRegion ViewModel="@NumberModel">
<p>Number: @NumberModel.Number</p>
</ViewModelRegion>
<ViewModelRegion ViewModel="@ToggleModel">
<p>Toggle: @ToggleModel.Toggle</p>
</ViewModelRegion>
</div>
And, yes, you can even nest them if you prefer.
This solution comes with a lot of caveats. I havenāt tested performance, there may be better ways to notify on property changes, and we might be able to extend the framework to automatically detect view models and register for changes. I havenāt explored support for ICommand
yet and may not dig much deeper because Blazor is still in such an early state. However, it works and was a fun experiment to try.
I created a very simple project to demonstrate this solution:
You can clone it and/or fork it from this repository:
Are you using Blazor? I look forward to your thoughts and feedback!
Regards,
Related articles:
- Advanced Blazor: Shared Assemblies and Debugging from Edge (Web Development)
- Build a Blazor WebAssembly Line of Business App Part 3: Query, Delete and Concurrency (WebAssembly)
- Build a Blazor WebAssembly LOB App Part 4: Make it Blazor-Friendly (WebAssembly)
- Build a Single Page Application (SPA) Site With Vanilla.js (JavaScript)
- Build an Azure AD Secured Blazor Server Line of Business App (WebAssembly)
- Build Data-Driven Web Apps Blazing Fast with Blazor and OData (Web Development)