ASP.NET MVC5 Change Language using Route - asp.net-mvc

I'm new using MVC5 and I try to do a website with different language.
So far I do some configuration and when I click a different language it changes the url but it doesn't change the text in view.
Here is my code if anyone have idea:
In App_Data folder I have 2 files: CultureConstraint and CultureFilter
public class CultureFilter : IAuthorizationFilter
{
private readonly string defaultCulture;
public CultureFilter(string defaultCulture)
{
this.defaultCulture = defaultCulture;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var values = filterContext.RouteData.Values;
string culture = (string)values["culture"] ?? this.defaultCulture;
CultureInfo ci = new CultureInfo(culture);
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
public class CultureConstraint : IRouteConstraint
{
private readonly string defaultCulture;
private readonly string pattern;
public CultureConstraint(string defaultCulture, string pattern)
{
this.defaultCulture = defaultCulture;
this.pattern = pattern;
}
public bool Match(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration &&
this.defaultCulture.Equals(values[parameterName]))
{
return false;
}
else
{
return Regex.IsMatch((string)values[parameterName], "^" + pattern + "$");
}
}
}
In RouteConfig I add:
routes.MapRoute(
name: "DefaultWithCulture",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { culture = new CultureConstraint(defaultCulture: "en", pattern: "[a-z]{2}") }
);
In filterConfig:
filters.Add(new CultureFilter(defaultCulture: "en"));
In the _Layout View Page:
#using Test.App_LocalResources;
#{
var routeValues = this.ViewContext.RouteData.Values;
var controller = routeValues["controller"] as string;
var action = routeValues["action"] as string;
}
<ul>
<li>#Html.ActionLink("Russia", #action, #controller, new { culture = "ru" }, new { rel = "alternate", hreflang = "ru" })</li>
<li>#Html.ActionLink("English", #action, #controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" })</li>
</ul>
<div style="background: silver">
#GlobalRes.Money: <br />
#GlobalRes.DateAndTime: <br />
</div>
And Also in the View Page:
#using Test.App_LocalResources;
#{
ViewBag.Title = GlobalRes.Title;
}
I put the language file in App_LocalResources with the name Global.resx and Global.ru.resx.
So Title, Money and DateTime always get the english text never the russia one.
Did I miss anything?

Related

How to make MVC Routing handle url with dashes

I have a route defined as follows in MVC:
routes.MapRoute(
name: "ContentNavigation",
url: "{viewType}/{category}-{subCategory}",
defaults: new { controller = "Home", action = "GetMenuAndContent", viewType = String.Empty, category = String.Empty, subCategory = String.Empty });
If I navigate to http://example.com/something/category-and-this-is-a-subcategory
It fills the variables as:
viewType: "something"
category: "category-and-this-is-a"
subCategory: "subcategory".
What I want is for the word before the first dash to always go into category, and the remaining into subcategory. So it would produce:
viewType: "something"
category: "category"
subCategory: "and-this-is-a-subcategory"
How can I achieve this?
One possibility is to write a custom route to handle the proper parsing of the route segments:
public class MyRoute : Route
{
public MyRoute()
: base(
"{viewType}/{*catchAll}",
new RouteValueDictionary(new
{
controller = "Home",
action = "GetMenuAndContent",
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var catchAll = rd.Values["catchAll"] as string;
if (!string.IsNullOrEmpty(catchAll))
{
var parts = catchAll.Split(new[] { '-' }, 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1)
{
rd.Values["category"] = parts[0];
rd.Values["subCategory"] = parts[1];
return rd;
}
}
return null;
}
}
that you will register like that:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("ContentNavigation", new MyRoute());
...
}
Now assuming that the client requests /something/category-and-this-is-a-subcategory, then the following controller action will be invoked:
public class HomeController : Controller
{
public ActionResult GetMenuAndContent(string viewType, string category, string subCategory)
{
// viewType = "something"
// category = "category"
// subCategory = "and-this-is-a-subcategory"
...
}
}

stackoverflow URL rewrite

How does SO perform the URL rewrite if we only put in the question ID?
questions/{id}/{whatever}
to
questions/{id}/{question-slug}
I've been working for some time with MVC and I have it working (routes, action, everything) so that it picks up the right content based on the provided ID.
However, the part after the {id} (the slug part) stays the same as typed in. So if someone typed in content/5/foobar it will display the right content but will leave the foobar in there.
In the controller (or somewhere else, please suggest where) I would need to go into the DB and pull out the right slug, put it in the route data and then perform a redirect to the same action with this correct data, I guess?
This is a try with Execute Result override. It works but does not redirect or replace/display the correct URL in browser:
protected override void Execute(System.Web.Routing.RequestContext requestContext) {
if (requestContext.RouteData.Values["id"] != null) {
string currentSlug = _repository.Find(int.Parse(requestContext.RouteData.Values["id"].ToString())).Slug;
if (requestContext.RouteData.Values["slug"] != null) {
requestContext.RouteData.Values.Remove("slug");
}
requestContext.RouteData.Values.Add("slug", currentSlug);
}
base.Execute(requestContext);
}
This is another, nicely working, version of a Display action, so you can see what it does and get an idea what I want:
//
// GET: {culture}/directory/5/{slug}
public virtual ActionResult Display(int id, string slug)
{
var model = _repository.Find(id);
if (model != null) {
if (!model.Slug.Equals(slug, System.StringComparison.OrdinalIgnoreCase)) {
return RedirectToActionPermanent(pndng.DirectoryEntry.ActionNames.Display, pndng.DirectoryEntry.Name, new { id = model.Id, slug = model.Slug });
}
return View(model);
}
// no model found
return InvokeHttp404(HttpContext);
}
This one performs permanent redirect (it does what I want) but is it right?
I guess I need a redirect to refresh the browser URL, don't I?
public ActionResult Details(int id, string slug)
{
var session = MvcApplication.CurrentRavenSession;
var blogPostRelations = session
.Query<BlogPost, BlogPosts_WithRelatedData>()
.Where(x => x.IntId == id)
.As<BlogPostRelations>()
.FirstOrDefault()
;
if (blogPostRelations == null)
return HttpNotFound();
if (blogPostRelations.BlogPost.DisplayData.Slug.Value != slug)
return RedirectToActionPermanent("Details", new { id = id, slug = blogPostRelations.BlogPost.DisplayData.Slug.Value });
return View(blogPostRelations);
}
Notice the:
if (blogPostRelations.BlogPost.DisplayData.Slug.Value != slug)
return RedirectToActionPermanent("Details", new { id = id, slug = blogPostRelations.BlogPost.DisplayData.Slug.Value });
So your #2 approach is the right one.
You could write a custom route for this:
public class QuestionsRoute : Route
{
public QuestionsRoute()
: base(
"questions/{id}/{slug}",
new RouteValueDictionary(new
{
controller = "questions",
action = "index",
slug = UrlParameter.Optional
}),
new RouteValueDictionary(new
{
id = #"\d+"
}),
new MvcRouteHandler()
)
{ }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var id = rd.GetRequiredString("id");
var slug = rd.Values["slug"] as string;
if (string.IsNullOrEmpty(slug))
{
slug = GoFetchSlugFromDb(id);
if (string.IsNullOrEmpty(slug))
{
return null;
}
httpContext.Response.RedirectToRoutePermanent(new
{
action = "index",
controller = "questions",
id = id,
slug = slug
});
return null;
}
return rd;
}
private string GoFetchSlugFromDb(string id)
{
// TODO: you know what to do here
throw new NotImplementedException();
}
}
which will be registered in Application_Start:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("questions", new QuestionsRoute());
}
Now your QuestionsController will be pretty simple:
public class QuestionsController: Controller
{
public ActionResult Index(int id, string slug)
{
...
}
}

ASP.NET MVC to ignore ".html" at the end of all url

I am new to asp.net mvc and now struggling with url routing. I'm using asp.net mvc 3 RC2.
How can I create a url routing that IGNORES the very end extension in url. the extension can be: .html, .aspx, .php, .anything.
For example, these urls:
/Home.html
/Home.en
/Home.fr
/Home
should go to Home controller?
one more example:
/Home/About.html
/Home/About.en
/Home/About.fr
/Home/About
should go to Home controller and About action.
thank you :)
I'm not sure if you're using IIS7, but if so, then I would recommend a rewrite rule which checks for urls ending in .xyz and then doing a rewrites for them without the .xyz.
Something like this:
<rewrite>
<rules>
<rule name="HtmlRewrite">
<match url="(.*)(\.\w+)$" />
<action type="Rewrite" url="{R:1}" />
</rule>
</rules>
</rewrite>
This will handle the use cases you suggested. Anything that ends with an extension and some characters will be rewritten to a url without the extension. The benefit of this is that you will only need one route because everything goes into your application without one.
You just need to tweak the default route in Global.asax.cs, try this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}.{extension}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
The {extension} value from the url will be included in the route data, but you can just safely ignore it if you don't need it
Either create your own route class, or use this regex route implementation: http://blog.sb2.fr/post/2009/01/03/Regular-Expression-MapRoute-With-ASPNET-MVC.aspx
You could handle this in IIS instead of ASP.NET MVC using IIS Url rewriting. See for example: http://learn.iis.net/page.aspx/496/iis-url-rewriting-and-aspnet-routing/
I started to work on this question as a weekend assignment :D
below code will work as requested in question. please refer below references
1] MyUrlRoute Class : RouteBase
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace MvcIgnoreUrl
{
#region //References
// SO question /http://stackoverflow.com/questions/4449449/asp-net-mvc-to-ignore-html-at-the-end-of-all-url
// Implementing Custom Base entry - Pro Asp.Net MVc Framework
//- http://books.google.com/books?id=tD3FfFcnJxYC&pg=PA251&lpg=PA251&dq=.net+RouteBase&source=bl&ots=IQhFwmGOVw&sig=0TgcFFgWyFRVpXgfGY1dIUc0VX4&hl=en&ei=z61UTMKwF4aWsgPHs7XbAg&sa=X&oi=book_result&ct=result&resnum=6&ved=0CC4Q6AEwBQ#v=onepage&q=.net%20RouteBase&f=false
// SO previous Question on ihttphandler - http://stackoverflow.com/questions/3359816/can-asp-net-routing-be-used-to-create-clean-urls-for-ashx-ihttphander-handle
// phil haack's Route Debugger http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx
#endregion
public class MyUrlRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
//~/Account/LogOn
//~/Home.aspx - Works fine
//~/home/index.aspx -Works Fine
//http://localhost:57282/home/index/1/2/3 - Works fine
//http://localhost:57282/Account/Register http://localhost:57282/Account/LogOn - Works Fine
string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
//check null for URL
const string defaultcontrollername = "Home";
string[] spliturl = url.Split("//".ToCharArray());
string controllername = String.Empty;
string actionname = "Index";
if (spliturl.Length == 2) //for ~/home.aspx and ~/
{
if (String.IsNullOrEmpty(spliturl[1])) //TODO: http://localhost:57282/ not working - to make it working
{
controllername = defaultcontrollername;
}
else
{
controllername = spliturl[1];
if (controllername.Contains("."))
{
controllername = controllername.Substring(0, controllername.LastIndexOf("."));
}
}
}
else if (spliturl.Length == 3) // For #/home/index.aspx and /home/about
{
controllername = spliturl[1];
actionname = spliturl[2];
if (actionname.Contains("."))
{
actionname = actionname.Substring(0, actionname.LastIndexOf("."));
}
}
else //final block in final case sned it to Home Controller
{
controllername = defaultcontrollername;
}
RouteData rd = new RouteData(this, new MvcRouteHandler());
rd.Values.Add("controller", controllername);
rd.Values.Add("action", actionname);
rd.Values.Add("url", url);
return rd;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
}
in global.asax.cs add below code
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new MyUrlRoute()); // Add before your default Routes
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
working as expected.
possibly you can improve if/elseif code.
Using the Application_BeginRequest, will allow you to intercept all incoming requests, and allow you to trim the extension. Make sure to ignore requests for your content, such as .css, .js, .jpg, etc. Otherwise those requests will have their extensions trimmed as well.
protected void Application_BeginRequest(object sender, EventArgs e)
{
String originalPath = HttpContext.Current.Request.Url.AbsolutePath;
//Ignore content files (e.g. .css, .js, .jpg, .png, etc.)
if (!Regex.Match(originalPath, "^/[cC]ontent").Success)
{
//Search for a file extension (1 - 5 charaters long)
Match match = Regex.Match(originalPath, "\\.[a-zA-Z0-9]{1,5}$");
if (match.Success)
{
String modifiedPath = String.Format("~{0}", originalPath.Replace(match.Value, String.Empty));
HttpContext.Current.RewritePath(modifiedPath);
}
}
}
If you are using IIS 7, you should look at Dan Atkinson's answer.
I'm using IIS 6, so, in my case, I have the option to install isapi rewrite for IIS 6 or create custom route. I prefer to create my simple custom route class
AndraRoute.cs
// extend Route class,
// so that we can manipulate original RouteData
// by overriding method GetRouteDate
public class AndraRoute : Route
{
// constructor
public AndraRoute(
string url,
RouteValueDictionary defaults,
RouteValueDictionary constraints,
IRouteHandler routeHandler)
: base(url, defaults, constraints, routeHandler)
{
}
// get original RouteData
// check if any route data value has extension '.html' or '.anything'
// remove the extension
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var data = base.GetRouteData(httpContext);
if (data == null) return null;
// from original route data, check
foreach (var pair in data.Values)
{
if (pair.Value.ToString().Contains('.'))
{
var splits = pair.Value.ToString().Split('.');
if (splits[1] == "html" || splits[1] == "anything")
{
data.Values[pair.Key] = splits[0];
}
break;
}
}
return data;
}
}
RouteCollectionExtensionHelper.cs
public static class RouteCollectionExtensionHelper
{
public static Route MapAndraRoute(this RouteCollection routes,
string name, string url, object defaults, object constraints,
string[] namespaces)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
var route = new AndraRoute(url,
new RouteValueDictionary(defaults),
new RouteValueDictionary(constraints),
new MvcRouteHandler());
if ((namespaces != null) && (namespaces.Length > 0))
{
route.DataTokens = new RouteValueDictionary();
route.DataTokens["Namespaces"] = namespaces;
}
routes.Add(name, route);
return route;
}
}
RegisterRoutes method in Global.asax
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("Content/{*pathInfo}");
routes.MapAndraRoute(
"Product",
"product/{id}/{slug}",
new { controller = "product", action = "detail" },
null, null
);
routes.MapAndraRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "home", action = "index", id = UrlParameter.Optional },
null, null
);
}

MVC RouteDatas are confused

I am using asp.net mvc for my website project. I think i have wrong things in my routedata but i am not sure it is wrong or ok. i will explain the situation.
I am caching my action results (html outputs) in Cache with a generated key
public static string GetKeyFromActionExecutingContext(ControllerContext filterContext)
{
StringBuilder keyBuilder = new StringBuilder();
if (filterContext.IsChildAction)
keyBuilder.Append("C-");
else
keyBuilder.Append("P-");
foreach (var item in filterContext.RouteData.Values)
{
keyBuilder.AppendFormat("{0}={1}.", item.Key, item.Value);
}
return keyBuilder.ToString();
}
ex: For HomePage , generated cache key is P-Controller=Home.Action=Index and
I have also childactions in my sitemaster like LoginBox(It is in MembershipController/LoginBox)
Its cache key is C-Controller=Membership.Action=LoginBox.
Everything is okey till now.
I have also subcategories in my website like
domain/category1
domain/category1/subcategory1
domain/category1/subcategory2
domain/category2
When i am browsing a sub category from domain/category1
My generated keys are failed because my routedatas are wrong
filterContext.RouteData.Values:
Controller = Membership
Action = LoginBox
ctg1 = category1
ctg2 = ""
ctg3 = ""
Why these are mixed. It is using the "Category" routemapping but I think it must use "Default" routemapping.
My global.asax like below
routes.MapRoute(
"Category",
"{ctg0}/{ctg1}/{ctg2}/{ctg3}",
new
{
controller = "Category",
action = "Index",
ctg0 = "",
ctg1 = "",
ctg2 = "",
ctg3 = ""
},
new
{
ctg0 = new CategoryRouteConstraint(),
}
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" },
new { controller = #"[^\.]*" }
);
Also my CategoryRouteConstraint Method it is checking from db that ctg0 value is a category name
public class CategoryRouteConstraint : IRouteConstraint
{
public Boolean Match(
HttpContextBase httpContext,
Route route,
String sParameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
if ((routeDirection == RouteDirection.IncomingRequest))
{
if (values["ctg0"] != null && !string.IsNullOrEmpty(values["ctg0"].ToString()))
return Category.IsRoutingForCategory(values["ctg0"].ToString());
return false;
}
return false;
}
}
Hopefully this may help you, it will show you which routes a url matches, I was a little confused by the question.
http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx

How can dynamic breadcrumbs be achieved with ASP.net MVC?

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.

Resources