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

Advertisements

3 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?

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 )

Google+ photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s

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

%d bloggers like this: