MVC with Bootstrap Navbar - Set Selected Item to Active - asp.net-mvc

I'm learning Bootstrap and can't get the selected item into an "active" state. The active state remains on the default item. The newly selected/clicked item changes to active briefly, but reverts back. I've read all the posts and still can't get this code to work. I'm using MVC5 and JQuery 2.1.
EDIT:
If I change the li's hrefs to href="#", then the active class gets applied properly. What's happening when a new view gets loaded? I think Sebastian's response is close, but gets messy with Areas.
Markup
<div class="navbar-wrapper">
<div class="container">
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-header">
<a class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="navbar-brand" href="#">Test</a>
</div>
<div class="btn-group pull-right">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-user"></i>Login
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li>Profile</li>
<li class="divider"></li>
<li>Sign Out</li>
</ul>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active">Home</li>
<li>About</li>
<li>Students Sample</li>
<li class="dropdown">
Dropdown <b class="caret"></b>
<ul class="dropdown-menu">
<li>Admin</li>
<li>Another action</li>
<li>Something else here</li>
<li class="divider"></li>
<li>Separated link</li>
<li>One more separated link</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<!-- /container -->
</div>
<!-- /navbar wrapper -->
Script
<script type="text/javascript">
$(function () {
$('.navbar-nav li').click(function () {
$(this).addClass('active').siblings().removeClass('active');
});
});
</script>
EDIT: Here's what I ended up doing with the help of the posted answers and some research.
public static string MakeActive(this UrlHelper urlHelper,string action, string controller, string area = "")
{
string result = "active";
string requestContextRoute;
string passedInRoute;
// Get the route values from the request
var sb = new StringBuilder().Append(urlHelper.RequestContext.RouteData.DataTokens["area"]);
sb.Append("/");
sb.Append(urlHelper.RequestContext.RouteData.Values["controller"].ToString());
sb.Append("/");
sb.Append(urlHelper.RequestContext.RouteData.Values["action"].ToString());
requestContextRoute = sb.ToString();
if (string.IsNullOrWhiteSpace(area))
{
passedInRoute = "/" + controller + "/" + action;
}
else
{
passedInRoute = area + "/" + controller + "/" + action;
}
// Are the 2 routes the same?
if (!requestContextRoute.Equals(passedInRoute, StringComparison.OrdinalIgnoreCase))
{
result = null;
}
return result;
}

You have to check in your controller or view which menu item is active based on the current url:
I have an extension method similar to this:
public static string MakeActiveClass(this UrlHelper urlHelper, string controller)
{
string result = "active";
string controllerName = urlHelper.RequestContext.RouteData.Values["controller"].ToString();
if (!controllerName.Equals(controller, StringComparison.OrdinalIgnoreCase))
{
result = null;
}
return result;
}
You can use it in your view like this:
<!-- Make the list item active when the current controller is equal to "blog" -->
<li class="#Url.MakeActive("blog")">
....
</li>

The JavaScript isn't working because the page is getting reloaded after it runs. So it correctly sets the active item and then the page loads because the browser is following the link. Personally, I would remove the JavaScript you have because it serves no purpose. To do this client side (instead of the server side code you have), you need JavaScript to set the active item when the new page loads. Something like:
$(document).ready(function() {
$('ul.nav.navbar-nav').find('a[href="' + location.pathname + '"]')
.closest('li').addClass('active');
});
I recommend adding an id or other class to your navbar so you can be sure you have selected the correct one.

Simplest thing to do is send a ViewBag parameter from your controllers like following;
public ActionResult About()
{
ViewBag.Current = "About";
return View();
}
public ActionResult Contact()
{
ViewBag.Current = "Contact";
return View();
}
In the cshtml page do the following;
<ul class="nav navbar-nav">
<li class="#(ViewBag.Current == "About" ? "active" : "")">#Html.ActionLink("About", "About", "Home")</li>
<li class="#(ViewBag.Current == "Contact" ? "active" : "")">#Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
Courtesy to #Damith in here

Simply you can do this in any view
<ul class="nav navbar-nav">
#Html.NavigationLink("Link1", "Index", "Home")
#Html.NavigationLink("Link2", "Index", "Home")
#Html.NavigationLink("Link3", "Index", "Home")
#Html.NavigationLink("Links with Parameter", "myAction", "MyController", new{ id=999}, new { #class= " icon-next" })
</ul>
After you add this method to a new class or existing HtmlExtensions class
public static class HtmlExtensions
{
public static MvcHtmlString NavigationLink(this HtmlHelper html, string linkText, string action, string controller, object routeValues=null, object css=null)
{
TagBuilder aTag = new TagBuilder("a");
TagBuilder liTag = new TagBuilder("li");
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(css);
string url = (routeValues == null)?
(new UrlHelper(html.ViewContext.RequestContext)).Action(action, controller)
:(new UrlHelper(html.ViewContext.RequestContext)).Action(action, controller, routeValues);
aTag.MergeAttribute("href", url);
aTag.InnerHtml = linkText;
aTag.MergeAttributes(htmlAttributes);
if (action.ToLower() == html.ViewContext.RouteData.Values["action"].ToString().ToLower() && controller.ToLower() == html.ViewContext.RouteData.Values["controller"].ToString().ToLower())
liTag.MergeAttribute("class","active");
liTag.InnerHtml = aTag.ToString(TagRenderMode.Normal);
return new MvcHtmlString(liTag.ToString(TagRenderMode.Normal));
}
}

I believe you have the selection backward. You're adding the class, then removing it from the siblings, and I think doing the remove second is causing the issue. Can you try reversing this to be:
<script type="text/javascript">
$(function () {
$('.navbar-nav li').click(function () {
$(this).siblings().removeClass('active');
$(this).addClass('active');
});
});
</script>

Your javascript function should work fine... The issue is that your links route to a controller and reload the entire page. In order to avoid this behavior you could render your body content as a partial view, that way the navbar elements do not reload. You shouldn't have to write a function to handle dom events - that is what javascript is for.
To see what I mean, change your code:
<li>About</li>
<li>Students Sample</li>
to:
<li>About</li>
<li>Students Sample</li>

Just add this JQuery coded :
<script>
$(document).ready(function () {
$('body').find('a[href="' + location.pathname + '"]')
.addClass('active');
});
</script>

With this simplified version of the code from here, you can use a tag helper to mark the anchor 'active' if the controller & action match (or just the controller if no action is supplied).
The nav:
<li class="nav-item px-2">
<a class="nav-link" asp-active-route asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item px-2">
<a class="nav-link" asp-active-route asp-controller="Car" asp-action="Add">Add Car</a>
</li>
The tag helper class
[HtmlTargetElement(Attributes = "asp-active-route")]
public class ActiveRouteTagHelper : TagHelper
{
[HtmlAttributeName("asp-controller")] public string Controller { get; set; }
[HtmlAttributeName("asp-action")] public string Action { get; set; }
[HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (IsActive())
{
output.AddClass("active", HtmlEncoder.Default);
}
output.Attributes.RemoveAll("asp-active-route");
base.Process(context, output);
}
private bool IsActive()
{
var currentController = ViewContext.RouteData.Values["Controller"].ToString();
var currentAction = ViewContext.RouteData.Values["Action"].ToString();
var active = false;
if (!string.IsNullOrWhiteSpace(Controller) && !string.IsNullOrWhiteSpace(currentController))
{
active = Controller.Equals(currentController, StringComparison.CurrentCultureIgnoreCase);
if (active && !string.IsNullOrWhiteSpace(Action) && !string.IsNullOrWhiteSpace(currentAction))
{
active = Action.Equals(currentAction, StringComparison.CurrentCultureIgnoreCase);
}
}
return active;
}
}
Don't forget to add the tag helper to your _ViewImports.cshtml
#addTagHelper *, My.Awesome.Web
Future improvement would be to also check areas and possibly wrap the entire nav in a control/helper so you only have to compare the routes once and flag the item

You can use Bootstrap's nav-pills to change the color of active menu item.
Bootstrap's nav-pills to change the color of active menu item

Here is a simple solution that works - store class name in TempData:
In each action of the controller add one line:
// add "active" class to nav-item TempData["Home"] = "active";
In Layout view nav item:
<a class="nav-link #TempData["Home"]">Home</a>
Thus only one nav-item will get class "active"

Related

Populate Navigation Bar based from Login using ASP.NET Core MVC

I have a problem and I don't know if this is the correct forum.
I have to create a system using ASP.NET Core MVC and the system have 10 plans. And in order to know the plan it will be based on the login.
And this is the sample navigation bar for (there will be lots of navigation bar and only some can be found for some plans like the order and report).
Plan 1
a. Orders
b. Reprint Receipt
c. Report
d. Setting
Plan 2
a. Orders
b. Buy
c. Report
d. Setting
Plan 3
a. Orders
b. Buy
c. Cancel Purchase
There are 10 plans and some have different navigations (redirects to a different page).
My plan is to create the pages for each plan.
But how can I populate the navigation bar for each plan?
I don't want to have many if statement in the view to hide some navigation bar for each plan.
How can I solve this? I'm thinking of having a config file. Then after the login read the config file in order to know the navigation for each plan.
Is this a correct solution for this? Or is there a better solution?
Thank you
To populate the navigation menu, first, I suggest you create a NavigationModel model to store the navigation text and the url, the NavigationModel model as below:
public class NavigationModel
{
public int Id { get; set; }
public string NavigationName { get; set; }
public string NavigationUrl { get; set; }
public string PlanType { get; set; }
}
And the following DataRepository contains the test data, in your application, you could store the data into database, then get the navigations via the DbContext:
public class DataRepository : IDataRepository
{
public List<NavigationModel> GetNavigations()
{
return new List<NavigationModel>()
{
new NavigationModel(){ Id =1001, NavigationName="Orders", NavigationUrl="Home/Orders", PlanType="Plan1"},
new NavigationModel(){ Id =1002, NavigationName="Reprint Receipt", NavigationUrl="Home/Reprint", PlanType="Plan1"},
new NavigationModel(){ Id =1003, NavigationName="Report", NavigationUrl="Home/Report", PlanType="Plan1"},
new NavigationModel(){ Id =1004, NavigationName="Setting", NavigationUrl="Home/Setting", PlanType="Plan1"},
new NavigationModel(){ Id =1005, NavigationName="Orders", NavigationUrl="Home/Orders", PlanType="Plan2"},
new NavigationModel(){ Id =1006, NavigationName="Buy", NavigationUrl="Home/Buy", PlanType="Plan2"},
new NavigationModel(){ Id =1007, NavigationName="Report", NavigationUrl="Home/Report", PlanType="Plan2"},
new NavigationModel(){ Id =1008, NavigationName="Setting", NavigationUrl="Home/Setting", PlanType="Plan2"},
new NavigationModel(){ Id =1009, NavigationName="Orders", NavigationUrl="Home/Orders", PlanType="Plan3"},
new NavigationModel(){ Id =10010, NavigationName="Buy", NavigationUrl="Home/Buy", PlanType="Plan3"},
new NavigationModel(){ Id =10011, NavigationName="Cancel Purchase", NavigationUrl="Home/CancelPurchase", PlanType="Plan3"},
};
}
}
Then, you could try to use the following methods to populate the navigation menu.
Use session to store the navigation information.
First, configure the application to use session and add the SessionExtensions (Note the session expired time)
Second, after user login successfully, you could refer the following code to get the relate navigation menu, and store them in the session:
public IActionResult Index()
{
var isUserLogin = true;
var plantype = "Plan2";
if (isUserLogin && _dataRepository.GetNavigations().Any(c => c.PlanType.Contains(plantype)))
{
var navigations = _dataRepository.GetNavigations().Where(c => c.PlanType == plantype).ToList();
if (HttpContext.Session.Get<List<NavigationModel>>("Navigation") == default)
{
HttpContext.Session.Set<List<NavigationModel>>("Navigation", navigations);
}
}
return View();
}
In the _Layout.cshtml page, add the following code in the header:
#using Microsoft.AspNetCore.Http
#inject IHttpContextAccessor HttpContextAccessor
[Note] In the Startup.ConfigureServices method, use services.AddHttpContextAccessor(); to register the HttpContextAccessor.
Then, we could access the session and populate the navigation menu:
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
#if (HttpContextAccessor.HttpContext.Session.Get<List<NavigationModel>>("Navigation") != default)
{
var navigations = HttpContextAccessor.HttpContext.Session.Get<List<NavigationModel>>("Navigation");
foreach (var item in navigations)
{
<li class="nav-item">
<a class="nav-link text-dark" href="#item.NavigationUrl">#item.NavigationName</a>
</li>
}
}
</ul>
Use JQuery Ajax to call the action method and populate the navigation menu:
In the Home Controller, create an action method to get the navigation menu:
[HttpPost]
public IActionResult GetNavigation(string plantype)
{
//check if user login and get the relate navigation menus.
return Json(_dataRepository.GetNavigations().Where(c => c.PlanType == plantype).ToList());
}
In the _Layout.cshtml page, use the following code to add the navigation menu:
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
<script>
$(function () {
$.ajax({
url: "Home/GetNavigation",
method: "Post",
data: { "plantype": "Plan2" },
success: function (items) {
for (var i = 0; i < items.length; i++) {
var li = document.createElement('li');
li.setAttribute('class', 'nav-item');
var a = document.createElement('a');
a.setAttribute('class', 'nav-link text-dark');
a.href = items[i].navigationUrl;
var linkText = document.createTextNode(items[i].navigationName);
a.appendChild(linkText);
li.appendChild(a);
$(".navbar-nav")[0].appendChild(li);
}
}
})
});
</script>
The result as below (display plan 2):

How to pass data from Controller to _Layout.cshtml

I show list of city in _Layout. I use a ViewBag for pass data to this,
#{
var cities = (List<CustomCityViewModel>)ViewBag.Cities;
}
<ul class="list-items">
#foreach (var city in cities)
{
<li>
<span>
#if (city.Id == null)
{
<a href="#Url.Action("Search","Home")">
#city.Name
</a>
}
else
{
<a class="city-search-item" data-name="#city.Name" data-id="#city.Id">
#city.Name
<i class="fa fa-angle-left"></i>
</a>
}
</span>
</li>
}
</ul>
I should set viewbag value in every action.
How can I send data so that it is not assigned inside each action?
//create a base Model class
public class BaseModel
{
public BaseModel(){
//here init base data to get in _layout
//sete data to cities
}
public IList<City> cites{set;get;}
}
// all other Model extend from BaseModel
// in your _Layout use BaseModel
#model BaseModel
#{
var cities =Model;
}
<ul class="list-items">
#foreach (var city in cities)
{
<li>
<span>
#if (city.Id == null)
{
<a href="#Url.Action("Search","Home")">
#city.Name
</a>
}
else
{
<a class="city-search-item" data-name="#city.Name" data-id="#city.Id">
#city.Name
<i class="fa fa-angle-left"></i>
</a>
}
</span>
</li>
}
</ul>

Separate ng-table pager template from the *.cshtml page and reuse when it requires

I'm using ngTable for showing data on my ASP.net MVC project.On that directive I use pager template as shown below.
Here is the Pagination template.
<!-- Pager Template -->
<script type="text/ng-template" id="custom/pager">
<ul class="pager ng-cloak">
<li ng-repeat="page in pages"
ng-class="{'disabled': !page.active, 'previous': page.type == 'prev', 'next': page.type == 'next'}"
ng-show="page.type == 'prev' || page.type == 'next'" ng-switch="page.type">
<a ng-switch-when="prev" ng-click="params.page(page.number)" href="">« Previous</a>
<a ng-switch-when="next" ng-click="params.page(page.number)" href="">Next »</a>
</li>
<li>
<div class="btn-group btn-group-sm">
<button type="button" ng-class="{'active':params.count() == 10}" ng-click="params.count(10)" class="btn btn-default">10</button>
<button type="button" ng-class="{'active':params.count() == 25}" ng-click="params.count(25)" class="btn btn-default">25</button>
<button type="button" ng-class="{'active':params.count() == 50}" ng-click="params.count(50)" class="btn btn-default">50</button>
<button type="button" ng-class="{'active':params.count() == 100}" ng-click="params.count(100)" class="btn btn-default">100</button>
</div>
</li>
</ul>
</script>
My question is how can I separate it from my *.cshtml file and use when it needs for the other pages as well ? At this moment where I use same code block again and again on each and every page.So if you can simulate your solution on Plunk, it's highly appreciate.
how about this
ControllerExtension .cs
public static class ControllerExtension
{
public static string RenderRazorViewToString(this Controller cont, string viewName, object model)
{
cont.ViewData.Model = model;
using (var sw = new StringWriter())
{
var viewResult = ViewEngines.Engines.FindPartialView(cont.ControllerContext,
viewName);
var viewContext = new ViewContext(cont.ControllerContext, viewResult.View,
cont.ViewData, cont.TempData, sw);
viewResult.View.Render(viewContext, sw);
viewResult.ViewEngine.ReleaseView(cont.ControllerContext, viewResult.View);
return sw.GetStringBuilder().ToString();
}
}
public static ContentResult NgTemplate(this Controller cont, string viewPath)
{
return new ContentResult()
{
Content = RenderRazorViewToString(cont,viewPath, null),
ContentEncoding = Encoding.Default,
ContentType = "text/ng-template"
};
}
}
the RenderRazorViewToString method was taken from Render a view as a string
your template
PaginationTemplate.cshtml
<ul class="pager ng-cloak">
...
</ul>
then asp net mvc controller,for example
HomeController
public ActionResult Template()
{
return this.NgTemplate("PaginationTemplate");
}
and then in some view ,for example
Index.cshtml
...
<table ng-table="tableParams" template-pagination="'/Home/Template'" class="table">
...
Note
don't forget to put url to action inside single quotes,here ' url '

How do I call a partial view that expects a IEnumerable collection in a view that gets a model object?

I am trying to build a version of WordPress in MVC4. Currently I am working on the page view.
I've decided that in this view I should also include a menu of all the other pages that have been created.
This is my method for the showPage view:
public ActionResult showPage(string myTitle)
{
var query = from a in db.Pages
where a.title == myTitle
select a;
PageModels item = new PageModels();
item = query.FirstOrDefault<PageModels>();
if (item != null)
{
return View(item);
}
else
{
item.content = "No page with title :" + myTitle + " found.";
return View(item);
}
}
This is my method for the partial I am trying to render:
public ActionResult List()
{
return View(db.Pages.ToList());
}
This is how my view looks like:
#model Dynamic_Web_Pages.Models.PageModels
#{
ViewBag.Title = "Page Preview";
}
#Html.Partial("_ListPartial", new IEnumerable<Dynamic_Web_Pages.Models.PageModels>)
<div class="content">#Html.Raw(Model.content)</div>
Finally this is my partial:
#model IEnumerable<Dynamic_Web_Pages.Models.PageModels>
<div class="dropdown">
#foreach (var page in Model)
{
if(page.parent == 0)
{
<div class="btn-group">
<a class="btn dropdown-toggle" id="#Html.DisplayFor(modelItem => page.id)" role="button" data-toggle="dropdown" data-target="#" href="/#Html.DisplayFor(modelItem => page.title)" >#Html.DisplayFor(modelItem => page.title)
<b class="caret"></b>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="#Html.DisplayFor(modelItem => page.id)">
#foreach (var child in Model)
{
if (child.parent == page.id)
{
<li><a class="child" href="/#Html.DisplayFor(modelItem => child.title)" >#Html.DisplayFor(modelItem => child.title)</a></li>
}
}
</ul>
</div>
}
}
</div>
I get the following error in my view:
Cannot create an instance of the abstract class or interface 'System.Collections.Generic.IEnumerable<Dynamic_Web_Pages.Models.PageModels>'
What should be the second argument of the #Html.Partial be?
Your view accepted a single instance of PageModels
#model Dynamic_Web_Pages.Models.PageModels
#{
ViewBag.Title = "Page Preview";
}
and yet you are passing that in your partial that accepts an IEnumerable and so you got that exception.
Now based on our conversation in your comments, you can load the partial using jquery:
<script>
$("#navmenu").load('#Url.Action("List")');
</script>
where you replace this code:
#Html.Partial("_ListPartial", new IEnumerable<Dynamic_Web_Pages.Models.PageModels>)
// with this
<div id="navmenu"></div>
remove new IEnumerable
and just keep it as
Html.Partial("_ListPartial") in the view
So you should return PartialView instead of view see below code
public ActionResult List()
{
return PartialView (db.Pages.ToList());
}

Convert HREF to RouteLink

It seems to be simple, but I can't get anything to work. This code was generated by my template generator and needs to be changed.
<li><a href="../Home/Contact" class="active"><span class="l"></span><span class="r">
</span><span class="t">Nous contacter</span></a> </li>
My best bet up to now is:
<li><span class="l"></span><span class="r"></span>
#Html.RouteLink("Contact", new { Controller = "Home", Action = "Contact" }, new { #class = "t" })</li>
But it doesn't do anything.
Just to make sur that my question is clear: The link works in both cases, that's fine. The formating doesn't work. That's my issue here.
The second will generate:
<li>
<span class="l"></span>
<span class="r"></span>
<a class="t" href="/Home/Contact">Contact</a>
</li>
which is different than what you had in the first place which might explain the formatting problems:
<li>
<a href="../Home/Contact" class="active">
<span class="l"></span>
<span class="r"></span>
<span class="t">Nous contacter</span>
</a>
</li>
The problem with Html helpers such as Html.ActionLink and RouteLink is that they by always Html encode the text, so you cannot use HTML as text. So one possibility is the following:
<li>
<a href="#Url.RouteUrl("Contact", new { controller = "home", action = "contact" })" class="active">
<span class="l"></span>
<span class="r"></span>
<span class="t">Nous contacter</span>
</a>
</li>
Another possibility if you have lots of those to generate is to write a custom Html helper that will do the job for you:
public static class HtmlExtensions
{
public static IHtmlString MyLink(
this HtmlHelper htmlHelper,
string linkText,
string routeName,
object routeValues
)
{
var spans = string.Format(
"<span class=\"l\"></span><span class=\"r\"></span><span class=\"t\">{0}</span>",
htmlHelper.Encode(linkText)
);
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var url = urlHelper.RouteUrl(routeName, routeValues);
var anchor = new TagBuilder("a");
var rvd = new RouteValueDictionary(routeValues);
var rd = htmlHelper.ViewContext.RouteData;
var currentAction = rd.GetRequiredString("action");
var currentController = rd.GetRequiredString("controller");
var controller = rvd["controller"] as string;
var action = rvd["action"] as string;
if (string.Equals(controller, currentController, StringComparison.OrdinalIgnoreCase) &&
string.Equals(action, currentAction, StringComparison.OrdinalIgnoreCase))
{
anchor.AddCssClass("active");
}
anchor.Attributes["href"] = url;
anchor.InnerHtml = spans;
return new HtmlString(anchor.ToString());
}
}
and then:
<li>
#Html.MyLink("Nous contacter", "Contact", new { controller = "home", action = "contact" })
</li>
Just use something like this:
#Url.Action("Index", "Home")

Resources