I have a User entity, and in various views, I want to create links to a user home page basically. This functionality should be available in different controllers, so I can easily redirect to the user's home page. Each user in my site has a role ; for example reader, writer, editor, manager and admin. Ideally, I want to try to achieve something like this:
In a controller, for example
public ActionResult SomeThingHere() {
return View(User.GetHomePage());
//OR
return RedirectToROute(User.GetHomePage());
}
in a View, I also want to use the same functionality, for example:
<%= Html.ActionLink("Link to home", user.GetHomePage() %>
Is it possible to achieve such a design in MVC? If so , how should I go about it?
I currently use a method like this, but it is only in one controller at the moment. Now I need to use the same code somewhere else and I am trying to figure out how I could refractor this and avoid repeating myself?
....
private ActionResult GetHomePage(User user){
if (user.IsInRole(Role.Admin))
return RedirectToAction("Index", "Home", new { area = "Admin" });
if (user.IsInRole(Role.Editor))
// Managers also go to editor home page
return RedirectToAction("Index", "Home", new {area = "Editor"});
if (user.IsInRole(Role.Reader))
// Writer and reader share the same home page
return RedirectToAction("Index", "Home", new { area = "Reader" });
return RedirectToAction("Index", "Home");
}
...
How about something like this:
private string GetArea(User u)
{
string area = string.empty;
if (User.IsInRole(Admin)) area = "admin";
else if (...)
return area;
}
I would suggest a custom extension to the HtmlHelper class. Top of my head (liable to have syntax errors), something like this
public static class RoleLinksExtension
{
public static string RoleBasedHomePageLink(this HtmlHelper helper, string text)
{
if (user.IsInRole(Role.Admin))
return helper.ActionLink(text, "Index", "Home", new { area = "Admin" });
// other role options here
return string.Empty; // or throw exception
}
}
Then it's just
<%= Html.RoleBasedHomePageLink("Link to home") %>
in your markup.
You don't really want to have a link to somewhere that simply redirects somewhere else, if you can avoid it.
Edit: No idea why I didn't think of this earlier, but if you do need to redirect (perhaps if you need some functionality before going to the home page), you could extend IPrinciple instead
public static class AreaHomePageExtensions
{
public static string GetArea(this IPrinciple user)
{
if (user.IsInRole(Role.Admin))
return "Admin";
// Other options here
}
}
Then you can do
return RedirectToAction("Index", "Home", new { area = User.GetArea() });
whenever you like.
Well I finally came up with a design that seems to work. I have written an controller extension,
with a GetHomePage Method. This extension can also be used in your views. Here is how I did It:
public static class UserHelperExtension {
public static string GetHomePage(this ControllerBase controller, User user) {
return = "http://" + controller.ControllerContext
.HttpContext.Request
.ServerVariables["HTTP_HOST"] + "/"
+ GetHomePage(user);
}
//need this for views
public static string GetHomePage(string httphost, User user) {
return = "http://" + httphost + "/" + GetHomePage(user});
}
private static string GetHomePage(User user) {
if (user.IsInRole(Role.Admin))
return "/Admin/Home/Index";
if (user.IsInRole(Role.Editor))
return "/Editor/Home/Index";
if (user.IsInRole(Role.Reader))
return "/Reader/Home/Index";
return "/Home/Index";
}
}
The action method in the controller looks like this:
using Extensions;
...
public ActionResult SomethingHere() {
return Redirect(this.GetHomePage(user));
}
...
In the view I have this:
...
<%# Import Namespace="Extensions"%>
<%=UserHelperExtension.GetHomePage(Request.ServerVariables["HTTP_HOST"], user)%>
...
The advantage is that I can easily use this "GetHomePage" method in various controllers,
or views thoughout my application, and the logic is in one place. The disadvantage is that
I would have preferred to have it more type safe. For example, in my orignal tests, I had access to RouteValues collection:
public void User_should_redirect_to_role_home(Role role,
string area, string controller, string action) {
...
var result = (RedirectToRouteResult)userController.SomeThingHere();
Assert.That(result.RouteValues["area"],
Is.EqualTo(area).IgnoreCase);
Assert.That(result.RouteValues["controller"],
Is.EqualTo(controller).IgnoreCase);
Assert.That(result.RouteValues["action"],
Is.EqualTo(action).IgnoreCase);
...
}
But now that I am using a string so it is not type safe, and checking the RedirectResult.Url.
...
var result = (RedirectResult) userController.SomethingHere();
Assert.That(result.Url.EndsWith("/" + area + "/" + controller + "/" + action),
Is.True);
...
Related
Is it possible to use the first code to then add it to the Global file (second code)? I can't seem to get it to work. I want to display string user from the second code in the global.asax.
Thanks,
EB
void Session_Start(object sender, EventArgs e)
{
string username = HttpContext.Current.User.Identity.Name;
Session["Username"] = username;
}
Add This:
using (var context = new PrincipalContext(ContextType.Domain, "Domain.local"))
{
var user = UserPrincipal.FindByIdentity(context, username);
}
You can use User.Identity.Name both in Controller and Global.asax.cs.
This User comes from Base Controller Class which in inherited by your custom controller.
If you want to get names in your plain class files you can use these:
System.Security.Principal.WindowsIdentity.GetCurrent().Name
System.Web.HttpContext.Current.User.Identity.Name
In layout I am using this:
<a href="#" id="userRole">
<i class="fas fa-user" style="font-size:14px;"></i> #Business.FindPersonByMSID(User.Identity.Name.Substring(3))
</a>
You can use
#if(User.Identity.Name) in .cshtml anywhere.
Business.FindPersonByMSID is a static method in my Business class I am using to get the Full name of the person by his MSID or NTID .
You did not ask for below code but it will give you full view of what I am doing.
public static string FindPersonByMSID(string msid)
{
string displayName = "";
if (msid.Trim() != "")
{
DirectorySearcher searcher = new DirectorySearcher();
searcher.Filter = string.Format("(&(objectCategory=person)(objectClass=user)(cn={0}))", msid);
SearchResult allResults;
allResults = searcher.FindOne();
DirectoryEntry deMembershipUser = allResults.GetDirectoryEntry();
deMembershipUser.RefreshCache();
displayName = (string)deMembershipUser.Properties["displayname"].Value;
}
return displayName;
}
My hope is to provide a method to end users that will let them enter a value 'SmallBuildingCompany', and then use this value to make a custom url that will redirect to an informational view. so for example, www.app.com/SmallBuildingCompany. Can anyone point me to some information to help on this?
edited 161024
My attempt so far:
I added this within RouteConfig.
RouteTable.Routes.MapRoute(
"Organization",
"O/{uniqueCompanyName}",
new { controller = "Organization", action = "Info" }
and added a new controller method and view under the organization controller.
public async Task<ActionResult> Info(string uniqueCompanyName)
{
var Org = db.Organizations.Where(u => u.uniqueCompanyName == uniqueCompanyName).FirstOrDefault();
Organization organization = await db.Organizations.FindAsync(Org.OrgId);
return View("Info");
}
You can achieve this by using the SmallBuildingCompany part of the URL as a parameter for an action that is used to display every informational view.
Set up the Route in Global.asax.cs to extract the company name as parameter and pass it to the Index action of CompanyInfoController:
protected void Application_Start() {
// Sample URL: /SmallBuildingCompany
RouteTable.Routes.MapRoute(
"CompanyInfo",
"{uniqueCompanyName}",
new { controller = "CompanyInfo", action = "Index" }
);
}
Note that this Route will probably break the default route ({controller}/{action}/{id}), so maybe you want to prefix your "Info" route:
protected void Application_Start() {
// Sample URL: Info/SmallBuildingCompany
RouteTable.Routes.MapRoute(
"CompanyInfo",
"Info/{uniqueCompanyName}",
new { controller = "CompanyInfo", action = "Index" }
);
}
Then the CompanyInfoController Index action can use the uniqueCompanyName parameter to retrieve the infos from the database.
public ActionResult Index(string uniqueCompanyName) {
var company = dbContext.Companies.Single(c => c.UniqueName == uniqueCompanyName);
var infoViewModel = new CompanyInfoViewModel {
UniqueName = company.UniqueName
}
return View("Index", infoViewModel);
}
ASP.NET Routing
Here's the problem that I have been tearing hair out over since Friday.
I have a single MVC application that serves several different subgroups for a single client. For branding and for some style elements, the Url's need to be formatted like:
www.site.com/Login
www.site.com/Client1/Login
www.site.com/Client2/Login
www.site.com/Client3/Login
and so on.
We would also like to maintain this structure, moving onto www.site.com/Client1/News, etc.
Static routing is off the table. Even a tool to generate them. The site will have X pages with a unique route for Y clients, and I shudder to think about the performance. And because of teh dynamic nature, creating virtual dirs on the fly is not a route I want to travel down.
At first the solution seemed trivial. I tried two test solutions.
The first derived from CustomRouteBase and was able to determine the correct route in the overridden GetRouteData method and then generate the correct Url using GetVirtualPath. The second solution used constraints to see if a client was in the pattern and route accordingly. Both would hit the correct controllers and generate correct links.
Then I added areas (this is just a prototype, but the real site uses areas).
Both solutions failed. The areas were registered properly and worked as they typically should. But with the first solution, I could not find a way to override GetVirtualPath for area registration. I know there is an extension methods off the Area class, but this doesn't fit what I need.
I also tried using a constraint, but the "client" part of the Url was not being added to any of the action links to the areas and trying to route to a controller using a constraint gave me the good old view not found error (searching from the root even though I had specified the area in the route).
So my first question is am I going about this the wrong way? If there is a better way to accomplish this, I am all eras. If not, then how do I manage areas?
I could put some code up here, but it all works for what I want it to do. I'm sort of lost at how to approach the area issue. But unfortunately as always I inherited this project 3 months before launch and my team simply doesn't have the resources to restructure the site without them.
#Max... I tried something similar, but thh areas still would not display their links correctly when in www.site.com/Client1... This is from prototype 6, I tried a few different ways.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//throws view engine can't find view error. If I use www.website.com/Client1, hvering over area links does not give the correct path
routes.MapRoute("Area",
"{folder}/{area}/{controller}/{action}/{id}",
new { area = "Authentication", controller = "Authentication", action = "Index", id = UrlParameter.Optional },
new { folder = new IsFolderContsraint(), area = new IsArea() }
);
//works fine.. if I use www.website.com/Client1, hovering over regular (non area) links works fine
routes.MapRoute("Branded",
"{folder}/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { folder = new IsFolderContsraint() }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
Here is another way I tried to tackle it. Please don't crack on the implementation, it's just a POC. The problem here was that Areas are not part of RouteBase, so I can't modify the virtual paths for them. So every action link, etc, get's rendered correctly and all works well as long as the action is not in an area.
public class Folders
{
private static Dictionary<string, string> _folders= new Dictionary<string, string>()
{ {"test1", "style1"},
{"test2", "style2"},
{"test3", "style3"}
};
public static Dictionary<string, string> FolderNames { get { return _folders; } }
}
public class AreaDefinitions
{
private static Dictionary<string, string> _areas = new Dictionary<string, string>()
{ {"Authentication", "Authentication"} };
public static Dictionary<string, string> AreaDefinition { get { return _areas; } }
}
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new CustomRouteBase());
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
}
public class CustomRouteBase : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
//Creates routes based on current app execution pass
//a bit like doing my own routing constraints, but is more dynamic.
//get route data (and CreateVirtualPath) will be called for any action needing to be rendered in the current view.
//But not for areas :( Ugly but just a prototype
string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
url = url.StartsWith("~/") ? url.Substring(2, url.Length - 2) : url;
string[] urlParts = url.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
RouteData rd = new RouteData(this, new MvcRouteHandler());
if (urlParts.Length == 0)
{
rd.Values.Add("controller", urlParts.Length > 0 ? urlParts[0] : "Home");
rd.Values.Add("action", urlParts.Length > 1 ? urlParts[1] : "Index");
return rd;
}
if (Folders.FolderNames.ContainsKey(urlParts[0]))
{
if (urlParts.Length > 1 && AreaDefinitions.AreaDefinition.ContainsKey(urlParts[1]))
{
rd.DataTokens["area"] = urlParts[1];
rd.Values.Add("controller", urlParts.Length > 2 ? urlParts[2] : "Home");
rd.Values.Add("action", urlParts.Length > 3 ? urlParts[3] : "Index");
}
else
{
rd.Values.Add("controller", urlParts.Length > 1 ? urlParts[1] : "Home");
rd.Values.Add("action", urlParts.Length > 2 ? urlParts[2] : "Index");
}
rd.DataTokens.Add("folder", urlParts[0]);
}
else
{
rd.Values.Add("controller", urlParts.Length > 0 ? urlParts[0] : "Home");
rd.Values.Add("action", urlParts.Length > 1 ? urlParts[1] : "Index");
}
return rd;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//Assembled virtial path here using route and values.
//Worked (ugly but functioned, but was never called for areas.)
string url = "";
if (requestContext.RouteData.DataTokens.ContainsKey("folder"))
url += requestContext.RouteData.DataTokens["folder"] + "/";
if (values.ContainsKey("controller"))
url += values["controller"] + "/";
if (values.ContainsKey("action"))
url += values["action"];
var vpd = new VirtualPathData(requestContext.RouteData.Route, url);
return vpd;
}
}
Thanks Liam... that's similar to how our view engine is customized, to read from a "plugins" folder first for view overrides. But the problem is a little different here. This is one client that already has a site with view overrides, but in turn has multiple clients of their own. The behavior here is just to control style sheets, logos, etc. After a user is logged in, I can identify what the style and branding should be, but for landing pages the client wants to use a(n) url like "www.site.com/Client1" to identify this. I've given up and written a handler that just turns the request into www.site.com/?client=client1 so I can handle landing page styling, but it would be so much nicer to leave the url as www.ste.com/Client1/login, etc. This is a conversion from a classic asp app that used vdirs to host different directories prior to this. Because of the number of potential clients (100's), static routing gets heavy. My solutions all work to a point... it's the areas that are causing all the problems. If I could just find a way to remap their virtual paths dynamically like I can with the routes I create in RouteBase, I would be in business... I think.
How can I get pretty urls like localhost:8888/News/Example-post instead of localhost:8888/Home/Details/2
My HomeController has the following for the Details method
public ActionResult Details(int id)
{
var ArticleToView = (from m in _db.ArticleSet where m.storyId == id select m).First();
return View(ArticleToView);
As the ASP.NET routing system is somewhat complicated, there are many ways to accomplish what you describe.
First of all, do you just want to have a pretty URL for the Details method? If so, you might consider renaming HomeController to NewsController or moving the Details method into a new NewsController class - that will automatically form the /News part of the URL. If you don't want a /Details part, you might rename your Details method Index, as that will be automatically called by /News. Finally, you need to change your int id parameter into string name.
If you want many custom URLs, you're going to have to define your own routes. Here are two ways of doing this:
1.
The easiest way I've found is to use an ASP.NET MVC Attribute-Based Route Mapper. That way, all you have to do is add an attribute on each method you want a pretty URL for and specify what URL you want.
First, you must follow a few steps to set up the attribute-based route mapping system, as outlined on that link.
After completing those steps, you must change your method to look like this:
[Url("News/{name}")]
public ActionResult Details(string name)
{
var ArticleToView = (from m in _db.ArticleSet where m.storyName == name select m).First();
return View(ArticleToView);
}
2.
Alternatively, you can define your custom routes manually in Global.asax.cs. In your RegisterRoutes method, you can add the following in the middle:
routes.MapRoute(
"NewsDetails",
"News/{name}",
new { controller = "News", action = "Details", name = "" }
);
What I do on my sites is that I check the URL against either the Page Title or Page Stub in cases where the page titles could have the same name for instance if you have a site that posts a "Picture of the Week" you may want to use a stub instead of title as you'll have multiples named the same thing.
URLs look like this: http://mySite.com/Page/Verse-of-the-Week
Global.asax contains this:
routes.MapRoute("Pages", "{controller}/{pageID}", new { controller = "Page", action = "Index", pageID = "Home" });
PageController is this:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Index(string pageID)
{
if (pageID == null)
{
pageID = pageRepository.HomeOrLowest();
}
var p = pageRepository.ByStub(pageID);
if (p == null) { return RedirectToAction("NotFound", "Error"); }
return View(p);
}
The repository looks like this:
private static Func<mvCmsContext, string, Page> _byStub =
CompiledQuery.Compile((mvCmsContext context, string pageTitle) =>
(from p in context.Pages
where p.pageTitle.Replace(" ", "-") == pageTitle
select p).SingleOrDefault());
public Page ByStub(string pageTitle)
{
return _byStub(context, pageTitle);
}
I hope that helps.
Edit to add duplicate handling:
private static Func<mvCmsContext, string, int> _pageExists =
CompiledQuery.Compile((mvCmsContext context, string pageTitle) =>
(from p in context.Pages
where p.pageTitle.Replace(" ", "-") == pageTitle
select p).Count());
public bool PageExists(string pageTitle)
{
return Convert.ToBoolean(_pageExists(context, pageTitle));
}
Validates like this:
IValidationErrors errors = new ValidationErrors();
if (CreateOrEdit == "Create")
{
if (pageRepository.PageExists(model.pageTitle) && !String.IsNullOrEmpty(model.pageTitle))
errors.Add("pageTitle", "A page with this title already exists. Please edit it and try again.");
}
Please check out this package I've created: https://www.nuget.org/packages/LowercaseDashedRoute/
And read the one-line configuration here: https://github.com/AtaS/lowercase-dashed-route
How can dynamic breadcrumbs be achieved with ASP.net MVC?
If you are curious about what breadcrumbs are:
What are breadcrumbs? Well, if you have ever browsed an online store or read posts in a forum, you have likely encountered breadcrumbs. They provide an easy way to see where you are on a site. Sites like Craigslist use breadcrumbs to describe the user's location. Above the listings on each page is something that looks like this:
s.f. bayarea craigslist > city of san francisco > bicycles
EDIT
I realize what is possible with the SiteMapProvider. I am also aware of the providers out there on the net that will let you map sitenodes to controllers and actions.
But, what about when you want a breadcrumb's text to match some dynamic value, like this:
Home > Products > Cars > Toyota
Home > Products > Cars > Chevy
Home > Products > Execution Equipment > Electric Chair
Home > Products > Execution Equipment > Gallows
... where the product categories and the products are records from a database. Some links should be defined statically (Home for sure).
I am trying to figure out how to do this, but I'm sure someone has already done this with ASP.net MVC.
Sitemap's are definitely one way to go... alternatively, you can write one yourself! (of course as long as standard MVC rules are followed)... I just wrote one, I figured I would share here.
#Html.ActionLink("Home", "Index", "Home")
#if(ViewContext.RouteData.Values["controller"].ToString() != "Home") {
#:> #Html.ActionLink(ViewContext.RouteData.Values["controller"].ToString(), "Index", ViewContext.RouteData.Values["controller"].ToString())
}
#if(ViewContext.RouteData.Values["action"].ToString() != "Index"){
#:> #Html.ActionLink(ViewContext.RouteData.Values["action"].ToString(), ViewContext.RouteData.Values["action"].ToString(), ViewContext.RouteData.Values["controller"].ToString())
}
Hopefully someone will find this helpful, this is exactly what I was looking for when I searched SO for MVC breadcrumbs.
ASP.NET 5 (aka ASP.NET Core), MVC Core Solution
In ASP.NET Core, things are further optimized as we don't need to stringify the markup in the extension method.
In ~/Extesions/HtmlExtensions.cs:
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace YourProjectNamespace.Extensions
{
public static class HtmlExtensions
{
private static readonly HtmlContentBuilder _emptyBuilder = new HtmlContentBuilder();
public static IHtmlContent BuildBreadcrumbNavigation(this IHtmlHelper helper)
{
if (helper.ViewContext.RouteData.Values["controller"].ToString() == "Home" ||
helper.ViewContext.RouteData.Values["controller"].ToString() == "Account")
{
return _emptyBuilder;
}
string controllerName = helper.ViewContext.RouteData.Values["controller"].ToString();
string actionName = helper.ViewContext.RouteData.Values["action"].ToString();
var breadcrumb = new HtmlContentBuilder()
.AppendHtml("<ol class='breadcrumb'><li>")
.AppendHtml(helper.ActionLink("Home", "Index", "Home"))
.AppendHtml("</li><li>")
.AppendHtml(helper.ActionLink(controllerName.Titleize(),
"Index", controllerName))
.AppendHtml("</li>");
if (helper.ViewContext.RouteData.Values["action"].ToString() != "Index")
{
breadcrumb.AppendHtml("<li>")
.AppendHtml(helper.ActionLink(actionName.Titleize(), actionName, controllerName))
.AppendHtml("</li>");
}
return breadcrumb.AppendHtml("</ol>");
}
}
}
~/Extensions/StringExtensions.cs remains the same as below (scroll down to see the MVC5 version).
In razor view, we don't need Html.Raw, as Razor takes care of escaping when dealing with IHtmlContent:
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
#Html.BuildBreadcrumbNavigation()
<!-- #endregion -->
#RenderBody()
<hr />
...
...
ASP.NET 4, MVC 5 Solution
=== ORIGINAL / OLD ANSWER BELOW ===
(Expanding on Sean Haddy's answer above)
If you want to make it extension-driven (keeping Views clean), you can do something like:
In ~/Extesions/HtmlExtensions.cs:
(compatible with MVC5 / bootstrap)
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace YourProjectNamespace.Extensions
{
public static class HtmlExtensions
{
public static string BuildBreadcrumbNavigation(this HtmlHelper helper)
{
// optional condition: I didn't wanted it to show on home and account controller
if (helper.ViewContext.RouteData.Values["controller"].ToString() == "Home" ||
helper.ViewContext.RouteData.Values["controller"].ToString() == "Account")
{
return string.Empty;
}
StringBuilder breadcrumb = new StringBuilder("<ol class='breadcrumb'><li>").Append(helper.ActionLink("Home", "Index", "Home").ToHtmlString()).Append("</li>");
breadcrumb.Append("<li>");
breadcrumb.Append(helper.ActionLink(helper.ViewContext.RouteData.Values["controller"].ToString().Titleize(),
"Index",
helper.ViewContext.RouteData.Values["controller"].ToString()));
breadcrumb.Append("</li>");
if (helper.ViewContext.RouteData.Values["action"].ToString() != "Index")
{
breadcrumb.Append("<li>");
breadcrumb.Append(helper.ActionLink(helper.ViewContext.RouteData.Values["action"].ToString().Titleize(),
helper.ViewContext.RouteData.Values["action"].ToString(),
helper.ViewContext.RouteData.Values["controller"].ToString()));
breadcrumb.Append("</li>");
}
return breadcrumb.Append("</ol>").ToString();
}
}
}
In ~/Extensions/StringExtensions.cs:
using System.Globalization;
using System.Text.RegularExpressions;
namespace YourProjectNamespace.Extensions
{
public static class StringExtensions
{
public static string Titleize(this string text)
{
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text).ToSentenceCase();
}
public static string ToSentenceCase(this string str)
{
return Regex.Replace(str, "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1]));
}
}
}
Then use it like (in _Layout.cshtml for example):
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
#Html.Raw(Html.BuildBreadcrumbNavigation())
<!-- #endregion -->
#RenderBody()
<hr />
...
...
There is a tool to do this on codeplex: http://mvcsitemap.codeplex.com/ [project moved to github]
Edit:
There is a way to derive a SiteMapProvider from a database: http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx
You might be able to modify the mvcsitemap tool to use that to get what you want.
I built this nuget package to solve this problem for myself:
https://www.nuget.org/packages/MvcBreadCrumbs/
You can contribute here if you have ideas for it:
https://github.com/thelarz/MvcBreadCrumbs
For those using ASP.NET Core 2.0 and looking for a more decoupled approach than vulcan's HtmlHelper, I recommend having a look at using a partial view with dependency injection.
Below is a simple implementation which can easily be molded to suit your needs.
The breadcrumb service (./Services/BreadcrumbService.cs):
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;
namespace YourNamespace.YourProject
{
public class BreadcrumbService : IViewContextAware
{
IList<Breadcrumb> breadcrumbs;
public void Contextualize(ViewContext viewContext)
{
breadcrumbs = new List<Breadcrumb>();
string area = $"{viewContext.RouteData.Values["area"]}";
string controller = $"{viewContext.RouteData.Values["controller"]}";
string action = $"{viewContext.RouteData.Values["action"]}";
object id = viewContext.RouteData.Values["id"];
string title = $"{viewContext.ViewData["Title"]}";
breadcrumbs.Add(new Breadcrumb(area, controller, action, title, id));
if(!string.Equals(action, "index", StringComparison.OrdinalIgnoreCase))
{
breadcrumbs.Insert(0, new Breadcrumb(area, controller, "index", title));
}
}
public IList<Breadcrumb> GetBreadcrumbs()
{
return breadcrumbs;
}
}
public class Breadcrumb
{
public Breadcrumb(string area, string controller, string action, string title, object id) : this(area, controller, action, title)
{
Id = id;
}
public Breadcrumb(string area, string controller, string action, string title)
{
Area = area;
Controller = controller;
Action = action;
if (string.IsNullOrWhiteSpace(title))
{
Title = Regex.Replace(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(string.Equals(action, "Index", StringComparison.OrdinalIgnoreCase) ? controller : action), "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1]));
}
else
{
Title = title;
}
}
public string Area { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public object Id { get; set; }
public string Title { get; set; }
}
}
Register the service in startup.cs after AddMvc():
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<BreadcrumbService>();
Create a partial to render the breadcrumbs (~/Views/Shared/Breadcrumbs.cshtml):
#using YourNamespace.YourProject.Services
#inject BreadcrumbService BreadcrumbService
#foreach(var breadcrumb in BreadcrumbService.GetBreadcrumbs())
{
<a asp-area="#breadcrumb.Area" asp-controller="#breadcrumb.Controller" asp-action="#breadcrumb.Action" asp-route-id="#breadcrumb.Id">#breadcrumb.Title</a>
}
At this point, to render the breadcrumbs simply call Html.Partial("Breadcrumbs") or Html.PartialAsync("Breadcrumbs").
Maarten Balliauw's MvcSiteMapProvider worked pretty well for me.
I created a small mvc app to test his provider: MvcSiteMapProvider Test (404)
For whoever is interested, I did an improved version of a HtmlExtension that is also considering Areas and in addition uses Reflection to check if there is a Default controller inside an Area or a Index action inside a Controller:
public static class HtmlExtensions
{
public static MvcHtmlString BuildBreadcrumbNavigation(this HtmlHelper helper)
{
string area = (helper.ViewContext.RouteData.DataTokens["area"] ?? "").ToString();
string controller = helper.ViewContext.RouteData.Values["controller"].ToString();
string action = helper.ViewContext.RouteData.Values["action"].ToString();
// add link to homepage by default
StringBuilder breadcrumb = new StringBuilder(#"
<ol class='breadcrumb'>
<li>" + helper.ActionLink("Homepage", "Index", "Home", new { Area = "" }, new { #class="first" }) + #"</li>");
// add link to area if existing
if (area != "")
{
breadcrumb.Append("<li>");
if (ControllerExistsInArea("Default", area)) // by convention, default Area controller should be named Default
{
breadcrumb.Append(helper.ActionLink(area.AddSpaceOnCaseChange(), "Index", "Default", new { Area = area }, new { #class = "" }));
}
else
{
breadcrumb.Append(area.AddSpaceOnCaseChange());
}
breadcrumb.Append("</li>");
}
// add link to controller Index if different action
if ((controller != "Home" && controller != "Default") && action != "Index")
{
if (ActionExistsInController("Index", controller, area))
{
breadcrumb.Append("<li>");
breadcrumb.Append(helper.ActionLink(controller.AddSpaceOnCaseChange(), "Index", controller, new { Area = area }, new { #class = "" }));
breadcrumb.Append("</li>");
}
}
// add link to action
if ((controller != "Home" && controller != "Default") || action != "Index")
{
breadcrumb.Append("<li>");
//breadcrumb.Append(helper.ActionLink((action.ToLower() == "index") ? controller.AddSpaceOnCaseChange() : action.AddSpaceOnCaseChange(), action, controller, new { Area = area }, new { #class = "" }));
breadcrumb.Append((action.ToLower() == "index") ? controller.AddSpaceOnCaseChange() : action.AddSpaceOnCaseChange());
breadcrumb.Append("</li>");
}
return MvcHtmlString.Create(breadcrumb.Append("</ol>").ToString());
}
public static Type GetControllerType(string controller, string area)
{
string currentAssembly = Assembly.GetExecutingAssembly().GetName().Name;
IEnumerable<Type> controllerTypes = Assembly.GetExecutingAssembly().GetTypes().Where(o => typeof(IController).IsAssignableFrom(o));
string typeFullName = String.Format("{0}.Controllers.{1}Controller", currentAssembly, controller);
if (area != "")
{
typeFullName = String.Format("{0}.Areas.{1}.Controllers.{2}Controller", currentAssembly, area, controller);
}
return controllerTypes.Where(o => o.FullName == typeFullName).FirstOrDefault();
}
public static bool ActionExistsInController(string action, string controller, string area)
{
Type controllerType = GetControllerType(controller, area);
return (controllerType != null && new ReflectedControllerDescriptor(controllerType).GetCanonicalActions().Any(x => x.ActionName == action));
}
public static bool ControllerExistsInArea(string controller, string area)
{
Type controllerType = GetControllerType(controller, area);
return (controllerType != null);
}
public static string AddSpaceOnCaseChange(this string text)
{
if (string.IsNullOrWhiteSpace(text))
return "";
StringBuilder newText = new StringBuilder(text.Length * 2);
newText.Append(text[0]);
for (int i = 1; i < text.Length; i++)
{
if (char.IsUpper(text[i]) && text[i - 1] != ' ')
newText.Append(' ');
newText.Append(text[i]);
}
return newText.ToString();
}
}
If can definitely can be improved (probably does not cover all the possible cases), but it did not failed me until now.