Routing is a key feature of any single-page application. It allows the developer to arrange the website and the user to navigate different pages.
Whenever we use the @page
directive on a Blazor component, the compiler will add the RouteAttribute
during compilation, making it available as a route.
It’s what differentiates regular components that are referenced within other Blazor components from routable components, usually referenced as Blazor pages.
When creating a Blazor project using Visual Studio 2022, you’ll get the following code in the App.razor
file:
<Router AppAssembly="@typeof(App).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 role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
The Router component is from the Microsoft.AspNetCore.Components.Routing namespace and requires a reference to the application assembly.
At startup, the router reads the referenced application assembly and looks for RouteAttribute
applied to compiled Blazor component classes.
A route can either be found or not found. In case it’s found, we use the RouteView
component to render the view connected with the routing information.
We also specify a default layout component. The default layout is used for all page components that do not explicitly specify a layout using the @layout
directive.
If the requested route cannot be found, we display a simple error using a trivial HTML snippet. We could customize and improve the error message if a page is not found here, but that’s beyond the focus of routing and navigation fundamentals.
The FocusOnNavigate
component allows you to focus on an HTML element after the navigation has been completed. We can use any CSS selector to define what element to focus on in navigation. The default template
uses the h1
selector, which is a good default that helps screen readers understand that the page has been navigated.
Important: Routing in Blazor requires that we set the base
tag in the head section of the HTML. However, using the default template, it is correctly set in the _Host.cshtml
file. Make sure to adjust this value depending on where and how you host your application in production.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- code omitted -->
<base href="~/" />
<!-- code omitted -->
</head>
Hint: The router acts the same for Blazor Server, as well as Blazor WebAssembly applications.
Static routes are the foundation, but dynamic routes are what we need in a real application. Dynamic routes contain parameters to pass data during page navigation.
Consider an example where you have the following Library
page component:
@page "/library"
<h3>Library</h3>
<ul>
<li><a href="/book/1">My first book</a></li>
<li><a href="/book/2">My second book</a></li>
<li><a href="/book/3">My third book</a></li>
</ul>
We define a list of three books. Every book is a link to a page. We provide the id of the book as the route parameter.
Now let’s take a look at the Book
page component.
@page "/book/{id}"
<h3>Book @Id</h3>
@code {
[Parameter]
public string Id { get; set; }
}
First, we use the @page
directive to define the route. We use /book
as the static part of the route and use curly braces to define the id parameter as a placeholder.
In the code section, we define a property of type string
and name it the same as the placeholder in the route. Routes are case-insensitive.
We can use the Id
property in the HTML template or the C# code like every other property. It will be populated on navigation. In this example, we render the id as part of the h3
tag.
In the previous example, we defined the id route parameter of type string
. It’s the default because whenever we define a URL, it is a string
and can contain any type
of route parameter.
In our example, we use the id of the book as the route parameter. It would proably be better to use int
instead of string
as the type for the Id
property.
If we only change the type of the parameter, we’ll get an error during navigation because the Router could not pass a string
argument to an int
property.
However, we can specify the type of a route parameter with the following syntax:
@page "/book/{id:int}"
We use a colon followed by the type we want to use for the route parameter.
The whole Book
component now looks like this:
@page "/book/{id:int}"
<h3>Book @Id</h3>
@code {
[Parameter]
public int Id { get; set; }
}
I suggest using the correct type for route parameters. It allows for cleaner code within the page implementation. For example, we do not need to manually parse the id from a string
in
our example. It saves a few extra lines of code.
The less code we write, the less potential for introducing bugs we have.
So far, we used links to navigate between pages. The NavigationManager
allows us to navigate from one page to another within the C# interaction code.
Let’s quickly change the previous example to navigate by clicking on a li
element instead of using the anchor tag.
@page "/library"
@inject NavigationManager Navigation
<h3>Library</h3>
<ul>
<li @onclick="() => NavigateToBook(1)">My first book</li>
<li @onclick="() => NavigateToBook(2)">My second book</li>
<li @onclick="() => NavigateToBook(3)">My third book</li>
</ul>
@code {
public void NavigateToBook(int id)
{
Navigation.NavigateTo($"book/{id}");
}
}
We removed the a
tag from within the li
element of the book list. Instead, we added the @onclick
event to the li
tag. We
then call the NavigateToBook
event and provide the id of the book as its argument.
The NavigateToBook
method defined within the code section of the component uses the Navigation
property injected using the @inject
directive on top of the
component.
When navigating using the NavigationManager
, we can provide different NavigationOptions
. For example, we can bypass client-side routing and force the browser to load the new page from the
server using the ForceLoad
property. Further, we can replace the history entry using the ReplaceHistoryEntry
property.
Hint: Besides initiating navigation, we can also listen to the LocationChanged
event for route changes and execute custom code when navigating on a specific page.
Besides working with URL parameters using the Parameter attribute, Blazor also offers a simple API to read query strings.
A query string is a URL that looks like this:
/search?firstname=Claudio&lastname=Bernasconi
To let a page component read a parameter from a query string, we need to use the SupplyParameterFromQuery
attribute and the Parameter
attribute.
Consider the following Search
page component code:
@page "/search"
<h3>Search</h3>
<p>Search intent: First name: @FirstName Last name: @LastName</p>
@code {
[Parameter]
[SupplyParameterFromQuery]
public string FirstName { get; set; }
[Parameter]
[SupplyParameterFromQuery]
public string LastName { get; set; }
}
We define two properties of type string
and apply both the Parameter
and the SupplyParameterFromQuery
attributes.
In the template code, we simply render the content of both properties on screen.
When we call the URL /search?firstname=Claudio&lastname=Bernasconi
, Blazor provides both properties with their respective portion of the query string. It’s a simple and very powerful solution.
If you want to have a different name for the property than in the URL, you can use the Name
property of the SupplyParameterFromQuery
attribute to define the name in the URL.
Sometimes loading a page can take a long time. We can use the Navigating
component within the Router
component to show custom markup during page transition.
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<!-- Code omitted -->
</Found>
<NotFound>
<!-- Code omitted -->
</NotFound>
<Navigating>
<p>Page is loading...</p>
</Navigating>
</Router>
I generally keep page navigations as short as possible. For example, I load data asynchronously, making the page load a lot faster (initially without data).
The NavLink
component can be used instead of a simple HTML anchor tag. It automatically applies an “active” CSS class based on whether its URL matches the current URL.
The NavMenu
component generated by the Visual Studio 2022 project templates, contains an example of how to use the NavLink
component:
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
</nav>
It uses the NavLink
component and provide the default CSS class applied to all links. In this case, it’s “nav-link”. The link is provided to the href
property.
Besides specifying the matching behavior using the NavLinkMatch
enumeration, we can also provide a custom CSS class for the active NavLink
using the ActiveClass
property. The default value is “active”.
Routing and navigation are fundamentals when working with single-page applications. Blazor provides a simple yet powerful implementation of both out of the box.
We don’t have to select one of the multiple options for routing; it’s all included in Blazor. Still, it provides a lot of room for custom code and is very flexible and extensible. It not only handles simple use cases but also provides APIs for advanced scenarios.
It contributes to Blazor, allowing for the rapid development of modern single-page web applications.
If you want to learn more about Blazor development, consider reading other articles of the Blazor Basics series, or watch the FREE Blazor Crash Course on YouTube.
Claudio Bernasconi is a passionate software engineer and content creator writing articles and running a .NET developer YouTube channel. He has more than 10 years of experience as a .NET developer and loves sharing his knowledge about Blazor and other .NET topics with the community.