Try the MVVM pattern with Blazor

3 minute read

Introduction

I was developing an application with Blazor, and I thought that it would be easier to develop if I could apply the MVVM pattern that was done during WPF development.
The sample is created as a Blazor Server app.
As a preliminary preparation, ReactiveProperty, a library that assists in the development of MVVM patterns, is installed via nuget.

·environment
.Net Core 3.1
ReactiveProperty 7.2.0

This time, we will create an application that displays the number of characters when you enter characters in the text box.

 striglength.gif

Create View

StringLengthCounter.razor


@page "/stringLengthCounter"
@inject StringLengthCounterViewModel ViewModel

<h1>String counter</h1>
<input id="text1" @bind="ViewModel.Text1.Value" @bind:event="oninput" />
<p>@ViewModel.Text2.Value</p>

Create a Razor component that corresponds to the View. There is a text box for entering characters and a part for displaying the number of characters in the result.
Get an instance of ViewModel registered in the DI container with @ inject. View’s policy is to minimize coding and only bind ViewModel parameters.

For data binding in Razor component, refer to the following page.
ASP.NET Core Blazor Data Binding

Creating a ViewModel

StringLengthCounterViewModel.cs


    /// <summary>
    ///String counter view model
    /// </summary>
    public class StringLengthCounterViewModel
    {
        public ReactivePropertySlim<string> Text1 { get; set; } = new ReactivePropertySlim<string>();

        public ReadOnlyReactivePropertySlim<string> Text2 { get; set; }

        public StringLengthCounterViewModel(StringLengthCounterModel model)
        {
            this.Text1 = model.Text1;
            this.Text2 = model.Text2;
        }
    }

Create a ViewModel with the items handled by View. This time, it’s as simple as assigning the Model value to the property as it is.
Model is obtained from the DI container by constructor injection.

Creating a Model

    /// <summary>
    ///String counter model
    /// </summary>
    public class StringLengthCounterModel
    {
        public ReactivePropertySlim<string> Text1 { get; set; } = new ReactivePropertySlim<string>();

        public ReadOnlyReactivePropertySlim<string> Text2 { get; set; }

        public StringLengthCounterModel()
        {
            this.Text2 =
                Text1
                .Select(x => string.IsNullOrEmpty(x) ? "0 characters" : x.Length + "letter")
                .ToReadOnlyReactivePropertySlim();
        }
    }

Create a Model that checks the number of characters with Text1 as input and outputs the result to Text2.

DI container registration

Startup.cs


        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddScoped<StringLengthCounterModel>();
            services.AddTransient<StringLengthCounterViewModel>();
        }

Register the created ViewModel and Model in the DI container. It is done by adding to the ConfigureServices method in the existing Startup.cs.
There are methods called ʻAddScoped ʻAddTransient, but each has a different object lifespan. Also, the specifications differ between Blazor Server and Blazor WebAssembly.
With the above, we have created an application that displays the number of characters entered in the text box.

Details of the DI container are described on the official website.
Insert ASP.NET Core Blazor Dependency

Supplement: Notification from ViewModel to View

Modify the Model of the application created as above to delay the notification to Text2 by 100ms using the Delay method.

        public StringLengthCounterModel()
        {
            this.Text2 =
                Text1
                .Select(x => string.IsNullOrEmpty(x) ? "0 characters" : x.Length + "letter")
                .Delay(TimeSpan.FromMilliseconds(100))
                .ToReadOnlyReactivePropertySlim();
        }

I think that the result will be displayed with a delay of 100ms, and it behaves differently than expected. The string is not counted correctly.

striglength2.gif

Furthermore, rewrite the previous process.

        public StringLengthCounterModel()
        {
            this.Text2 =
                Text1
                .Select(x => string.IsNullOrEmpty(x) ? "0 characters" : x.Length + "letter")
                .Delay(TimeSpan.FromMilliseconds(100), Scheduler.Immediate)
                .ToReadOnlyReactivePropertySlim();
        }

The changed part is the second argument of the Delay method. Now, as expected, the Text2 changes are reflected in the View with a delay of 100ms. This time I specified Scheduler.Immediate, but the default scheduler for the Delay method is ThreadPoolScheduler. Apparently, when I use the thread pool in Model, the changes are not automatically reflected in View.

Even if you do not specify Scheduler.Immediate, you can make the intended operation by describing the process of notifying the change from ViewModel in View.

@page "/stringLengthCounter"
@inject StringLengthCounterViewModel ViewModel

<h1>String counter</h1>

<input id="text1" @bind="ViewModel.Text1.Value" @bind:event="oninput" />

<p>@ViewModel.Text2.Value</p>

@code {
    protected override void OnInitialized()
    {
        ViewModel.Text2.Subscribe(_ => this.InvokeAsync(() => this.StateHasChanged()));
    }
}

Describe the processing when the change is made in Text2 in the Subscribe method. Calling the StateHasChanged method will re-render the component. This will allow re-rendering to occur when Text2 changes. ʻThe InvokeAsync method is used to call the StateHasChanged method from the thread in which the component is running. Calling the StateHasChanged` method on a different thread than the one the component is running on will result in a run-time error.

at the end

I created a simple MVVM pattern application based on Blazor.
I found that it is necessary to devise when reflecting the value from ViewModel to View.

Source code operated this time
https://github.com/ttlatex/BlazorMvvmTiny