Uno de las características más útiles de DXA es la capacidad de ser extendido, para cumplir con cualquiera de nuestros requisitos, y la aplicación de ejemplo que se incluye “Out Of The Box” es un gran punto de entrada para descubrir lo que disponemos y qué podemos hacer para ampliar sus características.

En este artículo vamos a ver cómo funciona el menú y la navegación, y trataremos de expandir uno de sus elementos, un segundo nivel en el menú de navegación izquierdo.

Entorno

Mi entorno de trabajo es DXA 1.7 en una aplicación en .NET, con SDL Web 8.5.

Situación Inicial

Lo primero es analizar el menú que se ofrece por defecto en el ejemplo de DXA.

En la imagen, podemos ver 3 de los elementos ofrecidos por defecto en la aplicación ejemplo de DXA. “Navegación Superior“, “Migas” y “Navegación Izquierda“. Si investigamos en las vistas encargadas de renderizar estas entidades, encontramos que la vista de “Left Navigation“, que es la que queremos modificar, tiene el siguiente código:

@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>

Podemos navegar en el repositorio oficial DXA de SDL, para encontrar en la librería “Sdl.Web.Common” el código para esta entidad:

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

Por lo que, queda claro que sólo podemos tener un nivel de navegación, ya que la clase consiste en un sólo nivel de enlaces (Items).

Ademas, podemos encontrar el código donde se maneja esta entidad, que es la clase “StaticNavigationProvider” que se inyecta en el interfaz “INavigationProvider“, configurado en el fichero “Uniti.config” de la aplicación.

Y este “Provider” se utiliza en el controlador “NavigationController” definido en la clase “CoreAreaRegistration”.

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

Si vemos la plantilla de componente “Left Navigation“, descubrimos estos elementos configurados, el controlador de entidad o “Entity Controller” (Navigation) y la vista de la entidad o “Entity View” (LeftNavigation) que queremos modificar.

¿Que necesitamos?

Para modificar (extender) el comportamiento principal de la aplicación DXA de ejemplo, pero sin tocar su propio código, necesitamos crear un nuevo módulo, que vamos a llamar “Custom”. (En este artículo no se explica la creación y configuración del módulo)

Para extender estas entidades de Navegación primero tenemos que crear un “OwnNavigationController“, que va a llamar a nuestro “OwnNavigationProvider” que rellenará nuestro propio modelo de datos “OwnNavigationLinks” con nuestro segundo nivel de navegación, Y con nuestra vista “OwnLeftNavigation” para renderizarlo todo.

Veamos estos elementos:

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

Siendo la variable “LinkExtended” el segundo nivel de enlaces que se van a incluir.

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

La clase “OwnNavigationProvider” se basa en la clase “StaticNavigationProvider” que usa el fichero “navigation.json” publicado en la aplicación, que extrae la información de toda la navegación y los enlaces que se necesitan de los grupos de estructura de la publicación.

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));
    }
  }
}

La clase “OwnNavigationController” es la que va a invocar a este proveedor para rellenar la entidad correspondiente, necesaria para la vista. Se basa en el “NavigationController” del modulo principal, pero para nuestro ejemplo sólo vamos a implementar el tipo “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);
  }
 }
}

Necesitamos nuestra propia vista para utilizar este controlador y este proveedor que hemos comentado. El elemento que se usa para enlazar estos elementos es la “Plantilla de Componente” que vamos a utilizar para renderizar la navegación lateral, en el fichero existente donde se incluye.

Podemos copiar la plantilla de componente “Left Navigation” existente de la carpeta “SiteManager” del módulo principal (Core), dentro de nuestro propio módulo, para modificar el controlador con nuestro controlador “Custom:OwnNMavigationController“, y la vista donde se va a renderizar con nuestra vista “Custom:OwnLeftNavigation“, como podemos ver en la imagen:

La plantilla de componente se usa junto al componente “Navigation Configuration” en una página de tipo “include“, llamada “Left Navigation” en la cual deberemos cambiar la plantilla de componente por la nueva, como mostramos en la imagen

Ahora en la vista, podremos usar la entidad “OwnLinkExtended” para rellenar dos niveles de enlaces de navegación. Para este ejemplo vamos a mostrar solamente los enlaces de este segundo nivel, sin ninguna funcionalidad extra que pudiera incluirse para hacer más “usable” el menú.

@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>

Para probar este ejemplo, hemos creado algunos grupos de estructura en la aplicación de ejemplo, con el correspondiente convenio de nombres para que aparezcan en el menú, y hemos publicado la página “Navigation” correspondiente.

Si compilamos y ejecutamos todo el código comentado, podemos observar que la entidad “OwnNavigationLinks” se rellena con los dos niveles de navegación correspondientes a los grupos de estructura publicados en la publicación, Como podemos ver en la imagen final del resultado.

Futuras mejoras

Este ejemplo es una demostración muy simplificada de cómo se puede modificar el menú lateral de la aplicación de ejemplo en DXA, podemos pensar en mejoras como:

  • Generalizar los niveles de navegación
  • Extender otros menús (Top Navigation)
  • Refactorizar la clase Provider para usar más “lambdas
  • Intentar extender el INavigationProvider como Inyección de Dependencia
  • Usar javascript para dar “vida” al menú

Conclusiones

Es es un ejemplo rápido y sencillo de cómo podemos controlar y modificar los niveles del menú de navegación, pero seguro que , como casi todo en este mundo de Tridion, se podría conseguir de varias maneras, por lo que estaría encantado de discutirlas, y así aprender de todos vosotros.

Hasta la próxima.

noviembre 29, 2017

Segundo nivel en la navegación DXA

Uno de las características más útiles de DXA es la capacidad de ser extendido, para cumplir con cualquiera de nuestros requisitos, y la aplicación de ejemplo que se incluye “Out Of The Box” es un gran punto de entrada para […]
octubre 29, 2015

Literales y variables de contexto