Second Level in DXA Navigation

One of the most usefull things of DXA is the capacity of being extended to meet mostly of all our requirements, and the OOTB example site is a great starting point to discover what you have and what you can do to improve their characteristics.

In this article I’m going to discuss the Navigation menu and try to expand one of its characteristics, specifically the second level of navigation in the left menu.

Environment

My working environment is DXA 1.7 in a .NET application, with SDL Web 8.5.

Initial situation

First of all, let’s analyse the menu offered out of the box, by DXA.

In the image, we can see 3 of the navigation facilities offered in the OOTB DXA example site. “Top Navigation” “Breadcrumbs” and “Left Navigation”. If we dig inside the views in charge of render this entities, we find the “Left Navigation” View, that we want to improve, with the following code:

@model NavigationLinks
<nav @Html.DxaEntityMarkup()>
   <ul class="nav nav-sidebar">
   @foreach (Link item in Model.Items)
   {
      string linkUrl = @Url.NormalizePageUrlPath(item.Url);
      string requestUrl = @Url.NormalizePageUrlPath(Request.Url.LocalPath);
      string cssClass = requestUrl.StartsWith(linkUrl) ? "active" : string.Empty;
      <li class="@cssClass">
         <a href="@linkUrl" title="@item.AlternateText">@item.LinkText</a>
      </li>
   }
   </ul>
</nav>

We can browse in the official SDL DXA repository for the “Sdl.Web.Common” classes the code for this entity:

public class NavigationLinks : EntityModel
{
   public List<Link> Items { get; set; }
   public NavigationLinks()
   {
      Items = new List<Link>();
   }
}

And so, it’s clear that we can only have one level of navigation, as it consists in only one level of links (Items).

Furthermore, we can find the code where this entity is managed, that is the “StaticNavigationProvider” that is injected in the “INavigationProvider” interface, configured in the “Unity.config” file of the application.

And this provider is controlled by the controller  “NavigationController” defined in the “CoreAreaRegistration.cs” class.

RegisterViewModel("LeftNavigation", typeof(NavigationLinks), "Navigation");

If we see the “Left Navigation” Component Template, we discover this configuration items, the Entity Controller (Navigation) and the Entity View (LeftNavigation) that we want to modify.

What do we need?

To modify (extend) the core behaviour of the example DXA application, but without touching its own code, we need to creat a new module, that we are going to call “Custom”. (We don’t explain the creation and configuration of the module in this post)

To extend this Navigation entities we first create an “OwnNavigationController“, that is going to call an “OwnNavigationProvider” to fill our own “OwnNavigationLinks” model with our second level of navigation. And with our “OwnLeftNavigation” view to render it all.

Let’s see this elements:

namespace Sdl.Web.Site.Navigation
{
   public class OwnNavigationLinks : EntityModel
   {
      public OwnNavigationLinks() { }
      public List<LinkExtended> Items { get; set; }
   }
}

Being “LinkExtended” the second level of links that are included.

namespace Sdl.Web.Site.Navigation
{
   public class LinkExtended : Link
   {
      public LinkExtended() { }
      public List<Link> Items { get; set; }
   }
}

The class “OwnNavigationProvider” is based on the “StaticNavigationProvider” that uses the “navigation.json” file published to the application, that extracts the information of all the navigation and links that are needed.

public class OwnNavigationProvider
{
 
  public SitemapItem GetNavigationModel(Localization localization)
  {
    using (new Tracer(localization))
    {
    return SiteConfiguration.CacheProvider.GetOrAdd(
      localization.LocalizationId,
      CacheRegions.StaticNavigation,
      () => BuildNavigationModel(localization)
      // TODO: dependency on navigation.json Page
      );
    }
  }

  public OwnNavigationLinks GetContextNavigationLinks(string requestUrlPath, Localization localization)
  {
    using (new Tracer(requestUrlPath, localization))
    { 
      // Find the context Sitemap Item; start with Sitemap root.
      SitemapItem contextSitemapItem = GetNavigationModel(localization);
      if (requestUrlPath.Contains("/"))
      {
        while (contextSitemapItem.Items != null)
        {
          SitemapItem matchingChildSg = contextSitemapItem.Items.FirstOrDefault(i => i.Type == SitemapItem.Types.StructureGroup && requestUrlPath.StartsWith(i.Url, StringComparison.InvariantCultureIgnoreCase));
          if (matchingChildSg == null)
          {
            // No matching child SG found => current contextSitemapItem reflects the context SG.
            break;
          }
          contextSitemapItem = matchingChildSg;
        }
      }
      if (contextSitemapItem.Items == null)
      {
        throw new DxaException($"Context SitemapItem has no child items: {contextSitemapItem}");
      }
      List<LinkExtended> firstLevel = new List<LinkExtended>();
      NavigationLinks noLevel = new NavigationLinks
      {
        Items = contextSitemapItem.Items.Where(i => i.Visible).Select(i => i.CreateLink(localization)).ToList()
      };
      // fill second level
      for (int j = 0; j < noLevel.Items.Count; j++)
      {
        NavigationLinks secondLevel = new NavigationLinks
        {
          Items = contextSitemapItem.Items[j].Items.Where(i => i.Visible).Select(i => i.CreateLink(localization)).ToList()
        };
        LinkExtended secondLevelLink = new LinkExtended
        {
          Items = secondLevel.Items,
          Url = noLevel.Items[j].Url,
          LinkText = noLevel.Items[j].LinkText,
          AlternateText = noLevel.Items[j].AlternateText
        };
        firstLevel.Add(secondLevelLink);
      }
      OwnNavigationLinks withLevel = new OwnNavigationLinks()
      {
        Items = firstLevel
      };
      return withLevel;
    }
  }

  private SitemapItem BuildNavigationModel(Localization localization)
  {
    using (new Tracer(localization))
    {
      string navigationJsonUrlPath = SiteConfiguration.LocalizeUrl("navigation.json", localization);

      Log.Debug("Deserializing Navigation Model from raw content URL '{0}'", navigationJsonUrlPath);
      IRawDataProvider rawDataProvider = SiteConfiguration.ContentProvider as IRawDataProvider;
      if (rawDataProvider == null) 
      {
        throw new DxaException(
        string.Format("The current Content Provider '{0}' does not implement interface '{1}' and hence cannot be used in combination with Navigation Provider '{2}'.",
        SiteConfiguration.ContentProvider.GetType().FullName, typeof(IRawDataProvider).FullName, GetType().FullName)
        );
      }
      return JsonConvert.DeserializeObject<SitemapItem>(rawDataProvider.GetPageContent(navigationJsonUrlPath, localization));
    }
  }
}

The class “OwnNavigationController” is the class that is going to invoke this Provider to fill the Entity needed for the view. Is based on the “NavigationController”, but only we are going to implement the “Left navType“:

namespace Sdl.Web.Mvc.Controllers
{
public class OwnNavigationController : EntityController
{
 [HandleSectionError(View = "SectionError")]
 public virtual ActionResult Navigation(EntityModel entity, string navType, int containerSize = 0)
 {
  SetupViewData(entity, containerSize);
  OwnNavigationProvider navigationProvider = new OwnNavigationProvider();
  string requestUrlPath = Request.Url.LocalPath;
  Localization localization = WebRequestContext.Localization;
  OwnNavigationLinks model;
  switch (navType)
  {
    case "Left":
      model = navigationProvider.GetContextNavigationLinks(requestUrlPath, localization);
      break;
    default:
      throw new DxaException("Unexpected navType: " + navType);
  }

  EntityModel sourceModel = (EnrichModel(entity) as EntityModel) ?? entity;
  model.XpmMetadata = sourceModel.XpmMetadata;
  model.XpmPropertyMetadata = sourceModel.XpmPropertyMetadata;

  return View(sourceModel.MvcData.ViewName, model);
  }
 }
}

We need to make our own view to use this controller and this provider. The element which is used to link this elements is the Component Template that we are going to use to render the left navigation, in the existing include file.

We can copy the “Left Navigation” Component Template from the SiteManager folder of the Core module, into our own module, to modify the controller with our “Custom:OwnNavigationController“, and the rendered view with our “Custom:OwnLeftNavigation” view, as in the following image.

This component template is used with the Navigation Configuration component, in an included page called “Left Navigation” where we must change the component presentation, and publish, as it is showed in the next image

Now in the View, we can use the entity “OwnLinkExtended” to fill two levels of navigation links. For this example we are going to show only the links without any functionality that may be used to give more “usability” to the menu.

@model Sdl.Web.Site.Navigation.OwnNavigationLinks
<nav @Html.DxaEntityMarkup()>
  <ul class="nav nav-sidebar">
  @foreach (Sdl.Web.Site.Navigation.LinkExtended item in Model.Items)
  {
    string linkUrl = @Url.NormalizePageUrlPath(item.Url);
    string requestUrl = @Url.NormalizePageUrlPath(Request.Url.LocalPath);
    string cssClass = requestUrl.StartsWith(linkUrl) ? "active" : string.Empty;
    <li class="@cssClass">
      <a href="@linkUrl" title="@item.AlternateText">@item.LinkText</a>
      <ul>
      @foreach (Link item2 in item.Items)
      {
        string linkUrl2 = @Url.NormalizePageUrlPath(item2.Url);
        string requestUrl2 = @Url.NormalizePageUrlPath(Request.Url.LocalPath);
        string cssClass2 = requestUrl2.StartsWith(linkUrl2) ? "active" : string.Empty;
        <li class="@cssClass2">
          <a href="@linkUrl2" title="@item2.AlternateText">@item2.LinkText</a>
        </li>
      }
      </ul>
    </li>
  }
  </ul>
</nav>

To test this example, we have created some structure groups in the example application, with the necessary naming convention, and published the “Navigation” page.

If we compile and run all this code, we can see that the “OwnNavigationLinks” is now filled with the two levels of navigation provided by the structure groups published in the publication. As it is shown in the following image:

Further improvements

This example is a simple demostration of how we can change the left meu of the example DXA implementation, but for example, we can think of:

  • Generalize the levels of navigation
  • Extend for other menus (top)
  • Refactor the provider to use more “lambdas
  • Try to extend the INavigationProvider as a Dependency Injection
  • Use javascript to give action to the menu

Conclussions

This is a simple and fast example of how the levels of menu navigation can be managed, but I’m sure, as almost everything in the Tridion world, this can be achieved in several ways, so I will be eager to discuss them, and learn from you all.

Bye.