0

I have some tabs in bootstrap which has to be set as active depending on the action being hit. I have sub-tabs as well which has to be set as active depending on the action being hit as well.

Here is an image of how it looks like:

So when a sub-tab is being active the parent tab has to be active too.

enter image description here

So I thought to create a new Attribute where I save a pageId for each action and depending on the pageId on the view I can set it to active or not:

Here is the attribute:

public class YcoPageId : Attribute
{
    public YcoPageId(int pageId)
    {
        PageId = pageId;
    }
    public int PageId { get; } 
} 

Here is the action:

[YcoPageId(1)]
public ActionResult Admin()
{
    return View();
}

For the view I want to create an extension method to see if the tab and sub-tabs shall be active or not!

Here is my code:

public static bool IsActive(this HtmlHelper htmlHelper, params int[] ids)
{
    var viewContext = htmlHelper.ViewContext;
    var action = viewContext....
    //How to get the YcoPageId attribute from here and see the Id
    //Here i will test if ids contain id but dont know how to get it...
}

If you think adding an id for each page is bad idea I think for my case I will use this id for other purposes as well because it will be like identifier for a specific action...

So my question is how can I get the attribute YcoPageId for current action in my extension method ?

The view will look like this:

<li class="@(Html.IsActive(1, 4, 5... etc)? "active" : "")">
    <a href="@url">
        <div class="row text-center">
            <div class="col-md-12">
                <i class="fa @fontAwesomeIcon fa-4x" aria-hidden="true"></i>
                <br/>@menuName
            </div>
        </div>
    </a>
</li>

If there is any better idea how to solve this issue please go ahead!

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Rey
  • 3,663
  • 3
  • 32
  • 55
  • What is the relationship of these tabs? Does the top tab relate to Controllers and the sub-tab relate to Action methods in the selected controller? –  Aug 29 '17 at 22:56
  • Nope they dont have a relationship at all because I am migrating some ASP.NET Webforms code to ASP.MVC so I can create any kind of relation, for me its important to work :) – Rey Aug 29 '17 at 22:59
  • @StephenMuecke But now as I am thinking it would make sense to make a controller for a parent tab but this would make me test the sub-tabs for the action name meanwhile the parent tab only for the controller name, is that what you are trying to explain ? – Rey Aug 29 '17 at 23:01
  • Yes, that is what I was assuming. But its also unclear where `params int[] ids` comes from and how you set that –  Aug 29 '17 at 23:03
  • @StephenMuecke see the updated question so in the view I know the id of each page and set like this `Html.IsActive(2, 4, 5... etc)` – Rey Aug 29 '17 at 23:05
  • But were do those values come from? I think your approaching this all wrong by trying to use an attribute, but you can always use reflection to read it - refer [this Q/A](https://stackoverflow.com/questions/2656189/how-do-i-read-an-attribute-on-a-class-at-runtime) for an example –  Aug 29 '17 at 23:09
  • For each action I will set manually a random unique id which will be used for many purposes for that action - one of them is the one above, so lets suppose Admin action has the id = 1, Html.IsActive(1) will make active the the current tab if the Admin action is being hit, but now as I am talking to you I dont want to use reflecion but I will use ActionFilterAttribute and attach the id to TempDataDictionary so I can get it back in the view. What do you think about this approach ? – Rey Aug 29 '17 at 23:13

2 Answers2

1

Here is my solution to this problem:

First created a actionfilter attribute like below:

public class YcoPageIdAttribute : ActionFilterAttribute
{
    public YcoPageIdAttribute(int pageId)
    {
        PageId = pageId;
    }
    public int PageId { get; }
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.Result is ViewResult)
        {
            filterContext.Controller.TempData[DomainKeys.ViewPageId] = PageId;
        }
        else 
        {
            throw new Exception("Only ViewResult has unique id");
        } 
        base.OnActionExecuted(filterContext);
    }
} 

Then my action would look like this one:

[YcoPageId(1)]
public ActionResult Admin()
{ 
    return View();
}

I created an extension method like below:

public static bool IsActive(this HtmlHelper htmlHelper, params int[] ids)
{
    var viewContext = htmlHelper.ViewContext;
    return viewContext.TempData.ContainsKey(DomainKeys.ViewPageId) && 
        int.Parse(viewContext.TempData.Peek(DomainKeys.ViewPageId).ToString()).In(ids);
}

Since I know the id of an action now I have only to put the code as below in the view:

<li class="@(Html.IsActive(1)? "active" : "")">
    <a href="@url">
        <div class="row text-center">
            <div class="col-md-12">
                <i class="fa @fontAwesomeIcon" aria-hidden="true"></i>
                <br /><small>@menuName</small>
            </div>
        </div>
    </a>
</li>

I made another method on startup to check if I have actions with duplicated values like below:

public static void CheckForDuplicateActionId()
{
    Assembly asm = Assembly.GetExecutingAssembly();
    var controllerActionlist = asm.GetTypes()
        .Where(type => typeof(Controller).IsAssignableFrom(type))
        .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |
                                            BindingFlags.Public))
        .Where(m => !m.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute),
            true).Any())
        .Where(m => m.GetCustomAttribute<YcoPageIdAttribute>() != null)
        .Select(
            x =>
                new
                {
                    Controller = x.DeclaringType.Name,
                    Area = x.DeclaringType.FullName,
                    Action = x.Name,
                    ReturnType = x.ReturnType.Name,
                    Id = x.GetCustomAttribute<YcoPageIdAttribute>().PageId
                })
        .ToList();
    var actionIds = controllerActionlist.Select(x => x.Id).ToList();
    var actionIdsGrouped = actionIds.GroupBy(x => x).Where(x => x.Count() > 1).ToList();
    if (!actionIdsGrouped.IsNullOrEmpty()) 
    {
        StringBuilder error = new StringBuilder("");
        actionIdsGrouped.ForEach(actionId =>
        {
            var actions = controllerActionlist.Where(x => x.Id == actionId.Key);
            actions.ForEach(a =>
            {
                error.Append(
                    $" | Id : {a.Id}, Action Name: {a.Action}, Controller Name : {a.Controller}, Location : {a.Area}. ");
            });
        });
        var maxId = controllerActionlist.Max(x => x.Id);
        error.Append(
            "PLease consider changing the the duplicated id - Here are some options to choose from : Id {");
        for (int i = 1, j = 1; i < maxId + 5; i++)
        {
            if (actionIds.Contains(i)) continue;
            if (j < 5)
            {
                error.Append(i + ",");
                j++;
            }
            else
            {
                error.Append(i + "}");
                break;
            }
        }
        throw new Exception(
            $"There are more than one action duplicated with the same Id, The action data are as below : {error}");
    }
}

Probably I will add all these data in database so I can identify an action from one id from database as well :)

Now it is working good.

Rey
  • 3,663
  • 3
  • 32
  • 55
0

If I understand correctly you are trying to create an id for each page/view and use it in the page/view to dynamically set css classes for setting menu tabs as active. If that is the case... Rather than trying to set the Ids in the Controller, how about creating a Shared View with only the following code - something like this....

In the View write the following Razor/C# code.

    @{ 
        var iPageId = 0
        var sViewPath = ((System.Web.Mvc.BuildManagerCompiledView)ViewContext.View).ViewPath;
        
        //for example
        if (sViewPath.ToLower.IndexOf("admin") >= 0)
        {
          iPageId = 1;
        }
        else if (sViewPath.ToLower.IndexOf("dashboard") >= 0)
        {
          iPageId = 2;
        }
        else if (sViewPath.ToLower.IndexOf("vessels") >= 0)
        {
          iPageId = 3;
        }
        else if (sViewPath.ToLower.IndexOf("reports") >= 0)
        {
          iPageId = 4;
        }
    }

Render the Shared View in the primary view with the following snippet.

@Html.Partial("~/Views/Menu/_SharedViewName.cshtml")

Then you should be able to access the iPageId variable anywhere in the primary page/view(s) and set your CSS classes accordingly.

Tommy Snacks
  • 171
  • 6