Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine the current route in a Tag Helper. Is this possible?

On an ASP.NET MVC 6 project I have the following:

[Route("help/how-it-works")]
public IActionResult HowItWorks() {
   return View();
}

I want to create a tag helper as follows:

<a class="menu" asp-controller="Help" asp-action="HowItWorks" route-is="help/how-it-works" css-class="active">How it works</a>

So the route-is tag helper would check if the current route is "help/how-it-works" ... If it is then add "active" to the css class of the A tag.

So I started to create a tag helper:

[TargetElement("a", Attributes = "route-is, css-class")]
public class RouteTagHelper : TagHelper
{

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

    String routeIs = context.AllAttributes["route-is"].ToString();

    String cssClass = context.AllAttributes["css-class"].ToString();

    if (String.IsNullOrWhiteSpace(cssClass))
        cssClass = "active";

    ViewContext.RouteData.Values.Keys;

}
}// Process

My problem is how to determine if the current route is "help/how-it-works" and if it is add the Css class to the A tag without changing anything else.

Does anyone has any idea of how to do this?

UPDATE 1

Solved the problem with the duplicated values when using Attribute Routing and added an alternative approach of the one proposed by Daniel J.G.

[TargetElement("a", Attributes = RouteIsName)]
[TargetElement("a", Attributes = RouteHasName)]
public class ActiveRouteTagHelper : TagHelper
{
    private const String RouteIsName = "route-is";
    private const String RouteHasName = "route-has";
    private const String RouteCssName = "route-css";

    private IActionContextAccessor _actionContextAccessor;
    private IUrlHelper _urlHelper;

    [HtmlAttributeName(RouteCssName)]
    public String RouteCss { get; set; } = "active";

    [HtmlAttributeName(RouteHasName)]
    public String RouteHas { get; set; }

    [HtmlAttributeName(RouteIsName)]
    public String RouteIs { get; set; }


    public ActiveRouteTagHelper(IActionContextAccessor actionContextAccessor, IUrlHelper urlHelper)
    {

        _actionContextAccessor = actionContextAccessor;
        _urlHelper = urlHelper;

    } // ActiveRouteTagHelper


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

        IDictionary<String, Object> values = _actionContextAccessor.ActionContext.RouteData.Values;

        String route = _urlHelper.RouteUrl(values.Distinct()).ToLowerInvariant();

        Boolean match = false;

        if (!String.IsNullOrWhiteSpace(RouteIs))
        {

            match = route == RouteIs;

        } else {        

            if (RouteHas != null) {

                String[] keys = RouteHas.Split(',');

            if (keys.Length > 0) 
                match = keys.All(x => route.Contains(x.ToLowerInvariant()));

            }
        }

        if (match)
        {
            TagBuilder link = new TagBuilder("a");
            link.AddCssClass(RouteCss);
            output.MergeAttributes(link);
        }

    } // Process

} // ActiveRouteTagHelper
like image 288
Miguel Moura Avatar asked Dec 18 '22 23:12

Miguel Moura


1 Answers

You can take advantage of the fact that you target the same element <a> than the default MVC AnchorTagHelper. You just need to make sure that you add your helpers in _ViewImports.cshtml after the default ones:

@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, WebApplication5"

Then whenever your helper is executed, the TagHelperOutput will already contain the href generated by the default AnchorTagHelper using the asp-controller and asp-action tags.

//Get url from href attribute generated by the default AnchorTagHelper
var url = output.Attributes["href"].Value.ToString();

Then you can compare that url with the one that would be generated for the current request:

var currentRoutUrl = this.urlHelper.Action();

Initially I tried the code below, but it does not work when using attribute routing. I can see an entry with key !__route_group in the route values and an exception ArgumentException: An item with the same key has already been added is thrown:

var currentRouteUrl = this.urlHelper.RouteUrl(
                               this.actionContextAccessor.ActionContext.RouteData.Values);

Doing that instead of comparing the current request url has a reason. This way, regardless of the current request url being "/" or "/Home/Index", in both cases you would consider active a link for controller Home and action Index)

I have created a tag helper following this idea:

  • The tag helper will be used for <a> elements that have an attribute highlight-active defined. (That allows the attribute for the css class to be optional, in which case the default active class is used):

  • The marker attribute highlight-active is removed from the output html

  • The class attribute is merged with the css-active-class attribute (which is also removed from the output html)

The code looks like:

[HtmlTargetElement("a", Attributes = "highlight-active")]
public class RouteTagHelper : TagHelper
{
    private IActionContextAccessor actionContextAccessor;
    private IUrlHelper urlHelper;

    public RouteTagHelper(IActionContextAccessor actionContextAccessor, IUrlHelper urlHelper)
    {
        this.actionContextAccessor = actionContextAccessor;
        this.urlHelper = urlHelper;
    }

    //Optional attribute. If not defined, "active" class will be used
    [HtmlAttributeName("css-active-class")]
    public string CssClass { get; set; } = "active";

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        //Remove marker attribute
        output.Attributes.Remove(output.Attributes["highlight-active"]);

        //Get the url from href attribute generaed in the default AnchorTagHelper
        var url = output.Attributes["href"].Value.ToString();

        //Add active css class only when current request matches the generated href
        var currentRouteUrl = this.urlHelper.Action();
        if (url == currentRouteUrl)
        {
            var linkTag = new TagBuilder("a");
            linkTag.Attributes.Add("class", this.CssClass);
            output.MergeAttributes(linkTag);
        }
    }
}

So you can now write the following in your Home/Index page:

<a class="menu" asp-controller="Home" asp-action="Index" highlight-active>Home</a>
<a class="menu" asp-controller="Home" asp-action="About" highlight-active>About</a>
<a class="menu" asp-controller="Home" asp-action="Index" highlight-active css-active-class="myActiveClass">Home with special class name</a>
<a class="menu" asp-controller="Home" asp-action="Index">Home using default tag helper</a>

Which is rendered as follows (regardless of the current url being / or /Home or /Home/Index):

<a class="menu active" href="/">Home</a>
<a class="menu" href="/Home/About">About</a>
<a class="menu myActiveClass" href="/">Home with special class</a>
<a class="menu" href="/">Home using default tag helper</a>

PS. You might still need to consider the cases when you add an anchor directly specifying an href attribute (Which you can detect checking if you already have an href before calling base.Process). In that scenario, you might also want to compare against the current url (via httpContext.Request).

like image 124
Daniel J.G. Avatar answered May 22 '23 19:05

Daniel J.G.