In this chapter, we look at Blazor. Blazor is the new kid on the block, enabling full stack .NET. Blazor is a great piece of technology. It is still relatively new, but it improved remarkably between its experimental stage, first official release, and current state. In only a few years, it went from being an idea for the distant future to reality. Daniel Roth, a Principal Program Manager at Microsoft on the ASP.NET team, was most likely the most fervent believer who preached Blazor over that period. For a time, Blazor was the only thing I heard about (or maybe that was the internet spying on me).
Fun fact
Back in the day, we could use server-side JavaScript with classic ASP, making classic ASP the first full stack technology (that I know of).
UpdatePanel
with superpowers. SignalR enables real-time web features, like server code calling client code over WebSocket. This is known as the Blazor Server hosting model.A bit of history
If you don’t know what an UpdatePanel
is, you haven’t missed much. It was an ASP.NET Web Forms control released with .NET Framework 3.5 that helped run AJAX calls “automagically.”
Blazor also comes bundled with Razor components (why not Blazor components? I don’t know). It has some experimental projects revolving around it, a growing ecosystem of libraries accessible through NuGet, and a BlazorWebView
control in .NET Multi-platform App UI (MAUI).
Now that I have laid that out, the following topics are covered in this chapter:
Blazor Server is an ASP.NET Core web application that initially sends a page to the browser. Then, the browser updates part of the UI over a SignalR connection. The application becomes an automated AJAX client-server app on steroids. It is a mix of classic web apps and a SPA model, where the client loads the UI pieces to update from the server. So, less processing for the client and more processing for the server. There can also be a short delay (latency) since you must wait for a server response (steps 2 to 4); for example:
To make that diff (step 4), the server keeps a graph of the application state. It constructs that graph using components, which translates into Document Object Model (DOM) nodes.
Blazor Server makes stateful applications that must keep track of the current state of all visitors. It may be hard to scale up or would cost a lot of money in cloud hosting. I don’t want you to discard the option just yet; the model may fit your application’s needs. Moreover, paying more for hosting can save development costs, depending on many factors.
On the other hand, a Blazor Server application is smaller than a Blazor WebAssembly one because the server only sends the requested page to the browser instead of the whole source code compiled to Wasm binary code. Blazor Server can be used with clients that do not support Wasm since the code is executed on the server, like a classic ASP.NET Core application. Razor components can be used in both Blazor Server and Blazor WebAssembly; we explore them after the Blazor WebAssembly overview.
Blazor Server can also be used to prerender a Blazor WebAssembly app and speed up the initial load time.
Disclaimer
I have not deployed nor participated in building any Blazor Server applications yet. Nonetheless, it looks like an improved remake of Web Forms. That might just be me, but a “magic” SignalR connection, latency, and everything processed in a stateful server sound like going back to the past. I might be wrong. I recommend you do your experiments and research and judge for yourself. I may even change my mind in the future; the technology is still young.
To create a Blazor Server project, you can run the dotnet new blazorserver
command. That’s it for Blazor Server.
Next, we look into Blazor WebAssembly, which is way more promising (once again, my opinion).
Before getting into Blazor WebAssembly, let’s look at WebAssembly itself. WebAssembly allows browsers to run code that is not JavaScript (such as C# and C++). Wasm is an open standard, so it is not a Microsoft-only thing. Wasm runs in a sandboxed environment close to native speed (that’s the goal) on the client machine, enforcing browser security policies. Wasm binaries can interact with JavaScript.
As you may have “foreseen” from that last paragraph, Blazor WebAssembly is all about running .NET in the browser! And the coolest part is that it follows standards. It’s not like running VBScript in Internet Explorer (oh, I don’t miss that time). I think Microsoft’s new vision to embrace open standards, open source, and the rest of the world is very beneficial for us developers.
But how does that work? Like Blazor Server and other SPAs out there, we compose the application using components. A component is a piece of UI that can be as small as a button or as big as a page. Then, when a client requests our application, the following happens:
It is the same experience as any other web page so far. The difference is that when a user carries out an action, such as clicking a button, the action is executed by the client. Of course, the client can call a remote resource, as you would using JavaScript in React, Angular, or Vue. However, the important part here is that you don’t have to. You can control your user interface on the client using C# and .NET.
A significant advantage of Blazor Wasm is hosting: the compiled Blazor Wasm artifacts are only static resources, so you can host your web application in the cloud almost for free (provisioning Azure Blob storage and a Content Delivery Network (CDN), for example).
That leads to another advantage: scaling. Since each client runs the frontend, you don’t need to scale that part—only the delivery of static assets.
On the other hand, you can also use a server-side ASP.NET application to prerender your Blazor Wasm app if you prefer. That leads to a faster initial load time for the clients at an increased hosting cost.
Nevertheless, there is one significant disadvantage: it runs on .NET. But why would I have a problem with that? That’s blasphemy, right? Well, the browser must download the Wasm version of the .NET runtime, which is massive. Fortunately, the people at Microsoft worked on a way to trim unused parts, so browsers only download the required bits. Blazor also supports lazy-loading Wasm assemblies, so a client doesn’t need to download everything at once. That said, all in all, the minimum download size is still around 2 MB. With high-speed internet, 2 MB is small and fast to download, but it can take a bit longer for people living in a remote area. So, think about your audience before making a choice.
We can also leverage ahead-of-time (AOT) compilation to optimize performance-intensive applications while making the binaries bigger. So far, every version has improved the performance, size, and capabilities, which makes me hopeful to see Blazor shine in the future (at least for .NET developers).
To create a Blazor Wasm project, you can run the dotnet new blazorwasm
command.
Next, we explore Razor components and look at what Blazor has to offer.
Everything is a Razor component in Blazor Wasm, including the application itself, which is defined as a root component. In the Program.cs
file, that root component is registered as follows:
builder.RootComponents.Add<App>("#app");
The App
type is from the App.razor
component (we cover how components work later), and the string "#app"
is a CSS selector. The wwwroot/index.html
file contains a <div id="app">Loading...</div>
element that is replaced by the Blazor App
component once the application is initialized. #app
is the CSS selector identifying an element that has an id="app"
attribute. The wwwroot/index.html
static file is the default page served to clients; it is your Blazor app starting point. It contains the basic HTML structure of the page, including scripts and CSS. And that’s how a Blazor application is loaded.
The App.razor
file defines a Router
component that routes the requests to the right page. When the page exists, the Router
component renders the Found
child component. It displays the NotFound
child component when the page does not exist. Here is the default content of the App.razor
file:
<Router AppAssembly="@ typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Pages are Razor components, with a @page "/some-uri"
directive at the top, similar to Razor Pages. You can use most, if not all, of the same things as you can with Razor Pages to compose those routes.
A Razor component is a C# class that implements the IComponent
interface. You can also inherit from ComponentBase
, which implements the following interfaces for you: IComponent
, IHandleEvent
, and IHandleAfterRender
. All of these live in the Microsoft.AspNetCore.Components
namespace.
Next, we take a look at how to create Razor components.
You can create your components anywhere in your project, unlike Razor Pages and view components. I like to create my pages under the Pages
directory, so it is easier to find pages. Then you can create the non-page components wherever you see fit.
There are three ways to create a component:
You don’t have to pick only one way for the whole application; you can choose one per component. All three approaches end up compiled into a single C# class. Let’s take a look at those three ways of organizing components.
C#-only components are as simple as creating a class. In the following example, our component inherits from ComponentBase
, but we could implement only the interfaces we need.
Here is the first component (CSharpOnlyComponent.cs
):
namespace WASM
{
public class CSharpOnlyComponent : ComponentBase
{
[Parameter]
public string? Text { get; set; }
The Parameter
attribute allows setting the value of the Text
property when consuming the component. Officially, it becomes a component parameter. We see this in action once we are done with this class.
The BuildRenderTree
method, next, is responsible for rendering our component:
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "h1");
builder.AddAttribute(1, "class", "hello-world");
builder.AddContent(2, Text);
builder.CloseElement();
}
}
}
By overriding this method, we control the render tree. Those changes are eventually translated into DOM changes. Here, we are creating an H1
element with a hello-world
class. In it is a text node that contains the value of the Text
property.
Sequence numbers
The sequence numbers (0
, 1
, and 2
in the BuildRenderTree
method) are used internally to generate the diff tree that .NET uses to update the DOM. It is recommended to write those manually to avoid performance degradation in more complex code such as conditional logic blocks. See the ASP.NET Core Blazor advanced scenarios link in the Further reading section for more info.
Now, in the Pages/Index.razor
page, we can use that component like this:
@page "/"
<CSharpOnlyComponent Text="Hello World from C#" />
The name of the class becomes the name of its tag. That’s automatic; we have nothing to do to make it happen. We can set the value of the properties identified with the Parameter
attribute as HTML attributes. In this case, we set the value of the Text
property to Hello World from C#
. We could mark more than one property with that attribute and use them as we would any normal HTML attribute.
When rendering the page, our component is rendered to the following HTML:
<h1 class="hello-world">Hello World from C#</h1>
With those few lines of code, we created our first Razor component. Next, we will create a similar component using the Razor-only syntax.
Razor-only components are created in .razor
files. They are compiled into a C# class. The default namespace of that class depends on the directory structure where it is created. For example, a component created in the ./Dir/Dir2/MyComponent.razor
file generates the MyComponent
class in the [Root Namespace].Dir.Dir2
namespace. Let’s look at some code (RazorOnlyComponent.razor
):
<h2 class="hello-world">@Text</h2>
@code{
[Parameter]
public string? Text { get; set; }
}
If you like Razor, you probably prefer this one already. That listing is straightforward and allows us to code the same component as the previous section but more concisely and with less code. In the @code{}
block, we can add properties, fields, methods, and pretty much anything we would in a regular class, including other classes. We can also override ComponentBase
methods there if we need to. We can use this component the same way we did in the previous example; the same goes for parameters.
Next is the page consuming the RazorOnlyComponent
(Pages/Index.razor
):
@page "/"
<CSharpOnlyComponent Text="Hello World from C#" />
<RazorOnlyComponent Text="Hello World from Razor" />
The rendering is also very similar, but we went for an H2
instead of an H1
:
<h2 class="hello-world">Hello World from Razor</h2>
And with those few lines of code, we created our second component. Next, we create a hybrid of the two styles.
This third model can separate the C# code (known as code-behind) from the Razor code. This hybrid counterpart leverages partial classes to achieve the same as the other models and generates a C# class.
For this, we need two files:
[component name].razor
[component name].razor.cs
Let’s remake our previous component for the third time but render an H3
this time. Let’s begin with the Razor code (CodeBehindComponent.razor
):
<h3 class="hello-world">@Text</h3>
This code could barely be leaner; we have an H3
tag with the Text
property as its content. The .razor
file in this model replaces the BuildRenderTree
method. The compiler translates the Razor code into C#, generating the BuildRenderTree
method’s content for us.
The Text
parameter is defined in the following code-behind file (CodeBehindComponent.razor.cs
):
public partial class CodeBehindComponent
{
[Parameter]
public string? Text { get; set; }
}
It’s the same code as the previous two samples—we just divided it into two files. The key is the partial class
. It allows compiling a single class
from multiple files. In this case, there is our partial class
and the autogenerated one from the CodeBehindComponent.razor
file. We can use the CodeBehindComponent
in the same way as the other two.
Next is the page that consumes the CodeBehindComponent
(Pages/Index.razor
):
@page "/"
<CSharpOnlyComponent Text="Hello World from C#" />
<RazorOnlyComponent Text="Hello World from Razor" />
<CodeBehindComponent Text="Hello World from Code-Behind" />
That renders the same way as the others, but as an H3
with different content:
<h3 class="hello-world">Hello World from Code-Behind</h3>
Using code-behind can be very useful for two things:
.razor
file clean of C# codeThe tooling for .razor
files tends to explode on us from time to time, includes weird bugs, or provides half-support. It seems that handling HTML, C#, and Razor in a single file is not as easy as it sounds. On a more positive note, it is getting better, so I can only see more stable tooling in the future. I could see myself writing all the code of a component in a single .razor
file if the tooling was on par with the C# tooling (in many scenarios). That would lead to fewer files and closer proximity of all parts of the component (leading to better maintainability).
Next, we take a look at skinning our components with CSS, but with a twist…
Like other SPAs, Blazor allows us to create CSS styles scoped to a component. That means that we don’t have to worry about naming conflicts.
Unfortunately, this does not seem to work with C#-only components, so we will skin only two of the three components. Each of them has the same CSS class (hello-world
). We are about to change the text color by defining simple .hello-world
CSS selectors for both.
To achieve that, we must create a .razor.css
file named after our component. The following code represents the content of the RazorOnlyComponent.razor.css
file (RazorOnlyComponent
):
.hello-world {
color: red;
}
The following code represents the content of the CodeBehindComponent.razor.css
file (CodeBehindComponent
):
.hello-world {
color: aqua;
}
As you can see from those two files, they define the same .hello-world
selector with a different color.
In the wwwroot/index.html
file, the dotnet new blazorwasm
template added the following line:
<link href="[name of the project].styles.css" rel="stylesheet" />
That line links the bundled component-specific styles into the page. Yes, you did read bundled. The Blazor CSS isolation feature also bundles all those styles into a single .css
file, so the browser only loads one file.
If we load the page, we see this (without the layout):
Figure 18.1: Output after loading the page
So it worked! But how? Blazor autogenerated random attributes on each HTML element and used those in the generated CSS. Let’s first look at the HTML output:
<h1 class="hello-world">Hello World from C#</h1>
<h2 class="hello-world" b-cjkj1dpci4>Hello World from Razor</h2>
<h3 class="hello-world" b-0gygcymdih>Hello World from Code-Behind</h3>
Those two highlighted attributes are the “magic” links. Now, with the following CSS code, you should understand their usage and why they have been generated:
/* /CodeBehindComponent.razor.rz.scp.css */
.hello-world[b-0gygcymdih] {
color: aqua;
}
/* /RazorOnlyComponent.razor.rz.scp.css */
.hello-world[b-cjkj1dpci4] {
color: red;
}
If you are not too familiar with CSS, [...]
is an attribute selector. It allows you to do all kinds of things, including selecting an element with the specified attribute (as in this case). That’s what we need here.
The first selector means that all elements with the hello-world
class and an attribute named b-0gygcymdih
should have their color updated to aqua
. The second selector is the same, but for elements with an attribute named b-cjkj1dpci4
instead.
With that pattern in place, we can define component-scoped styles with a high level of confidence that they won’t conflict with other components’ styles.
Next, let’s explore the life cycle of those components.
The components, including the root components, must be rendered as DOM elements for the browser to display them. The same goes for any changes that subsequently occur. Two distinct phases compose the components’ life cycle:
During the first rendering, if we get rid of the duplicated sync/async methods, the life cycle of a Razor component looks like this:
Figure 18.2: Life cycle of a Razor component
SetParametersAsync
method is called.OnInitialized
method is called.OnInitializedAsync
method is called.OnParametersSet
method is called.OnParametersSetAsync
method is called.BuildRenderTree
method is called (the component is rendered).OnAfterRender(firstRender: true)
method is called.OnAfterRenderAsync(firstRender: true)
method is called.During re-rendering, if we get rid of the duplicated sync/async methods, the life cycle of a Razor component is leaner and looks like this:
Figure 18.3: Re-rendered version of a Razor component life cycle
ShouldRender
method is called. If it returns false
, the process stops here. If it is true
, the cycle continues.BuildRenderTree
method is called (the component is re-rendered).OnAfterRender(firstRender: false)
method is called.OnAfterRenderAsync(firstRender: false)
method is called.Note
Don’t worry if you have worked with Web Forms before and dread the Blazor life cycle’s complexity. It is leaner and doesn’t contain any postback. They are two different technologies. Microsoft is trying to push Blazor as the next logical step to migrate from Web Forms (which makes sense based on the current state of .NET), but the only significant similarity that I see is that the component model of Blazor is close to the control model of Web Forms. So if you moved away from Web Forms, don’t be afraid to look into Blazor; they are not the same — Blazor is all that Web Forms is not, with one similarity: they are both component-oriented.
I created a component named LifeCycleObserver
in the WASM
project (see https://adpg.link/ntqD). That component outputs its life cycle information to the console, leading to the following trick: Console.WriteLine
writes in the browser console, like this:
Figure 18.4: The browser debug console displaying the life cycle of the LifeCycleObserver component
Here is an example (highlighted) from the LifeCycleObserver
class:
public class LifeCycleObserver : ComponentBase
{
public override Task SetParametersAsync(ParameterView parameters)
{
Console.WriteLine("LifeCycleObserver.SetParametersAsync");
return base.SetParametersAsync(parameters);
}
// Omitted members
}
Next, we look at event handling and how to interact with our components.
So far, we’ve displayed the same component built using three different techniques. Now it is time to interact with a component and see how it works. There are multiple events in HTML that can be handled using JavaScript. In Blazor, we can handle most of them using C# instead.
I slightly modified the FetchData.razor
page component that comes with the generated project to show you two different event handler patterns:
Both ways call async
methods, but the same can be done with synchronous methods as well. Let’s now explore that code. I’ll skip some irrelevant markup, such as H1
and P
tags, to focus only on the real code, which starts with this:
@page "/fetchdata"
@inject HttpClient Http
At the top of the file, I left the injection of the HttpClient
and the @page
directive. These allow us to reach the page when navigating to the /fetchdata
URL and query resources over HTTP. The HttpClient
is configured in the Program.cs
file (the composition root). Then, I added a few buttons to interact with. Here is the first:
<button class="btn btn-primary mb-4" @onclick="RefreshAsync">Refresh</button>
All buttons of this code example have an @onclick
attribute. That attribute is used to react to click events, like the HTML onclick
attribute and the JavaScript "click"
EventListener
. That button delegates the click event to the RefreshAsync
method:
private Task RefreshAsync() => LoadWeatherAsync();
private async Task LoadWeatherAsync()
{
forecasts = await Http
.GetFromJsonAsync<WeatherForecast[]>(_uriList.Next());
}
The refresh method then calls the LoadWeatherAsync()
method, which in turn queries a resource returning an array of WeatherForecast
. The forecasts are three static JSON files located in the wwwroot/sample-data
directory. _uriList
is an instance of the Cycle
class that cycles through a series of strings. Its code is simple but helps simplify the rest of the page in an OOP manner:
private class Cycle
{
private int _currentIndex = -1;
private string[] _uris;
public Cycle(params string[] uris) => _uris = uris;
public string Next() => _uris[++_currentIndex % _uris.Length];
}
When the forecasts change (when we click on the Refresh button), the component is reloaded automatically, leading to an updated weather forecast table.
We can also access the event argument like we would in JavaScript. In the case of a click, we have access to the MouseEventArgs
instance associated with the event. Here is a quick sample displaying possible usage:
<button class="btn btn-primary mb-4" @onclick="DisplayXY">Display (X, Y)</button>
@code {
public void DisplayXY(MouseEventArgs e)
{
Console.WriteLine($"DOM(x, y): ({e.ClientX}, {e.ClientY}) | Button(x, y): ({e.OffsetX}, {e.OffsetY}) | Screen(x, y): ({e.ScreenX}, {e.ScreenY})");
}
}
In that code, the @onclick
attribute is used the same way as before, but the DisplayXY
method expects MouseEventArgs
as a parameter. The MouseEventArgs
argument is provided automatically by Blazor. Then, the method will output the mouse position in the browser’s DevTools console (F12 on Chromium-based browsers), which looks like this:
DOM(x, y): (921, 175) | Button(x, y): (119, 4) | Screen(x, y): (-999, 246)
DOM(x, y): (809, 197) | Button(x, y): (7, 26) | Screen(x, y): (-1111, 268)
To generate those coordinates, I clicked the top-right corner of the button, then the bottom-left corner. As we can deduce from the negative screen x position, my browser was on my left monitor.
Another possibility is to use lambda expressions as inline event handlers. Those lambda expressions can also call a method. Here is an example:
<button class="btn btn-primary mb-4" @onclick="@(e => Console.WriteLine($"DOM(x, y): ({e.ClientX}, {e.ClientY})"))">Lamdba (X, Y)</button>
That button outputs only the client (x, y) coordinate to improve readability.
That’s it for our overview of event handling. Next, we look into another way to manage the component’s state other than each component doing its own thing.
Unless you’ve never heard of React, you’ve most likely heard of Redux. Redux is a library that follows the Model-View-Update (MVU) pattern. MVU comes from The Elm Architecture. If you don’t know Elm, here is a quote from their documentation:
Elm is a functional language that compiles to JavaScript.
Next, let’s see what the goal behind MVU is.
The goal of MVU is to simplify the state management of applications. If you’ve built a stateful UI in the past, you probably know it can become hard to manage an application’s state. MVU takes the two-way binding complexity out of the equation and replaces it with a linear one-way flow. It also removes mutations from the picture by replacing them with immutable states, where state updates are moved to pure functions.
The MVU pattern is a unidirectional data flow that routes an action to an update function. An update function must be pure. A pure function is deterministic and has no side effects. A model is a state, which must be immutable. A state must have an initial state. A view is the code that knows how to display a state.
Depending on the technology, there are many synonyms. Let’s get into more details. It may sound confusing at first, but don’t worry, it’s not that bad.
An action is called a command or a request in MediatR. It is called an action in Redux, and a message in Elm. I will use action. An action is the equivalent of the commands that we used in our CQRS examples in Chapter 14, Mediator and CQRS Design Patterns. There is no notion of a query in MVU because a view always renders the current state.
Terminology |
MediatR |
Redux |
Elm |
Action |
Command Request |
Action |
Message |
An update function is called a handler in MediatR. It is called a reducer in Redux and an update in Elm. I will use reducer. The reducer is a pure function that always returns the same output for any given input (it’s deterministic). The pure function must have no impact on external actors (having no side effects). So, no mutation of external variables, no mutation of the input value: no side effects. One significant advantage of a pure function is testing. It is easy to assert the value of its output based on a given input since it is deterministic.
Terminology |
MediatR |
Redux |
Elm |
Reducer |
Handler |
Reducer |
Update |
A view is a component in React and Blazor and is a view function in Elm. I will mostly use view because component can be ambiguous and easily confused with Razor components, view components, or the plain notion of a UI component.
Terminology |
MediatR |
Redux |
Elm |
View |
Component |
Component |
View function |
A model or state cannot be altered and must be immutable. Every time a state changes, an altered copy of the state is created. The current state then becomes that copy. Elm calls the state a model; it is a state in Redux. We are using the term state as I find it defines the intent better.
Terminology |
Redux |
Elm |
State |
State |
Model |
Here is a diagram that represents this unidirectional data flow:
Figure 18.5: Unidirectional data flow chart
It may be hard to wrap your head around this at first. Like all new things, we must take the time to create new paths in our brains to get something fully. Don’t worry; we are about to see that in action.
All in all, it is straightforward; the flow goes only one way. Whenever the state changes, the component is re-rendered. Since states are immutable, we cannot alter them directly, so we must pass by reducers.
For this project, we will use an open source library that I created in 2020 while experimenting with C# 9 record classes. Since a record is immutable, it is a perfect candidate to represent an MVU state.
Moreover, it allows our example to be streamlined in a limited amount of space.
Note
There are multiple similar libraries, but they were all created before C# 9, so there’s no direct record support.
Context: We are building a counter page to increment and decrement a value.
I know it does not sound very exciting, but since many MVU libraries showcase one, as well as Blazor itself, I believe this is a good way to compare Blazor with others.
First, we need to install the library by loading the StateR.Blazor
NuGet package. In this case, we are using a prerelease version.
Redux DevTools
I also installed the StateR.Blazor.Experiments
NuGet package in the project. That project has a few experimental features, including a Redux DevTools connector. Redux DevTools is a browser extension that allows the tracking of states and actions. It also allows time travel between states.
Next, let’s code the Counter
feature (Features/Counter.cs
):
using StateR;
using StateR.Reducers;
namespace WASM.Features
{
public class Counter
{
public record State(int Count) : StateBase;
The State
record is our state. It exposes one init-only
property. It inherits from StateBase
, which is an empty record class. StateBase
serves as a generic constraint to make sure the state class is a record class so we can leverage the with
expression. States in StateR must be records; it is mandatory.
public class InitialState : IInitialState<State>
{
public State Value => new(0);
}
The InitialState
class, by implementing the IInitialState<State>
interface, represents the initial state of the State
record.
public record Increment : IAction;
public record Decrement : IAction;
Here, we declare two actions. They are records, but they could have been classes instead. Being a record is not a requirement but a shortcut to writing less code. In StateR, an action must implement the IAction
interface.
public class Reducers : IReducer<Increment, State>, IReducer<Decrement, State>
The Reducers
class implements the pure functions that handle the actions. In StateR, a reducer must implement the IReducer<TAction, TState>
interface. TAction
must be an IAction
, and TState
must be a StateBase
. The interface defines only a Reduce
method that inputs a TAction
and TState
and that outputs the updated TState
.
{
public State Reduce(Increment action, State state)
=> state with { Count = state.Count + 1 };
The Increment
reducer returns a copy of State
with its Count
incremented by 1.
public State Reduce(Decrement action, State state)
=> state with { Count = state.Count - 1 };
}
}
}
Finally, the Decrement
reducer returns a copy of State
with its Count
decremented by 1.
Using a with
expression like that makes very clean code, especially if the State
record has more than one property. Moreover, the record classes help enforce the immutability of the states, which is in line with the MVU pattern.
That is all that we need to cover the model (state) and the update (actions/reducers). Now to the view (component) portion. The view is the following Razor component (Features/CounterView.razor
):
@page "/mvu-counter"
@inherits StatorComponent
@inject IState<Counter.State> State
<h1>MVU Counter</h1>
<p>Current count: @State.Current.Count</p>
<button class="btn btn-primary" @onclick="() => DispatchAsync(new Counter.Increment())">+</button>
<button class="btn btn-primary" @onclick="() => DispatchAsync(new Counter.Decrement())">-</button>
There are only a few lines, but quite a lot of things to discuss here. First, the Razor component is accessible at the /mvu-counter
URL.
Then, it inherits from StatorComponent
. This is not required, but it is convenient. The StatorComponent
class implements a few things for us, including managing the component’s re-rendering when an IState<TState>
property changes.
That leads to the next line, the injection of an IState<Counter.State>
interface implementation accessible through the State
property. That interface wraps the TState
instance and gives access to the current state through its Current
property. The @inject
directive enables property injection in Razor components.
Next, we display the page. @State.Current.Count
represents the current count. Following that are two buttons. Both have an @onclick
attribute that calls a lambda expression that represents the action to be executed when a user clicks the button. The DispatchAsync
method comes from StatorComponent
. As its name implies, it dispatches actions through the StateR pipeline. It is similar to the MediatR Send
and Publish
methods.
Each button dispatches a different action; one is Counter.Increment
and the other is Counter.Decrement
. StateR knows the reducers and sends the action to the appropriate reducers.
That code creates a centralized state and uses the MVU pattern to manage it. If we need Counter.State
elsewhere, we only need to inject it, as we did here, and the same state would be shared between multiple components or classes. In this example, we injected the state in a Razor component, but we could also use the same pattern in any code.
One more thing: we need to initialize StateR. To do that, in the Program.cs
file, we need to register it like this:
using StateR;
using StateR.Blazor.ReduxDevTools; // Optional
// ...
builder.Services
.AddStateR(typeof(Program).Assembly)
.AddReduxDevTools() // Optional
.Apply()
;
The builder.Services
property is an IServiceCollection
. The AddStateR
method creates an IStatorBuilder
and registers StateR’s static dependencies.
Then, the optional AddReduxDevTools
method call links StateR to the Redux DevTools browser plugin that I mentioned previously. That helps to debug applications from the browser. Other optional mechanisms can be added here. A developer could also code their own extensions to add missing or project-specific features. StateR is DI-based.
Finally, the Apply
method initializes StateR by scanning the specified assemblies for every type that it can handle. In this case, we are scanning only the Wasm application assembly (highlighted). The initialization is a two-stage process, completed by the Apply
method call.
With that in place, we can run the application and play with our counter. I hope that you liked this little piece of Redux/MVU with StateR. If you did, feel free to use it. If you find missing features, bugs, or performance issues, or want to share your ideas, feel free to open an issue on GitHub (https://adpg.link/Z7Ej).
The MVU pattern uses a model to represent the current state of the application. The view renders that model. To update the model, an action is dispatched to a pure function (a reducer) that returns the new state. That change triggers the view to re-render.
The unidirectional flow of MVU reduces state management’s complexity. Having all state changes flowing in the same direction makes it easier to monitor, trace, and debug data flow errors.
Now let’s see how the MVU pattern can help us follow the SOLID principles:
Next, we take a quick peek at other Blazor information to give you an idea of what is available if you want to get started.
Your Blazor journey has just begun, and there are so many more features to Blazor than what we covered. Here are a few more possibilities to give you a glimpse of the options.
You can integrate Razor components with MVC and Razor Pages using the Component
Tag Helper. When doing so, you can also prerender your applications (the App
component) by setting the render-mode
attribute to Static
, leading to a faster initial render time. Prerendering can also be used to improve search engine optimization (SEO) and the initial load time of the page. The “drawback” is the need for an ASP.NET Core server to execute the prerendering logic.
Another lovely thing about full-stack C# is sharing code between the client and the server. Say we have a web API and a Blazor Wasm application; we could create a third project, a class library, and share the DTOs (API contracts) between the two.
In our component, we can also allow arbitrary HTML between the opening and closing tags by adding a RenderFragment
parameter named ChildContent
to that component. We can also catch arbitrary parameters and splat them on an HTML element of the component. Attribute splatting in Blazor means accepting multiple parameters in a single Dictionary<string, object>
property and splitting them into multiple HTML properties during rendering. In the following code, we capture any non-specified attributes (CaptureUnmatchedValues = true
) and render them as HTML attributes. Here is an example combining those two features (Card.razor
):
<div class="@($"card {Class}")" @attributes="Attributes">
<div class="card-body">
@ChildContent
</div>
</div>
@code{
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> Attributes { get; set; } = new()
[Parameter]
public string? Class { get; set; }
}
The Card
component renders a Bootstrap card and allows consumers to set any attributes they want on it. The content between the <Card>
and </Card>
tags can be anything. That content is rendered inside the div.card-body
. The highlighted lines represent that child content.
The Class
parameter is a workaround to allow consumers to add CSS classes while enforcing the card
class’s presence. The Attributes
parameter becomes a catch-all by setting the CaptureUnmatchedValues
property of the Parameter
attribute to true
.
Next is an example that consumes the Card
component (Pages/Index.razor
):
<Card style="width: 25%;" class="mt-4">
<h5 class="card-title">Card title</h5>
<h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="card-link">Card link</a>
<a href="#" class="card-link">Another link</a>
</Card>
We can see that the Card
component (the highlighted lines) is filled with arbitrary HTML (from the official Bootstrap documentation). There are two attributes specified as well, a style
and a class
.
Here is the rendered result:
<div class="card mt-4" style="width: 25%;">
<div class="card-body">
<h5 class="card-title">Card title</h5>
<h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="card-link">Card link</a>
<a href="#" class="card-link">Another link</a>
</div>
</div>
The highlighted lines represent the Card
component. Everything else is the ChildContent
. We can also notice how the attribute splatting added the style
attribute. The Class
attribute appended the mt-4
class to card
. Here is what it looks like in a browser:
Figure 18.6: The Card component rendered in a browser
The Virtualize
component allows the number of rendered items to be reduced to just those visible on the screen. You can also control the number of offscreen elements that are rendered to reduce the frequency at which elements are rendered while scrolling. I’ve left a link to the ASP.NET Core Blazor component virtualization documentation page in the Further reading section, at the end of the chapter.
As we saw in the counter project, Blazor has full support for dependency injection. For me, that’s a requirement. That’s also why I learned Angular 2 when it came out and not React or Vue. Blazor’s DI support is way better than all of the JavaScript IoC containers that I have seen so far, so this is a major benefit.
There are many other built-in features in Blazor, including an EditForm
component, validation support, and a ValidationSummary
component, as you’d expect in any MVC or Razor Pages application, but client-side.
Quick tip
Should you ever need to force the rendering of a component, you can call the StateHasChanged
method from ComponentBase
.
As mentioned earlier in this chapter, .NET code can interact with JavaScript and vice versa. To execute JavaScript code from C#, inject and use the IJSRuntime
interface. To execute C# code from JavaScript, use the DotNet.invokeMethod
or DotNet.invokeMethodAsync
functions. The C# method must be public static
and decorated with the JSInvokable
attribute. There are multiple other ways in which C# and JavaScript can interact, including non-static methods. By supporting this, developers can build wrappers around JavaScript libraries or use JavaScript libraries as is. It also means that we can implement features that Blazor does not support in JavaScript or even write browser-optimized code in JavaScript if Blazor is slower in one area or another. This also allows tapping into browsers’ APIs, like the Fetch API, Storage API, and Canvas API.
You can even write 2D and 3D games using a JavaScript wrapper around a canvas (such as BlazorCanvas) or a full-fledged game engine such as WaveEngine.
The last bit of additional information that I can think of is an experimental project named Blazor Mobile Bindings. That project is a Microsoft experiment that allows Blazor to run in a phone app. It allows native performance by wrapping Xamarin.Forms controls with Razor components. It also supports loading Blazor Wasm in a WebView
control, allowing for better reusability between mobile and web apps, but at a performance cost.
I’ve left a long list of links in the Further reading section to complement this chapter’s information.
Blazor is a great new piece of technology that could bring C# and .NET to a whole new level. In its present state, it is good enough to develop apps with. There are two main models; Server and WebAssembly.
Blazor Server links the client with the server over a SignalR connection, allowing the server to push updates to the client whenever needed (such as when a user carries out an action). Blazor WebAssembly is a .NET SPA framework that compiles C# to WebAssembly using AOT compilation or sends the Intermediate Language (IL) code to the browser where a .NET interpreter implemented in WebAssembly interprets that code. That allows .NET code to run in the browser. We can interact with JavaScript using IJSRuntime
and vice versa.
Blazor is component-based, meaning that every piece of UI in Blazor is a component, including pages. We explored three ways to create components: C#-only, Razor-only, and a hybrid that combines C# and Razor in two different files. A component can also have its own isolated CSS without the need to worry about conflicts.
We explored the life cycle of a Razor component, which is very simple yet powerful. We also took a look at handling events and how to react to them.
We then dug into the MVU pattern, which is very well suited for stateful user interfaces like Blazor. We used an open source library and leveraged C# 9.0 record classes to implement a basic example.
Finally, we took a look at the other possibilities that Blazor has to offer.
I will close this chapter with a personal opinion. I would like to see a Blazor-like model become the unified way to build user interfaces in .NET. I appreciate writing Razor way more than writing XAML, to name only one other way of writing UI code.
Let’s take a look at a few practice questions:
Here are a few links to build upon what we have learned in the chapter:
The last time I did 2D/3D development was back when XNA was a thing. I also used Ogre3D in C++ for a school project. That said, I hinted about 2D and 3D games in the chapter, so here are a few resources that I found for those of you who are interested:
This may be the end of the book, but it is also the beginning of your journey into software architecture and design. No matter who you are, I hope you found this to be a refreshing view of design patterns and how to design SOLID web apps.
Depending on your goal and current situation, you may want to explore one or more application-scale design patterns in more depth, start your next personal project, start a business, apply for a new job, or all of those at the same time. No matter your goal, keep in mind that designing software is technical but also an art. There is rarely one right way of implementing a feature, but multiple acceptable ways of doing so. Experience is your best friend, so keep programming, learn from your mistakes, and move forward. Remember that we are all born knowing next to nothing, so not knowing something is expected; we need to learn. Please ask your teammates questions, learn from them, and share your knowledge with others.
Now that this book is complete, I’ll get back to writing blog posts, so you can always learn new things there (https://adpg.link/blog). Feel free to hit me up on social media, such as Twitter @CarlHugoM
(https://adpg.link/twit). I hope you found the book educational and approachable and that you learned many things. I wish you success in your career.
3.145.96.102