Building Single Page Applications with Blazor Routing

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";
}

Tags: Blazor Routing SPA WebAssembly csharp

Posted on Fri, 28 Aug 2026 16:23:49 +0000 by MrBiz