Implement navigation

This page is part of a series you should follow sequentially from beginning to end. Go to the first step.

With the navigation content types in place, let’s use a view component to render the Navigation menu that exists in the Content hub.

Create view models

Start by creating view models that correspond to the Navigation item and Navigation menu classes.

Add a new Navigation folder under Kickstart.Web/Features, to hold all the navigation-related files.

Inside, create the NavigationItemViewModel. It only needs two properties - Title and Url.

C#
NavigationItemViewModel.cs

namespace Kickstart.Web.Features.Navigation;
public class NavigationItemViewModel
{
    public string Title { get; set; }

    public string Url { get; set; }
}

You might remember that in the page template step, you defined a method that created a LandingPageViewModel based on a LandingPage object.

We need a similar method to create a NavigationItemViewModel from a NavigationItem, but it will require dependency injection to find the page’s URL, so we’ll move it to a separate service (in the next section) rather than a view model. Because we’ll have this service anyway, we might as well also include the analogous method for NavigationMenuViewModel in the service to keep them together.

As a result, the NavigationViewModel class only needs properties to represent its Name and Items.

C#
NavigationMenuViewModel.cs

using System.Collections.Generic;

namespace Kickstart.Web.Features.Navigation;

public class NavigationMenuViewModel
{
    public string Name { get; set; }

    public IEnumerable<NavigationItemViewModel> Items { get; set; }
}

Add a service

Define a service interface with the two methods mentioned above.

C#
INavigationService.cs


using System.Threading.Tasks;

namespace Kickstart.Web.Features.Navigation;
public interface INavigationService
{
    Task<NavigationItemViewModel> GetNavigationItemViewModel(NavigationItem navigationItem);

    Task<NavigationMenuViewModel> GetNavigationMenuViewModel(NavigationMenu navigationMenu);
}

Moving on to implement this interface, we’ll need the following building blocks for GetNavigationItemViewModel:

  • IWebPageUrlRetriever to get the URL of the page referenced by the NavigationItemTarget.
  • IPreferredLanguageRetriever to ensure we get the URL of the correct language version of the target page.

Return null if the provided item is null or missing its target. Otherwise, use the GUID of the target page to retrieve a URL and build a view model.

C#
NavigationService.cs

using System.Linq;
using System.Threading.Tasks;
using CMS.Websites;
using Kentico.Content.Web.Mvc.Routing;
using Microsoft.IdentityModel.Tokens;

namespace Kickstart.Web.Features.Navigation;

public class NavigationService : INavigationService
{
    private readonly IWebPageUrlRetriever webPageUrlRetriever;
    private readonly IPreferredLanguageRetriever preferredLanguageRetriever;

    public NavigationService(IWebPageUrlRetriever webPageUrlRetriever,
    IPreferredLanguageRetriever preferredLanguageRetriever)
    {
        this.webPageUrlRetriever = webPageUrlRetriever;
        this.preferredLanguageRetriever = preferredLanguageRetriever;
    }
    
    public async Task<NavigationItemViewModel> GetNavigationItemViewModel(NavigationItem navigationItem)
    {
        if (navigationItem?.NavigationItemTarget?.IsNullOrEmpty() ?? true)
        {
            return null;
        }

        var targetGuid = navigationItem.NavigationItemTarget.FirstOrDefault().WebPageGuid;

        var targetUrl = await webPageUrlRetriever.Retrieve(targetGuid, preferredLanguageRetriever.Get());

        return new NavigationItemViewModel
        {
            Title = navigationItem.NavigationItemTitle,
            Url = targetUrl.RelativePath
        };
    }
}

Then, add the method that gets a menu view model for the provided NavigationMenu object, calling the GetNavigationItemViewModel method for each menu item.

C#
NavigationService.cs

...
public async Task<NavigationMenuViewModel> GetNavigationMenuViewModel(NavigationMenu navigationMenu)
{
    if (navigationMenu?.NavigationMenuItems?.IsNullOrEmpty() ?? true)
    {
        return null;
    }

    var menuItems = await Task.WhenAll(
        navigationMenu.NavigationMenuItems.Select(GetNavigationItemViewModel));

    return new NavigationMenuViewModel
    {
        Name = navigationMenu.NavigationMenuDisplayName,
        Items = menuItems
    };
}
...

The resulting service implementation should look like this file.

Finally, go to the Program.cs file in the root of the Kickstart.Web project and register the service implementation to the application’s IServiceCollection at some point between the WebApplication.CreateBuilder and builder.Build calls.

C#
Program.cs

...
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddSingleton<INavigationService, NavigationService>();
...
var app = builder.Build();
...

Add a using directive for the Kickstart.Web.Features.Navigation namespace where the service lives.

Define a view component

Websites often have multiple navigation menus. Let’s make a view component that renders the menu. You can reuse the view component across the site.

Define an asynchronous view component with a string parameter to indicate which menu item to retrieve.

C#
NavigationMenuViewComponent.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace Kickstart.Web.Features.Navigation;
public class NavigationMenuViewComponent : ViewComponent
{
    private readonly INavigationService navigationService;

    public NavigationMenuViewComponent(
        INavigationService navigationService)
    {
        this.navigationService = navigationService;
    }

    public async Task<IViewComponentResult> InvokeAsync(string navigationMenuCodeName)
    {
        // A placeholder representing the logic that retrieves the `NavigationMenu` item from the database.
        var menu = await RetrieveMenu(navigationMenuCodeName);

        if (menu == null)
        {
            // We will define this view in the next step.
            return View("~/Features/Navigation/NavigationMenuViewComponent.cshtml", new NavigationMenuViewModel());
        }

        var model = await navigationService.GetNavigationMenuViewModel(menu);

        return View("~/Features/Navigation/NavigationMenuViewComponent.cshtml", model);
    }
}

If you copy-paste the code above, you will see a couple of issues:

  1. The RetrieveMenu method does not exist - for now, we are using a placeholder to represent the logic that retrieves the menu item from the database.
  2. The ~/Features/Navigation/NavigationMenuViewComponent.cshtml view does not exist yet.

We will fix both of these shortly.

Add a private method to retrieve the menu.

Query the content items with the same approach we used for the LandingPageController in the earlier step, but filtering based on a provided codename rather than the WebPageDataContext.

C#
NavigationMenuViewComponent.cs

...
private async Task<NavigationMenu> RetrieveMenu(string navigationMenuCodeName)
{
    var builder = new ContentItemQueryBuilder()
        .ForContentType(NavigationMenu.CONTENT_TYPE_NAME,
        config => config
            .Where(where => where.WhereEquals(nameof(NavigationMenu.NavigationMenuCodeName), navigationMenuCodeName))
            .WithLinkedItems(2))
        .InLanguage(preferredLanguageRetriever.Get());

    var queryExecutorOptions = new ContentQueryExecutionOptions
    {
        ForPreview = webSiteChannelContext.IsPreview
    };

    var items = await contentQueryExecutor.GetMappedResult<NavigationMenu>(builder, queryExecutorOptions);

    return items.FirstOrDefault();
}
...

Once you add the necessary using directives and inject the required components, the view component should look like this.

Design the component’s view

Now, we just need a view to render the model provided by NavigationMenuViewComponent.

Add a file called NavigationViewComponent.cshtml that renders a nav element containing a list of links from the model’s Items.

Spice up the design of the menu with some Bootstrap CSS classes.

cshtml
NavigationMenuViewComponent.cshtml

@using Kickstart.Web.Features.Navigation

@model NavigationMenuViewModel

@if (Model?.Items?.Any() ?? false)
{
    <nav>
        <ul class="navbar-nav nav-underline">
            @foreach (var item in Model.Items)
            {
                <li class="nav-item">
                    <a href="@item.Url" class="nav-link link-light">@item.Title</a>
                </li>
            }
        </ul>
    </nav>
}

Finally, add the view component to the _Layout.cshtml view beneath the site heading, specifying MainNavigation as the codename of the menu to retrieve. Adjust the Bootstrap CSS classes as necessary.

cshtml
_Layout.cshtml

...
<div class="navbar navbar-primary navbar-expand-sm navbar-dark bg-primary p-3">
    <div class="navbar-brand">
        <h2>Xperience Kickstart</h2>
    </div>
    <vc:navigation-menu navigation-menu-code-name="MainNavigation" />
</div>
...

With all these elements in place, you should have a working navigation menu that looks something like this:

Screen recording of working navigation

If you haven’t already, check out our Kickstart repository to see the complete implementation of the website we created together in this series. To run the project, follow the README instructions.

Now that you have a website that uses a template to display linked reusable items alongside Page Builder content, all navigable with a working nav menu, you may be wondering where to go from here. Find out in the next step!

Previous step: Model navigation — Next step: Next steps

Completed steps: 12 of 13