I'm trying to learn ASP.NET MVC, and I want to have menus highlighted on the item that is currently selected. I know I did this before in web forms (although I don't actually remember how at the moment, but somehow with the sitemap). But how can this be done in MVC?
It seems like such a basic thing it should be simple to do in MVC? Sure, I can do it by adding CSS rules that are coupled between a body id and an li id from the menu (#home #homeli [style as current]), but it seems that would quickly become unwieldy, especially if there are also a lot of sub menus besides the main navigation (in several of the sub pages I have a sub menu in a contentplaceholder. BTW, I guess that's the only way to do it in MVC? In web forms the sub menus too could be handled by the sitemap, but I haven't seen a way to do this in MVC...)
Any suggestions?
Here is a tutorial that provides a very clean way to achieve this kind of menu:
http://www.dev102.com/2009/04/14/creating-a-tabbed-menu-control-for-aspnet-mvc/
The magic bit figuring out whether or not a menu item is active happens in the helper method that renders the items:
public static class MyHtmlHelper
{
public static string TabbedMenu(this HtmlHelper helper, IEnumerable<MenuTab> tabs)
{
var route = helper.ViewContext.RequestContext.RouteData;
//This is the current controller
var controller = route.GetRequiredString("controller");
var action = route.GetRequiredString("action");
var menu = "\n\n<ul id=\"menu\">";
foreach (var tab in tabs)
{
//if the menu controller and action match current controller and action, mark it as selected
if (controller == tab.Controller && action == tab.Action)
menu += "\n\t<li>" + helper.ActionLink(tab.Text, tab.Action,
tab.Controller, new { #class = "selected" }) + "</li>";
else
menu += "\n\t<li>" + helper.ActionLink(tab.Text,
tab.Action, tab.Controller) + "</li>";
}
menu += "\n</ul>\n\n";
return menu;
}
}
MenuTab class:
public class MenuTab
{
private MenuTab(string text, string action, string controller)
{
Text = text;
Action = action;
Controller = controller;
}
public static MenuTab Create(string text, string action, string controller)
{
return new MenuTab(text, action, controller);
}
public string Text { get; private set; }
public string Action { get; private set; }
public string Controller { get; private set; }
}
Usage:
<%= Html.TabbedMenu(new List<MenuTab> {
MenuTab.Create("Home", "Index", "Home"),
MenuTab.Create("About", "About", "Home"),
MenuTab.Create("Services", "Services", "Home"),
MenuTab.Create("Pricing", "Pricing", "Home"),
MenuTab.Create("Contact", "Contact", "Home")
}) %>
Menu item per Controller
#{
var currentController = (string)ViewContext.RouteData.Values["controller"];
Func<string, object> htmlAttributesFactory =
controller => currentController == controller ? new {#class = "selected"} : null;
Func<string, string, MvcHtmlString> menuItemFactory =
(title, controller) =>
Html.RouteLink(
title,
new {controller},
htmlAttributesFactory(controller));
}
#menuItemFactory("Home", "Home")
#menuItemFactory("Pending", "Pending")
Related
I have read some similar topics here and on the web, but I don't think I have seen one that would classify this as a duplicate, so I am going to go ahead and post it. I am currently loading my dynamic menus from the database like so:
public void LoadMenus()
{
var dbContext = new ContentClassesDataContext();
var menus = from m in dbContext.Menus
where m.MenuName != "Home" && m.MenuGroup == "RazorHome" && m.RoleID == "Facility"
orderby m.Sequence, m.MenuName
select m;
var html = "";
if (menus.Any())
{
html += "<span/>";
foreach (var menu in menus)
{
html = html + $"<a href='{menu.URL}'>{menu.MenuName}</a><br/>";
}
html += "<hr>";
}
Session["Menus"] = html;
}
LoadMenus() is in my controller class, so I am not able (to my knowledge) to use Razor syntax. I would prefer to load the menus from the view instead, so that I am able to use #Html.ActionLink(linkText, actionName, controllerName). Loading the HTML the way I am currently doing it will generate different link text depending on the current controller, so the links are not always correctly rendered. Is it possible to access the database from the view? Or perhaps to just pass in the content from the database from the controller to the view and then render the menu that way?
You should keep your html in the cshtml views.
You should pass the data through the viewmodel and not through the session.
1)
In the controller, get the menu data (in this example we fetch some fake data).
Create a viewmodel that can hold the menu data and pass it to the view, as shown below:
public class HomeController : Controller
{
public ActionResult Index()
{
var menu = GetMenu();
var vm = new ViewModel() {Menu = menu};
return View(vm);
}
private Menu GetMenu()
{
var menu = new Menu();
var menuItems = new List<MenuItem>();
menuItems.Add(new MenuItem() { LinkText = "Home" , ActionName = "Index", ControllerName = "Home"});
menuItems.Add(new MenuItem() { LinkText = "About", ActionName = "About", ControllerName = "Home" });
menuItems.Add(new MenuItem() { LinkText = "Help", ActionName = "Help", ControllerName = "Home" });
menu.Items = menuItems;
return menu;
}
}
2)
This is the viewmodel
public class ViewModel
{
public Menu Menu { get; set; }
}
This view is an example of how you could render the menu data as a html menu
#model WebApplication1.Models.ViewModel
<ul id="menu">
#foreach (var item in #Model.Menu.Items)
{
<li>#Html.ActionLink(#item.LinkText, #item.ActionName,
#item.ControllerName)</li>
}
</ul>
3)
This is the example menu classes used (representing your entities from the dbcontext)
public class Menu
{
public List<MenuItem> Items { get; set; }
}
public class MenuItem
{
public string LinkText { get; set; }
public string ActionName { get; set; }
public string ControllerName { get; set; }
}
Here are some links to get you started:
http://www.codeproject.com/Articles/585873/Basic-Understanding-On-ASP-NET-MVC
http://www.asp.net/mvc/overview/getting-started/introduction/getting-started
I am using MVC 5 with Umbraco 7. Trying to use ActionLink in my View but the markup it generates have empty href. any idea how to get it working?
Start Date
View:
#Html.ActionLink(
"Start Date",
"SearchV1",
"SearchV1",
new { sitetypeid = #Request.QueryString["sitetypeid"], leaNo = #Request.QueryString["leaNo"], orderBy = "VacStart" },
null)
Controller:
public class SearchV1Controller : RenderMvcController
{
public override ActionResult Index(RenderModel model)
{
return base.Index(model);
}
public ActionResult SearchV1(RenderModel model, int sitetypeId , int leaNo, string orderBy = "VacRelDate")
{
List<GetJobSearchResults_Result> searchResultsList = Workflow.Vacancy.GetJobSearchResults(sitetypeId , leaNo, "SCH", orderBy, "desc");
ViewBag.leaNo = leaNo;
return View(searchResultsList);
}
}
This cannot be possible in Umbraco unless you inherit your controller class from Surface controller.
I have a ListItem class that is used to represent menu items in my application:
public class ListItem : Entity
{
public virtual List List { get; set; }
public virtual ListItem ParentItem { get; set; }
public virtual ICollection<ListItem> ChildItems { get; set; }
public int SortOrder { get; set; }
public string Text { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string Area { get; set; }
public string Url { get; set; }
}
I use this data to construct the routes for the application, but I was wondering if there was a clean way to handle controller/views for static content? Basically any page that doesn't use any data but just views. Right now I have one controller called StaticContentController, which contains a unique action for each static page that returns the appropriate view like so:
public class StaticContentController : Controller
{
public ActionResult Books()
{
return View("~/Views/Books/Index.cshtml");
}
public ActionResult BookCategories()
{
return View("~/Views/Books/Categories.cshtml");
}
public ActionResult BookCategoriesSearch()
{
return View("~/Views/Books/Categories/Search.cshtml");
}
}
Is there some way I could minimize this so I don't have to have so many controllers/actions for static content? It seems like when creating my ListItem data I could set the Controller to a specific controller that handles static content, like I have done, but is there anyway to use one function to calculate what View to return? It seems like I still need separate actions otherwise I won't know what page the user was trying to get to.
The ListItem.Url contains the full URL path from the application root used in creating the route. The location of the View in the project would correspond to the URL location to keep the organization structure parallel.
Any suggestions? Thanks.
Edit: My Route registration looks like so:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("Shared/{*pathInfo}");
routes.MapRoute("Access Denied", "AccessDenied", new { controller = "Shared", action = "AccessDenied", area = "" });
List<ListItem> listItems = EntityServiceFactory.GetService<ListItemService>().GetAllListItmes();
foreach (ListItem item in listItems.Where(item => item.Text != null && item.Url != null && item.Controller != null).OrderBy(x => x.Url))
{
RouteTable.Routes.MapRoute(item.Text + listItems.FindIndex(x => x == item), item.Url.StartsWith("/") ? item.Url.Remove(0, 1) : item.Url, new { controller = item.Controller, action = item.Action ?? "index" });
}
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
You can use a single Action with one parameter (the View name) which will return all the static pages
public class StaticContentController : Controller
{
public ActionResult Page(string viewName)
{
return View(viewName);
}
}
You will also need to create a custom route for serving these views, for example:
routes.MapRoute(
"StaticContent", // Route name
"page/{viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" } // Parameter defaults
);
I see in your example that you specify different folders for your views. This solution will force you to put all static views in the Views folder of the StaticContentController.
If you must have custom folder structure, then you can change the route to accept / by adding * to the {viewName} like this {*viewname}. Now you can use this route: /page/Books/Categories. In the viewName input parameter you will receive "Books/Categories" which you can then return it as you like: return View(string.Format("~/Views/{0}.cshtml", viewName));
UPDATE (Avoiding the page/ prefix)
The idea is to have a custom constraint to check whether or not a file exists. Every file that exists for a given URL will be treated as static page.
public class StaticPageConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string viewPath = httpContext.Server.MapPath(string.Format("~/Views/{0}.cshtml", values[parameterName]));
return File.Exists(viewPath);
}
}
Update the route:
routes.MapRoute(
"StaticContent", // Route name
"{*viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" }, // Parameter defaults
new { viewName = new StaticPageConstraint() } // Custom route constraint
);
Update the action:
public ActionResult Page(string viewName)
{
return View(string.Format("~/Views/{0}.cshtml", viewName));
}
I'm trying to implement a menu for my MVC3 solution. In this menu, I have 2 kind of links:
classic links based on action + controller
Example 1: action = "Studies" + controller = "Main"
Example 2: action = "Contact" + controller = "Main"
a little more complex links based on action + controller + routeValues
Example 3: action = "List" + controller = "Project" + routeValues =
new { category = "BANK" }
Example 4: action = "List" + controller = "Project" + routeValues =
new { category = "PHARMA" }
The menu is displayed like this:
Studies
Contact
Bank
Pharma
...
I would like to select the currently active menu item based on the active page. To achieve this, I implement an htmlHelper like this:
public static MvcHtmlString ActionMenuItem(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
{
var route = htmlHelper.ViewContext.RequestContext.RouteData;
var controller = route.GetRequiredString("controller");
var action = route.GetRequiredString("action");
// some code here...
if ((controller == controllerName) && (action == actionName))
{
tag.AddCssClass("active");
}
else
{
tag.AddCssClass("inactive");
}
// some code here...
}
The problem with this basic implementation is that the condition to activate/inactivate menu item is based only on the action and controller values. I also need to check my routeValues for the "complex links" (example 3 & 4).
How can I implement this?
Thanks for your help.
public static MvcHtmlString ActionMenuItem(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
{
var route = htmlHelper.ViewContext.RequestContext.RouteData;
var rvd = HtmlHelper.AnonymousObjectToHtmlAttributes(routeValues);
// some code here...
if (IsRouteValuesMatch(rvd, route))
{
tag.AddCssClass("active");
}
else
{
tag.AddCssClass("inactive");
}
// some code here...
}
private static bool IsRouteValuesMatch(RouteValueDictionary rvd, RouteData routeData)
{
foreach (var item in rvd)
{
var value1 = item.Value as string;
var value2 = routeData.Values[item.Key] as string;
if (!string.Equals(value1, value2, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return true;
}
Would like to have clickable column titles eg click on TagCode once and it orders by that, and again it reverses. Same for Number.
Using MVC3/Razor and LightSpeed (ORM).
I know that a grid eg http://mvccontrib.codeplex.com/ may be the way forward. But as I don't need paging or filtering, I'd like to keep it simple for now.
Problem Is there a simple example of code (maybe with an up/down icon) that would help?
#Dave
Sorry, I missed your main point in my first answer
If you want to implement sorting using pure MVC3,
I can do that with following steps
list action method passing sortcolumn and order info to viewpage via ViewBag
Html Helper method to build actionlink with column ordering display
list viewpage calling the helper method
I uploaded source code here
List action method
public ActionResult Index(string sortColumn, bool? asc)
{
if (string.IsNullOrWhiteSpace(sortColumn))
sortColumn = "Number";
asc = asc ?? true;
SortDirection sortDirection = asc == true ? SortDirection.Ascending : SortDirection.Descending;
var query = _service.GetTags().OrderBy(sortColumn, sortDirection);
return View(query);
}
Html helper method
public static MvcHtmlString ActionLinkWithColumnOrder(this HtmlHelper helper,
string columnName,string action,string currentColumn,bool currentOrder)
{
object routeValues;
object htmlAttributes = null;
if (columnName == currentColumn)
{
routeValues = new { sortColumn = columnName, asc = !currentOrder };
htmlAttributes = new { #class = currentOrder ? "sort_asc" : "sort_desc" };
}
else
{
routeValues = new { sortColumn = columnName };
}
return helper.ActionLink(columnName, action, routeValues, htmlAttributes);
}
List View page
...
#Html.ActionLinkWithColumnOrder("TagCode", "Index", (string)ViewBag.sortColumn, (bool)ViewBag.asc)
...
#Html.ActionLinkWithColumnOrder("Number", "Index", (string)ViewBag.sortColumn, (bool)ViewBag.asc)
Happy Mvcing!
Sangsu PARK (http://supremeware.blogspot.com)
#Dave
How about using mvccontribgrid with ordering only as follows:
IMO, Using mvccontribgrid may bring more simple code.
This is controller code for example
public class HomeController : Controller
{
private AlbumService _service;
public HomeController()
{
_service = new AlbumService();
}
public ActionResult Index(GridSortOptions gridSortOptions)
{
var vm = new ViewModel<Album>()
{
DefaultSort = "AlbumId",
GridSortOptions = gridSortOptions,
List = _service.GetAlbums()
.OrderBy(gridSortOptions.Column, gridSortOptions.Direction),
};
return View(vm);
}
public ActionResult Details(int id)
{
var album = _service.GetAlbum(id);
ViewBag.RouteDicForList = Request.QueryString.ToRouteDic();
return View(album);
}
}
I attached simple sort function source code here with simple service & EF4.
Also, I posted full featured mvccontrib grid filtering & paging article here.