Single Page Applications
SPA applications update specific portions of the user interface without requiring full page reloads. These applications leverage JavaScript to manipulate the browser's Document Object Model (DOM), which typically consists of fixed UI elements and placeholder containers that get populated based on user interactions. A key advantage of SPAs is their ability to maintain application state in memory, similar to desktop applications. This chapter demonstrates SPA development using Blazor.
Layout Components
Every web application contains recurring UI elements like headers, footers, navigation menus, and copyright notices that appear across multiple pages. Duplicating these elements in each page would create maintenance overhead, requiring updates across numerous files whenever changes are needed. Development frameworks address this through layout mechanisms. For instance, ASP.NET WebForms uses master pages, while ASP.NET MVC employs layout views. Blazor implements this functionality through layout components.
Implementing Blazor Layout Components
Layout components in Blazor are standard components with one key requirement: they must inherit from the LayoutComponentBase class. This base class extends ComponentBase and adds a Body property of type RenderFragment, which serves as the insertion point for child content.
namespace Microsoft.AspNetCore.Components
{
public abstract class LayoutComponentBase : ComponentBase
{
[Parameter]
public RenderFragment? Body { get; set; }
}
}
Consider the MainLayout.razor component from the sample solution:
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank"
class="ml-md-auto">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
</div>
The first line declares inheritance from LayoutComponentBase. The @Body directive endicates where nested components will render within the layout structure.
Configuring Default Layouts
Components can specify their own layouts, or an application can define a default layout used by all components that don't explicitly declare one. The App.razor file configures this behavior:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
The RouteView component's DefaultLayout property sets the application-wide layout. When no matching route is found, the LayoutView displays an error message using the specified layout.
Selecting Layout Components
Individual components can override the default layout using the @layout directive. For example:
@page "/counter"
@layout MainLayoutRight
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
To apply a layout to multiple components within a folder, create an _Imports.razor file containing the layout directive.
Nested Layouts
Layout components can be nested to create hierarchical UI structures. A nested layout inherits from LayoutComponentBase and specifies another layout using the @layout directive:
@inherits LayoutComponentBase
@layout MainLayout
<div class="paper">
@Body
</div>
Blazor Routing
SPAs use routing to determine which component should populate the layout's Body property. The routing system matches browser URIs against route templates defined using the @page directive.
Router Setup
The Router component in App.razor manages application routing:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(ErrorLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Navigation Menu Component
The NavMenu component provides navigation links using NavLink components:
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span>
Home
</NavLink>
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span>
Counter
</NavLink>
Route Templates
Componetns define route templates using the @page directive. These templates can include parameters:
@page "/counter"
@page "/counter/{currentCount:int?}"
@layout MainLayoutRight
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
[Parameter]
public int CurrentCount { get; set; }
private void IncrementCount()
{
CurrentCount++;
}
}
Programmatic Navigation
Components can navigate programmatically using the NavigationManager:
@page "/counter"
@page "/counter/{currentCount:int?}"
@layout MainLayoutRight
@inject NavigationManager navigationManager
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<button class="btn btn-primary" @onclick="StartFrom50">Start from 50</button>
@code {
[Parameter]
public int CurrentCount { get; set; }
private void IncrementCount()
{
CurrentCount++;
}
private void StartFrom50()
{
navigationManager.NavigateTo("/counter/50");
}
}
Base URI Configuration
Blazor applications use the <base> HTML element to handle deployment paths:
<head>
<meta charset="utf-8" />
<title>SPA</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
</head>
Lazy Loading with Routing
For large applications, lazy loading defers component library downloads until they're needed.
Lazy Loading Component Libraries
To enable lazy loading, mark assemblies in the project file:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="LazyLoading.Library.dll" />
</ItemGroup>
</Project>
Dynamic Assembly Loading
Use the LazyAssemblyLoader service to load assemblies on demand:
@using System.Reflection
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader assemblyLoader
<Router AppAssembly="@typeof(Program).Assembly"
AdditionalAssemblies="@additionalAssemblies"
OnNavigateAsync="OnNavigate">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
<Navigating>
Loading additional components...
</Navigating>
</Router>
@code {
private List<Assembly> additionalAssemblies = new List<Assembly>();
private async Task OnNavigate(NavigationContext context)
{
if(context.Path == "counter")
{
var assembliesToLoad = new List<string> { "LazyLoading.Library.dll" };
var assemblies = await assemblyLoader.LoadAssembliesAsync(assembliesToLoad);
additionalAssemblies.AddRange(assemblies);
}
}
}
Adding Detail Pages to PizzaPlace
To implement component communication, we'll add a pizza detail page to the PizzaPlace application. Information can be passed between components through URI parameters, data binding, or shared state objects.
First, modify the State class to include a CurrentPizza property:
public class State
{
public Menu Menu { get; } = new Menu();
public ShoppingBasket Basket { get; } = new ShoppingBasket();
public UI UI { get; set; } = new UI();
public Pizza? CurrentPizza { get; set; }
public decimal TotalPrice => Basket.Orders.Sum(id => Menu.GetPizza(id)!.Price);
}
Update the PizzaItem component to include a navigation link:
<div class="row">
<div class="col">
@if (ShowPizzaInformation is not null)
{
<a href="" @onclick="@(() => ShowPizzaInformation?.Invoke(Pizza))">
@Pizza.Name
</a>
}
else
{
@Pizza.Name
}
</div>
<div class="col text-right">
@($"{Pizza.Price:0.00}")
</div>
</div>
Create a PizzaInfo component to display detailed information:
@page "/PizzaInfo"
<h2>Pizza @CurrentPizza.Name Details</h2>
<div class="row">
<div class="col">
@CurrentPizza.Name
</div>
<div class="col">
@CurrentPizza.Price
</div>
<div class="col">
<img src="@SpicinessImage(CurrentPizza.Spiciness)"
alt="@CurrentPizza.Spiciness" />
</div>
</div>
<div class="row">
<div class="col">
<a class="btn btn-primary" href="/">Back to Menu</a>
</div>
</div>
@code {
[Inject]
public State State { get; set; } = default!;
public Pizza CurrentPizza => State.CurrentPizza!;
private string SpicinessImage(Spiciness spiciness)
=> $"images/{spiciness.ToString().ToLower()}.png";
}