ASP.NET MVC 5文化的路线和url

我已经翻译了我的mvc网站,这很好。 如果我select另一种语言(荷兰语或英语),内容将被翻译。 这是有效的,因为我在会话中设置了文化。

现在我想在URL中显示选定的文化(=文化)。 如果它是默认的语言,它不应该显示在url中,只有当它不是默认的语言,它应该显示在url中。

例如:

默认文化(荷兰语):

site.com/foo site.com/foo/bar site.com/foo/bar/5 

对于非默认文化(英文):

 site.com/en/foo site.com/en/foo/bar site.com/en/foo/bar/5 

我的问题是,我总是看到这个:

site.com/nl/foo/bar/5即使我点击英文(参见_Layout.cs)。 我的内容被翻译成英文,但url中的路由参数保持在“nl”而不是“en”。

我如何解决这个问题或者我做错了什么?

我试图在global.asax设置RouteData,但没有帮助。

  public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("favicon.ico"); routes.LowercaseUrls = true; routes.MapRoute( name: "Errors", url: "Error/{action}/{code}", defaults: new { controller = "Error", action = "Other", code = RouteParameter.Optional } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = "[az]{2}" } );// or maybe: "[az]{2}-[az]{2} routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); } 

的Global.asax.cs:

  protected void Application_Start() { MvcHandler.DisableMvcResponseHeader = true; AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } protected void Application_AcquireRequestState(object sender, EventArgs e) { if (HttpContext.Current.Session != null) { CultureInfo ci = (CultureInfo)this.Session["Culture"]; if (ci == null) { string langName = "nl"; if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0) { langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2); } ci = new CultureInfo(langName); this.Session["Culture"] = ci; } HttpContextBase currentContext = new HttpContextWrapper(HttpContext.Current); RouteData routeData = RouteTable.Routes.GetRouteData(currentContext); routeData.Values["culture"] = ci; Thread.CurrentThread.CurrentUICulture = ci; Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name); } } 

_Layout.cs(我让用户改变语言)

 // ... <ul class="dropdown-menu" role="menu"> <li class="@isCurrentLang("nl")">@Html.ActionLink("Nederlands", "ChangeCulture", "Culture", new { lang = "nl", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "nl" })</li> <li class="@isCurrentLang("en")">@Html.ActionLink("English", "ChangeCulture", "Culture", new { lang = "en", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "en" })</li> </ul> // ... 

CultureController:(=我在GlobalAsax中设置了用来改变CurrentCulture和CurrentUICulture的会话)

 public class CultureController : Controller { // GET: Culture public ActionResult Index() { return RedirectToAction("Index", "Home"); } public ActionResult ChangeCulture(string lang, string returnUrl) { Session["Culture"] = new CultureInfo(lang); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } } 

这种方法有几个问题,但归结为是一个工作stream程问题。

  1. 您有一个CultureController其唯一目的是将用户redirect到网站上的另一个页面。 请记住, RedirectToAction会向用户的浏览器发送HTTP 302响应,这会告诉它在您的服务器上查找新的位置。 这是整个networking中不必要的往返。
  2. 您正在使用会话状态来存储用户在url中已有的文化。 在这种情况下会话状态是完全不必要的。
  3. 您正在阅读用户的HttpContext.Current.Request.UserLanguages ,这可能与他们在URL中请求的区别不同。

第三个问题主要是因为微软和谷歌之间关于如何处理全球化的看法根本不同。

微软(原创)的观点是,每个文化都应该使用相同的URL,浏览器的UserLanguages应该确定网站应该显示的语言。

谷歌的观点是, 每一种文化都应该被托pipe在不同的url上 。 如果你仔细想想,这会更有意义。 每个在search结果(SERP)中发现您的网站的人都可以使用其母语search内容。

一个网站的全球化应该被视为内容而不是个性化 – 你是在向一群人而不是一个人传播一种文化。 因此,使用ASP.NET的任何个性化function(如会话状态或Cookie)来实现全球化通常是没有意义的 – 这些function可以防止search引擎索引本地化页面的内容。

如果您可以简单地通过将用户路由到一个新的URL来将用户发送到不同的文化中,则不用担心 – 您不需要单独的页面供用户select他们的文化,只需在标题中包含一个链接或页脚来改变现有页面的文化,然后所有的链接将自动切换到用户select的文化(因为MVC 自动重用当前请求的路由值 )。

解决问题

首先,摆脱CultureControllerApplication_AcquireRequestState方法中的代码。

CultureFilter

现在,由于文化是一个交叉的问题,设置当前线程的文化应该在IAuthorizationFilter完成。 这确保在MVC中使用ModelBinder之前设置文化。

 using System.Globalization; using System.Threading; using System.Web.Mvc; 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); } } 

您可以将全局filter注册为全局filter来设置filter。

 public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new CultureFilter(defaultCulture: "nl")); filters.Add(new HandleErrorAttribute()); } } 

语言select

您可以通过redirect到当前页面的相同操作和控制器来简化语言select,并将其作为页面页眉或页脚中的选项包含在_Layout.cshtml

 @{ var routeValues = this.ViewContext.RouteData.Values; var controller = routeValues["controller"] as string; var action = routeValues["action"] as string; } <ul> <li>@Html.ActionLink("Nederlands", @action, @controller, new { culture = "nl" }, new { rel = "alternate", hreflang = "nl" })</li> <li>@Html.ActionLink("English", @action, @controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" })</li> </ul> 

如前所述,页面上的所有其他链接将自动从当前上下文中传递一种文化,因此它们将自动保持在相同的文化中。 在这种情况下没有理由明确传递文化。

 @ActionLink("About", "About", "Home") 

通过上面的链接,如果当前URL是/Home/Contact ,则生成的链接将是/Home/About 。 如果当前URL是/en/Home/Contact ,则链接将生成为/en/Home/About

默认文化

最后,我们谈谈你的问题的核心。 您的默认文化不能正确生成的原因是因为路由是一种双向映射,无论您是匹配传入请求还是生成传出url,第一个匹配总是胜出。 在构build您的url时,第一个匹配项是DefaultWithCulture

通常情况下,您可以简单地通过反转路由的顺序来解决这个问题。 但是,在你的情况下,会导致传入路由失败。

所以,在你的情况下最简单的select是build立一个自定义的路由约束来处理生成URL时的默认文化的特殊情况。 当提供默认文化时,您只需返回false,它将导致.NET路由框架跳过DefaultWithCulture路由并移到下一个注册的路由(本例中为Default )。

 using System.Text.RegularExpressions; using System.Web; using System.Web.Routing; 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 + "$"); } } } 

剩下的就是将约束添加到路由configuration中。 您还应该删除DefaultWithCulture路由中的默认文化设置,因为您只希望它在URL中提供文化时匹配。 另一方面, Default路由应该有一种文化,因为没有办法通过URL传递。

 routes.LowercaseUrls = true; routes.MapRoute( name: "Errors", url: "Error/{action}/{code}", defaults: new { controller = "Error", action = "Other", code = UrlParameter.Optional } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[az]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); 

AttributeRouting

注:本节仅适用于使用MVC 5.如果您使用的是以前的版本,则可以跳过此步骤。

对于AttributeRouting,可以通过为每个操作自动创build2个不同的路由来简化操作。 您需要稍微调整每条路线,并将其添加到MapMvcAttributeRoutes使用的相同类结构中。 不幸的是,微软决定把这些types放在内部,所以它需要reflection来实例化和填充它们。

RouteCollectionExtensions

在这里,我们只使用MVC的内置function来扫描我们的项目并创build一组path,然后在将实例添加到我们的MVC RouteTable之前插入文化和CultureConstraint的额外路由URL前缀。

还有一个单独的路由被创build用于parsingURL(与AttributeRouting所做的相同)。

 using System; using System.Collections; using System.Linq; using System.Reflection; using System.Web.Mvc; using System.Web.Mvc.Routing; using System.Web.Routing; public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object constraints) { MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(constraints)); } public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary constraints) { var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc"); var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc"); FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance); var subRoutes = Activator.CreateInstance(subRouteCollectionType); var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes); // Add the route entries collection first to the route collection routes.Add((RouteBase)routeEntries); var localizedRouteTable = new RouteCollection(); // Get a copy of the attribute routes localizedRouteTable.MapMvcAttributeRoutes(); foreach (var routeBase in localizedRouteTable) { if (routeBase.GetType().Equals(routeCollectionRouteType)) { // Get the value of the _subRoutes field var tempSubRoutes = subRoutesInfo.GetValue(routeBase); // Get the PropertyInfo for the Entries property PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries"); if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable))) { foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes)) { var route = routeEntry.Route; // Create the localized route var localizedRoute = CreateLocalizedRoute(route, urlPrefix, constraints); // Add the localized route entry var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute); AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry); // Add the default route entry AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry); // Add the localized link generation route var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute); routes.Add(localizedLinkGenerationRoute); // Add the default link generation route var linkGenerationRoute = CreateLinkGenerationRoute(route); routes.Add(linkGenerationRoute); } } } } } private static Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints) { // Add the URL prefix var routeUrl = urlPrefix + route.Url; // Combine the constraints var routeConstraints = new RouteValueDictionary(constraints); foreach (var constraint in route.Constraints) { routeConstraints.Add(constraint.Key, constraint.Value); } return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler); } private static RouteEntry CreateLocalizedRouteEntry(string name, Route route) { var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized"; return new RouteEntry(localizedRouteEntryName, route); } private static void AddRouteEntry(Type subRouteCollectionType, object subRoutes, RouteEntry newEntry) { var addMethodInfo = subRouteCollectionType.GetMethod("Add"); addMethodInfo.Invoke(subRoutes, new[] { newEntry }); } private static RouteBase CreateLinkGenerationRoute(Route innerRoute) { var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc"); return (RouteBase)Activator.CreateInstance(linkGenerationRouteType, innerRoute); } } 

那么这只是调用这个方法而不是MapMvcAttributeRoutes

 public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // Call to register your localized and default attribute routes routes.MapLocalizedMvcAttributeRoutes( urlPrefix: "{culture}/", constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[az]{2}") } ); routes.MapRoute( name: "DefaultWithCulture", url: "{culture}/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[az]{2}") } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } 

NightOwl888令人难以置信的职位。 虽然缺less一些东西 – 通过reflection添加的正常(不是本地化的)URL生成属性路由也需要一个默认的文化参数,否则你会在URL中得到一个查询参数。

?文化= NL

为了避免这种情况,必须做出这些改变:

 using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Web; using System.Web.Mvc; using System.Web.Mvc.Routing; using System.Web.Routing; namespace Endpoints.WebPublic.Infrastructure.Routing { public static class RouteCollectionExtensions { public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object defaults, object constraints) { MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); } public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary defaults, RouteValueDictionary constraints) { var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc"); var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc"); FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance); var subRoutes = Activator.CreateInstance(subRouteCollectionType); var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes); // Add the route entries collection first to the route collection routes.Add((RouteBase)routeEntries); var localizedRouteTable = new RouteCollection(); // Get a copy of the attribute routes localizedRouteTable.MapMvcAttributeRoutes(); foreach (var routeBase in localizedRouteTable) { if (routeBase.GetType().Equals(routeCollectionRouteType)) { // Get the value of the _subRoutes field var tempSubRoutes = subRoutesInfo.GetValue(routeBase); // Get the PropertyInfo for the Entries property PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries"); if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable))) { foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes)) { var route = routeEntry.Route; // Create the localized route var localizedRoute = CreateLocalizedRoute(route, urlPrefix, constraints); // Add the localized route entry var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute); AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry); // Add the default route entry AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry); // Add the localized link generation route var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute); routes.Add(localizedLinkGenerationRoute); // Add the default link generation route //FIX: needed for default culture on normal attribute route var newDefaults = new RouteValueDictionary(defaults); route.Defaults.ToList().ForEach(x => newDefaults.Add(x.Key, x.Value)); var routeWithNewDefaults = new Route(route.Url, newDefaults, route.Constraints, route.DataTokens, route.RouteHandler); var linkGenerationRoute = CreateLinkGenerationRoute(routeWithNewDefaults); routes.Add(linkGenerationRoute); } } } } } private static Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints) { // Add the URL prefix var routeUrl = urlPrefix + route.Url; // Combine the constraints var routeConstraints = new RouteValueDictionary(constraints); foreach (var constraint in route.Constraints) { routeConstraints.Add(constraint.Key, constraint.Value); } return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler); } private static RouteEntry CreateLocalizedRouteEntry(string name, Route route) { var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized"; return new RouteEntry(localizedRouteEntryName, route); } private static void AddRouteEntry(Type subRouteCollectionType, object subRoutes, RouteEntry newEntry) { var addMethodInfo = subRouteCollectionType.GetMethod("Add"); addMethodInfo.Invoke(subRoutes, new[] { newEntry }); } private static RouteBase CreateLinkGenerationRoute(Route innerRoute) { var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc"); return (RouteBase)Activator.CreateInstance(linkGenerationRouteType, innerRoute); } } } 

并给属性路由注册:

  RouteTable.Routes.MapLocalizedMvcAttributeRoutes( urlPrefix: "{culture}/", defaults: new { culture = "nl" }, constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[az]{2}") } );