Is Active Route Tag Helper for ASP.NET MVC Core with Razor Page support

Ben Cull did an excellent tag helper which makes it easy to set the active class element using the route data from an ASP.NET Core MVC application. This blog uses this and extends the implementation with support for Razor Pages.

Original blog: Is Active Route Tag Helper for ASP.NET MVC Core by Ben Cull

The IHttpContextAccessor contextAccessor is added to the existing code from Ben. This is required so that the actual active Page can be read from the URL. The Page property is also added so that the selected page can be read from the HTML element.

The ShouldBeActive method then checks if an ASP.NET Core MVC route is used, or a Razor Page. Depending on this, the URL Path is checked and compared with the Razor Page, or like in the original helper, the MVC routing is used to set, reset the active class.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Linq;

namespace DamienbodTaghelpers
{
    [HtmlTargetElement(Attributes = "is-active-route")]
    public class ActiveRouteTagHelper : TagHelper
    {
        private readonly IHttpContextAccessor _contextAccessor;

        public ActiveRouteTagHelper(IHttpContextAccessor contextAccessor)
        {
            _contextAccessor = contextAccessor;
        }

        private IDictionary<string, string> _routeValues;

        /// <summary>The name of the action method.</summary>
        /// <remarks>Must be <c>null</c> if <see cref="P:Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper.Route" /> is non-<c>null</c>.</remarks>
        [HtmlAttributeName("asp-action")]
        public string Action { get; set; }

        /// <summary>The name of the controller.</summary>
        /// <remarks>Must be <c>null</c> if <see cref="P:Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper.Route" /> is non-<c>null</c>.</remarks>
        [HtmlAttributeName("asp-controller")]
        public string Controller { get; set; }

        [HtmlAttributeName("asp-page")]
        public string Page { get; set; }
        
        /// <summary>Additional parameters for the route.</summary>
        [HtmlAttributeName("asp-all-route-data", DictionaryAttributePrefix = "asp-route-")]
        public IDictionary<string, string> RouteValues
        {
            get
            {
                if (this._routeValues == null)
                    this._routeValues = (IDictionary<string, string>)new Dictionary<string, string>((IEqualityComparer<string>)StringComparer.OrdinalIgnoreCase);
                return this._routeValues;
            }
            set
            {
                this._routeValues = value;
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="T:Microsoft.AspNetCore.Mvc.Rendering.ViewContext" /> for the current request.
        /// </summary>
        [HtmlAttributeNotBound]
        [ViewContext]
        public ViewContext ViewContext { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            base.Process(context, output);

            if (ShouldBeActive())
            {
                MakeActive(output);
            }

            output.Attributes.RemoveAll("is-active-route");
        }

        private bool ShouldBeActive()
        {
            string currentController = string.Empty;
            string currentAction = string.Empty;

            if (ViewContext.RouteData.Values["Controller"] != null)
            {
                currentController = ViewContext.RouteData.Values["Controller"].ToString();
            }

            if (ViewContext.RouteData.Values["Action"] != null)
            {
                currentAction = ViewContext.RouteData.Values["Action"].ToString();
            }

            if(Controller != null)
            {
                if (!string.IsNullOrWhiteSpace(Controller) && Controller.ToLower() != currentController.ToLower())
                {
                    return false;
                }

                if (!string.IsNullOrWhiteSpace(Action) && Action.ToLower() != currentAction.ToLower())
                {
                    return false;
                }
            }

            if (Page != null)
            {
                if (!string.IsNullOrWhiteSpace(Page) && Page.ToLower() != _contextAccessor.HttpContext.Request.Path.Value.ToLower())
                {
                    return false;
                }
            }

            foreach (KeyValuePair<string, string> routeValue in RouteValues)
            {
                if (!ViewContext.RouteData.Values.ContainsKey(routeValue.Key) ||
                    ViewContext.RouteData.Values[routeValue.Key].ToString() != routeValue.Value)
                {
                    return false;
                }
            }

            return true;
        }

        private void MakeActive(TagHelperOutput output)
        {
            var classAttr = output.Attributes.FirstOrDefault(a => a.Name == "class");
            if (classAttr == null)
            {
                classAttr = new TagHelperAttribute("class", "active");
                output.Attributes.Add(classAttr);
            }
            else if (classAttr.Value == null || classAttr.Value.ToString().IndexOf("active") < 0)
            {
                output.Attributes.SetAttribute("class", classAttr.Value == null
                    ? "active"
                    : classAttr.Value.ToString() + " active");
            }
        }
    }
}

The IHttpContextAccessor needs the be added to the IoC in the Startup class.

public void ConfigureServices(IServiceCollection services)
{
  services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

In the _viewImports, add the tag helper namespace which matches the namespace used in the class.

@addTagHelper *, taghelpNamespaceUsedInYourProject

Add the tag helper can be used in the razor views which will work for both MVC views and also Razor Page views.

    <ul class="nav nav-pills flex-column">
            <li class="nav-item">
                <a is-active-route class="nav-link" asp-action="Index" asp-controller="A_MVC_Controller">This is a MVC route</a>
            </li>
    
            <li class="nav-item">
                <a is-active-route class="nav-link" asp-page="/MyRazorPage">Some Razor Page</a>
            </li>
    </ul>

Links:

https://benjii.me/2017/01/is-active-route-tag-helper-asp-net-mvc-core/

https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.1

7 comments

  1. […] Is Active Route Tag Helper for ASP.NET MVC Core with Razor Page support – Damien Bowden […]

  2. […] Is Active Route Tag Helper for ASP.NET MVC Core with Razor Page support (Damien Bowden) […]

  3. Stupid Question: Does Line 107 work? On my header links, I have an asp-route-id=”1234″ and when I get to line 107, the ViewContext.RouteData.Values do not contain that value (only the Controller and the Action). This causes the whole item to not match even though I passed the two previous checks. Am I missing something?

  4. […] Is Active Route Tag Helper for ASP.NET MVC Core with Razor Page support Source: ASP.NET Daily Articles […]

  5. Heiko · · Reply

    This is not working of you are using Areas (asp-area=”…”). All initial pages in my areas are named “Index” so every nav-item is “active”. Too bad…

  6. Heiko · · Reply

    Another point: the “active” class attribute should be added on the element according to bootstrap documentation. Adding it to the anchor tag has no effect!

    1. Heiko · · Reply

      “on the element” should read: “on the link element with class=’nav-item'”

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.