SaaS Headless CMS ASP.NET Core Razor Pages Data-Driven Top Navigation View Component

This blog post describes a prototype ASP.NET Core razor pages view component that renders a top navigation based on the information architecture of the website as determined by its URL hierarchy. This post is a continuation of a series progressing an ASP.NET razor pages solution architecture using the Contentstack SaaS headless CMS.

An ASP.NET Core razor page view component consists of a razor pages view template (.cshtml file) and a class that typically constructs a model for that view. While this allows separation of logic from presentation, it has nothing to do with ASP.NET web forms, using the MVC architecture instead.

The top nav for the default ASP.NET razor pages site includes a link to the home page and the top-level pages of the site. To replace this, we need to retrieve metadata about the home page and its children in the CMS. We can use the Web API implemented previously that retrieves information about entries and their relations. I had refactored PathApiEntryModel to support storing URLs in different places and exposing a URL depth property that is not relevant to this implementation, so this is a bit more complicated than it needs to be.

using System;

public struct PathApiEntryModel
{
    public class PageProperties
    {
        public string PageTitle { get; set; }
        public string NavTitle { get; set; }
        public string Url { get; set; }
    }

    public PageProperties PageData { get; set; }
    public string ContentType { get; set; }
    public string Uid { get; set; }
    public string Title { get; set; }

    private string _url;
 
    public string Url
    {
        get
        {
            if (String.IsNullOrEmpty(_url))
            {
                _url = PageData.Url;
            }

            return _url;
        }

        set
        {
            _url = value;
        }
    }

    public int Depth
    {
        get
        {
            return Url.Length - Url.Replace("/", "").Length;
        }
    }

    public string NavTitle
    {
        get
        {
            return PageData.NavTitle ?? PageData.PageTitle ?? Title;
        }
    }

    public string PageTitle
    {
        get
        {
            return PageData.PageTitle ?? Title;
        }
    }
}

PathApiResultModel models the results of the Web API call.

using System.Collections.Generic;

public class PathApiResultModel
{
    public int Ancestors { get; set; }
    public int CurrentGeneration { get; set; }
    public int Descendants { get; set;  }
    public IEnumerable<PathApiEntryModel> Entries { get; set; }

    public int Total
    {
        get
        {
            return Ancestors + CurrentGeneration + Descendants;
        }
    }
}

To simplify view component logic, the view component class translates these results to a TopNavModel with properties that contain metadata about the home entry and its children. The view class uses an injected Web API client to retrieve the required entry metadata from the CMS, which becomes the model for the top nav view template to render.

using Microsoft.AspNetCore.Mvc;

using System.Collections.Generic;
using System.Threading.Tasks;

using Deliverystack.DeliveryApi.Models;
using Deliverystack.Models;

public class TopNav : ViewComponent
{
    public class TopNavModel
    {
        public PathApiEntryModel Home { get; set; }
        public PathApiEntryModel[] Children { get; set; }

        public static TopNavModel GetFromResults(PathApiResultModel data)
        {
            var result = new TopNavModel();
            var enumerator = data.Entries.GetEnumerator();
            if (enumerator.MoveNext()) result.Home = enumerator.Current;
            List<PathApiEntryModel> children = new List<PathApiEntryModel>();
            while (enumerator.MoveNext())
                children.Add(enumerator.Current);
            result.Children = children.ToArray();
            return result;
        }
    }

    private PathApiClient _client;

    public TopNav(PathApiClient client)
    {
        _client = client;
    }

    // May 18 2021 - logic is synchronous and lightweight; ignore warnings about async
    #pragma warning disable CS1998
    public async Task<IViewComponentResult> InvokeAsync()
    {
        return View(TopNavModel.GetFromResults(
            _client.Get(new PathApiBindingModel() { Path = "/", Descendants = 1 })));
    }
    #pragma warning restore CS1998 // Rethrow to preserve stack details
}

The view component template specifies the model and retrieves data about the home entry and its children.

@model TopNav.TopNavModel

<a class="navbar-brand" href="@Model.Home.Url">@Model.Home.NavTitle</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse"
        data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
        aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
</a>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    <ul class="navbar-nav flex-grow-1">
        @{
            foreach (var child in Model.Children)
            {
                <li class="nav-item">
                    <a class="nav-link text-dark" href="@child.Url">@child.NavTitle</a>
                </li>
            }
        }
    </ul>
</div>

You can invoke a view component from a view template, in my case from the layout.

@await Component.InvokeAsync("TopNav")

2 thoughts on “SaaS Headless CMS ASP.NET Core Razor Pages Data-Driven Top Navigation View Component

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: