Skip to content
Home » Blazor – My Portfolio – Part 6 – Navigation

Blazor – My Portfolio – Part 6 – Navigation

Spread the love

In the previous part of the series we provided data for our application. We can view all our projects grouped by category in the homepage. But what if we wanted to see just the projects in one specific category? Or what if we wanted to view just the projects where a specific technology is used? Or, finally, what if we wanted to view the details of one particular project? All these things are possible and even pretty simple to implement if you know how routing and navigation work in Blazor.

You can grab all the code and assets from Github.

Routing

We already mentioned that routing is used to map URLs to pages (the latter being components with the @page directive, aka independent components). The route is the string that follows the @page directive. Let’s have a look at some routes that are already there.

If you open the Index.razor page, the route consists of just the slash.

@page "/"

There always must be a slash at the beginning of a route. If there is nothing after it, it means that you will go to the Index page if you don’t type any URL:

Now have a look at the ProjectDetails component. There we have the following route:

@page "/ProjectDetails"

This means you have to use the /ProjectDetails URL to go to this independent component:

Now try the same URL but with all lowercase characters:

Turns out it’s case-insensitive. I’m going to turn the routes in all my pages to all-lowercase, and separate the words by hyphens, but this isn’t strictly necessary. Here are all the changes I’ve made:

Component (page)Old routeNew route
ProjectDetails/ProjectDetails/project-details
ProjectsByCategory/ProjectsByCategory/projects-by-category
ProjectsByTech/ProjectsByTech/projects-by-tech

At this moment the /project-details route will always take you to the details page of one particular, hard-coded project. But the whole idea behind details pages is that you want to view the details of the project you clicked on in the Index page, not only one and the same project no matter which project you click. This is where parameterized routes come in handy.

Parameterized Routes

To inform the ProjectDetails page which project’s details to display, we use a parameter in the route. The parameter is then mapped to a property in the component you are taken to. This is how data can be transferred via the URL. Parameters are placed inside curly braces.

The parameters can be of a couple different types, like string, int, bool, float, double, and some more. If it’s a string, you don’t have to specify the type in the route, like for example:

@page "/product/{productName}"

This example doesn’t come from our app, of course. If the parameter is of any other type, you have to specify the type after a colon, for example:

@page "/product/{productPrice:decimal}"

Or, in our app, we’ll pass the project’s id to the URL, which is an integer:

@page "/project-details/{ProjectId:int}"

This parameter identifier and type must match the identifier and type of the parameter in the component. In our case, the identifier is ProjectId and the type is int. So, we have to define the following parameter in the code section:

@page "/project-details/{ProjectId:int}"

<h1 class="mb-5">Forest Monsters</h1>
...

@code {
    [Parameter]
    public int ProjectId { get; set; }
}

In the code above you can clearly see how these two match.

Now we have to use the id in the ProjectDetails page to access the appropriate project and display its details instead of the hardcoded ones.

Just like we did with the data for all projects, let’s retrieve the data for one project by its id from the data.json file. Here’s the code section of the ProjectDetails page:

@page "/project-details/{ProjectId:int}"
...
@code {
    [Inject]
    HttpClient Http { get; set; }

    [Parameter]
    public int ProjectId { get; set; }

    public DataModel? Data { get; set; }
    public ProjectModel? SelectedProject { get; set; }
    public List<CategoryModel>? Categories { get; set; }
    public List<TechModel>? Techs { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

        Categories = Data.Categories;

        Techs = Data.Techs;

        SelectedProject = (from project in Data.Projects
                          where project.Id == ProjectId
                          join category in Categories
                          on project.Category equals category.Id
                          select new ProjectModel
                              {
                                  Id = project.Id,
                                  Name = project.Name,
                                  Description = project.Description,
                                  ImageUrl = project.ImageUrl,
                                  Category = category,
                                  Techs = (from tech in Techs
                                           where project.Techs.Contains(tech.Id)
                                           select tech).ToList(),
                                  Links = project.Links
                              }).SingleOrDefault();                          

    }
}

This time we get a single project instead of a list of projects. We use where to filter the projects and access just the right one. Then we create a new instance of ProjectModel and populate it with data. The single project we’re interested in is saved in the SelectedProject property and now we can use it in the Razor section. We’ll also add conditional rendering in case there is no project loaded yet. Here’s the Razor section of the ProjectDetails component:

@page "/project-details/{ProjectId:int}"
@using MyPortfolio.Models;

@if (SelectedProject == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <h1 class="mb-5">@SelectedProject.Name</h1>
    <div class="row">
        <div class="col-lg-6 mb-4">
            <img class="img-fluid" src="@SelectedProject.ImageUrl" />
        </div>
        <div class="col-lg-6">
            <p>@SelectedProject.Description</p>

            <h5 class="mb-4">Technologies used in this project:</h5>

            <div class="techs">
                @foreach (var tech in SelectedProject.Techs)
                {
                    <div class="tech">
                        <i class="@tech.Icon" />
                        @tech.Name
                    </div>
                }
            </div>

            <h5 class="mb-4">Useful links related to this project:</h5>

            <div>
                @foreach (var link in SelectedProject.Links)
                {
                    <div class="link">
                        <a href="@link.Destination" target="_blank">
                            <span><i class="@link.Icon" />@link.DisplayText</span>
                        </a>
                    </div>
                }
            </div>
        </div>
    </div>
}

@code {
    ...
}

Now run the app and use the following URL: /project-details/16 (A). This will take you to the details page for the project with id 16, which happens to be my book that I published in 2021 (B). If you click on one of the links (C), they will open in new tabs.

For example if you click the YouTube link, my video about the book will open in a new tab:

This is great, but you don’t want to enter the URL by hand, do you? Besides, you don’t want to memorize all the project ids so that you can navigate to the project you’re interested in. It would be much easier if you could just click on a project card and be taken to the corresponding details page. This is what we are going to implement right now.

Anchor Tags

Creating links in Razor is straightforward. You just use the regular anchor tag (<a>) and set the href attribute to where you want to navigate. We already did it in the ProjectDetails page. Have a look:

@page "/project-details/{ProjectId:int}"
...
            <div>
                @foreach (var link in SelectedProject.Links)
                {
                    <div class="link">
                        <a href="@link.Destination" target="_blank">
                            <span><i class="@link.Icon" />@link.DisplayText</span>
                        </a>
                    </div>
                }
            </div>
        ...

Now let’s do the same in the Project component. Actually, the whole project card is a link because it’s between the opening and closing anchor tags:

@using MyPortfolio.Models;
<a href="">
    <div class="card">
        ...
    </div>
</a>

@code {
    ...
}

All we have to do is set href to the URL:

@using MyPortfolio.Models;
<a href="/project-details/@ProjectData.Id">
    ...

Now run the app and click on one of the projects. This is where I ended up after clicking the Slugrace project in the Games category:

If you now want to go back, you can just click on the back arrow in the top-left corner of your browser window. If you want to go to the homepage from any page in your app, you can just click on Home near the top of the sidebar. You will notice that when you do that, not only do you navigate to the homepage, but also the link is highlighted:

This is how a NavLink works.

NavLink

We used <a> tags for external links. You can use them for navigating between pages too, but then you have to handle the navigation logic yourself. NavLink provides built-in navigation handling. As you just saw, it takes care of highlighting the current page in a menu. This provides more intuitive user experience. This is why we’re using NavLinks for the categories and technologies in the sidebar. Whenever you select a category or technology, you will be taken to the appropriate page where only projects in that category or using that technology will be displayed.

We already have the NavLinks in the CategoriesMenu and TechsMenu components. Here’s how they’re implemented in the former:

@using MyPortfolio.Models;
@inject HttpClient Http

<div class="sidebar-menu">
    ...
            @foreach (var category in Categories)
            {
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="">
                        <div class="sidebar-item">
                            <i class="@category.Icon" />
                            <p>@category.Name</p>
                        </div>
                    </NavLink>
                </div>
            }
        ...

The only thing that is missing is the value of the href attribute. Let’s implement filtering projects by category and technology first and then set the attribute accordingly. 

Filtering Projects by Category and Technology

We already have the ProjectsByCategory and ProjectsByTech components with some basic implementation, but we have to rewrite them. Let’s start with the former. Here’s the ProjectsByCategory.razor file:

@page "/projects-by-category/{CategoryId:int}"
@using MyPortfolio.Models;

@if (Projects == null)
{
    <p><em>Loading...</em></p>
}
else if (Projects.Count() == 0)
{
    <p><em>There are no projects in this category.</em></p>
}
else
{
    <h3>Category: @Category.Name (@Projects.Count())</h3>

    @if(Projects.Count() > 0)
    {
        <div class="row mt-3">
            <ProjectsDisplay Projects="@Projects" />
        </div>
    }
}

@code {
    [Inject]
    HttpClient Http { get; set; }

    [Parameter]
    public int CategoryId { get; set; }

    public List<ProjectModel>? Projects { get; set; }
    public List<CategoryModel>? Categories { get; set; }
    public CategoryModel? Category { get; set; }
    public List<TechModel>? Techs { get; set; }
    public DataModel? Data { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        Data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

        Categories = Data.Categories;

        Category = Categories.FirstOrDefault(c => c.Id == CategoryId);

        Techs = Data.Techs;

        Projects = (from project in Data.Projects
                    where project.Category == CategoryId
                    select new ProjectModel
                        {
                            Id = project.Id,
                            Name = project.Name,
                            Description = project.Description,
                            ImageUrl = project.ImageUrl,
                            Category = Category,
                            Techs = (from tech in Techs
                                     where project.Techs.Contains(tech.Id)
                                     select tech).ToList(),
                            Links = project.Links
                        }).ToList();
    }
}

We use a parameterized route here with the parameter CategoryId of type int, so we have to define the CategoryId parameter of type int in the code block too. Then we retrieve the data from data.json and use LINQ to filter the projects so that only the projects in our specified category remain. Note that this all happens inside the OnParametersSetAsync lifecycle method. As you remember from the previous part of the series, this method is called when the component’s parameters are updated from the URL or parent component. Here the CategoryId parameter is updated.

In the Razor section we use conditional code to check whether there are any projects. If no projects are loaded yet, we display the Loading… message. If there are no projects in the category, we leave another message. If there are any projects, we use the ProjectsDisplay component and pass them to its Projects parameter. We also display the category name and the number of projects in that category.

The ProjectsByTech component is implemented in a very similar way:

@page "/projects-by-tech/{TechId:int}"
@using MyPortfolio.Models;

@if (Projects == null)
{
    <p><em>Loading...</em></p>
}
else if (Projects.Count() == 0)
{
    <p><em>There are no projects using this technology.</em></p>
}
else
{
    <h3>Technology: @Tech.Name (@Projects.Count())</h3>

    @if (Projects.Count() > 0)
    {
        <div class="row mt-3">
            <ProjectsDisplay Projects="@Projects" />
        </div>
    }
}

@code {
    [Inject]
    HttpClient Http { get; set; }

    [Parameter]
    public int TechId { get; set; }

    public List<ProjectModel>? Projects { get; set; }
    public List<CategoryModel>? Categories { get; set; }    
    public List<TechModel>? Techs { get; set; }
    public TechModel? Tech { get; set; }
    public DataModel? Data { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        Data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

        Categories = Data.Categories;

        Techs = Data.Techs;

        Tech = Techs.FirstOrDefault(t => t.Id == TechId);
               
        Projects = (from project in Data.Projects
                    where project.Techs.Contains(TechId)
                    select new ProjectModel
                        {
                            Id = project.Id,
                            Name = project.Name,
                            Description = project.Description,
                            ImageUrl = project.ImageUrl,
                            Category = Categories.FirstOrDefault(c => c.Id == project.Category),
                            Techs = (from tech in Techs
                                     where project.Techs.Contains(tech.Id)
                                     select tech).ToList(),
                            Links = project.Links
                        }).ToList();
    }
}

This time we display the projects that use the technology specified by the TechId parameter.

With the ProjectsByCategory and ProjectsByTech components in place, let’s fix the NavLinks in the sidebar menu components so that they actually direct us to them. Here’s the CategoriesMenu component:

@using MyPortfolio.Models;
...
            @foreach (var category in Categories)
            {
                var link = "/projects-by-category/" + category.Id;

                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="@link">
                        ...
                    </NavLink>
                </div>
            }
        ...

Let’s do the same in the TechsMenu component:

@using MyPortfolio.Models;
...
            @foreach (var tech in Techs)
            {
                var link = "/projects-by-tech/" + tech.Id;

                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="@link">
                        ...
                    </NavLink>
                </div>
            }
        ...

Let’s now run the app and see how it works. Initially, we’re in the homepage, so the Home link in the sidebar is highlighted:

Now suppose we want to view just the projects in the Books category. If you click on Books in the Categories menu, it will be highlighted (A) and the books I wrote will be displayed (B). You can see the name of the category and the number of projects in this category as well. The route is also reflected in the URL (C):

And now let’s filter the projects by technology. Let’s say we want to view only the projects that use Python. This time the Python link in the sidebar is highlighted and all projects that use this technology are displayed. As you can see, the projects fall under all kinds of categories, like a game, a book, an online course or, if you scroll down, a magazine. What they all have in common, though, is that they use Python.

And what if there are no projects in a category or using a specific technology? Well, let’s try it out. I know I haven’t put any projects using .NET MAUI in my portfolio yet, so let’s select this technology. Now we see a message:

Good. We can now navigate between pages and to external links. Our little Blazor app looks complete. Now it’s time to show it to the world. Deployment is outside the scope of this series. I’ve deployed my app to Azure Static Web Apps, but you can try Github Pages or any other available solution.


Spread the love

Leave a Reply