ASP.NET 路由中的 MVC 5 文化和 url
ASP.NET MVC 5 culture in route and url
我翻译了我的 mvc 网站,效果很好。如果我 select 另一种语言(荷兰语或英语),内容会被翻译。
这是有效的,因为我在会话中设置了文化。
现在我想在 url 中展示 selected 文化(=culture)。
如果它是默认语言,则不应在 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 = "[a-z]{2}" }
);// or maybe: "[a-z]{2}-[a-z]{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");
}
}
}
这种方法有几个问题,但归结为工作流程问题。
- 您有一个
CultureController
,其唯一目的是将用户重定向到网站上的另一个页面。请记住 RedirectToAction
将向用户的浏览器发送 HTTP 302 响应,这将告诉它在您的服务器上查找新位置。这是不必要的网络往返。
- 当 URL 中已经可用时,您正在使用会话状态来存储用户的文化。在这种情况下完全不需要会话状态。
- 您正在阅读用户的
HttpContext.Current.Request.UserLanguages
,这可能与他们在 URL 中请求的文化不同。
第三个问题主要是因为 Microsoft 和 Google 在如何处理全球化方面存在根本不同的观点。
Microsoft 的(原始)观点是相同的 URL 应该用于每种文化,并且浏览器的 UserLanguages
应该决定网站应该显示什么语言。
Google的观点是every culture should be hosted on a different URL。如果您考虑一下,这会更有意义。每个在搜索结果 (SERP) 中找到您网站的人都希望能够以他们的母语搜索内容。
网站的全球化应被视为 内容 而不是个性化 - 您正在向 群体 的人传播一种文化,不是一个人。因此,使用 ASP.NET 的任何个性化功能(例如会话状态或 cookie)来实现全球化通常没有意义 - 这些功能会阻止搜索引擎对本地化页面的内容编制索引。
如果您只需将用户路由到一个新的 URL 就可以将用户带到不同的文化,那么就不用担心了 - 您不需要单独的页面让用户 select 他们的文化,只需在页眉或页脚中包含 link 即可更改现有页面的文化,然后所有 link 将自动切换到用户选择的文化(因为MVC automatically reuses route values from the current request).
解决问题
首先,去掉CultureController
和Application_AcquireRequestState
方法中的代码。
文化过滤器
现在,由于文化是一个横切关注点,设置当前线程的文化应该在 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);
}
}
您可以通过将其注册为全局过滤器来全局设置过滤器。
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CultureFilter(defaultCulture: "nl"));
filters.Add(new HandleErrorAttribute());
}
}
语言选择
您可以通过 link 将语言 select 简化为当前页面的相同操作和控制器,并将其作为选项包含在 [=26] 的页眉或页脚中=].
@{
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>
如前所述,页面上的所有其他 link 将自动从当前上下文传递一种文化,因此它们将自动保持在同一文化中。在这些情况下没有理由明确传递文化。
@ActionLink("About", "About", "Home")
加上上面的link,如果当前的URL是/Home/Contact
,生成的link就是/Home/About
。如果当前URL为/en/Home/Contact
,则link将生成为/en/Home/About
。
默认文化
终于到了您问题的核心。您的默认区域性未正确生成的原因是因为路由是双向映射,无论您是匹配传入请求还是生成传出请求 URL,第一个匹配项总是获胜。在构建 URL 时,第一个匹配项是 DefaultWithCulture
.
通常情况下,您只需颠倒路由的顺序即可解决此问题。但是,在您的情况下,这会导致传入路由失败。
因此,在您的情况下,最简单的选择是构建一个 custom route constraint 来处理生成 URL 时默认区域性的特殊情况。当提供默认区域性时,您只需 return 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 + "$");
}
}
}
剩下的就是将约束添加到路由配置中。您还应该删除 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: "[a-z]{2}") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
属性路由
NOTE: This section applies only if you are using MVC 5. You can skip this if you are using a previous version.
对于 AttributeRouting,您可以通过为每个操作自动创建 2 条不同的路由来简化操作。您需要稍微调整每条路线并将它们添加到 MapMvcAttributeRoutes
使用的相同 class 结构中。不幸的是,Microsoft 决定将类型设为内部类型,因此它需要反射来实例化和填充它们。
RouteCollectionExtensions
这里我们只是使用 MVC 的内置功能来扫描我们的项目并创建一组路由,然后在添加实例到我们的 MVC RouteTable。
还创建了一个单独的路由来解析 URLs(与 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: "[a-z]{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: "[a-z]{2}") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
默认文化修复
NightOwl888 的精彩 post。虽然缺少一些东西——正常的(非本地化的)URL-generation 属性路由,通过反射添加,也需要一个默认的文化参数,否则你会在 URL 中得到一个查询参数。
?culture=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: "[a-z]{2}") }
);
更好的解决方案
实际上,一段时间后,我需要添加 url 翻译,所以我深入挖掘,似乎没有必要进行所描述的反射黑客攻击。 ASP.NET 伙计们想到了,有更简洁的解决方案 - 相反,您可以像这样扩展 DefaultDirectRouteProvider:
public static class RouteCollectionExtensions
{
public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string defaultCulture)
{
var routeProvider = new LocalizeDirectRouteProvider(
"{culture}/",
defaultCulture
);
routes.MapMvcAttributeRoutes(routeProvider);
}
}
class LocalizeDirectRouteProvider : DefaultDirectRouteProvider
{
ILogger _log = LogManager.GetCurrentClassLogger();
string _urlPrefix;
string _defaultCulture;
RouteValueDictionary _constraints;
public LocalizeDirectRouteProvider(string urlPrefix, string defaultCulture)
{
_urlPrefix = urlPrefix;
_defaultCulture = defaultCulture;
_constraints = new RouteValueDictionary() { { "culture", new CultureConstraint(defaultCulture: defaultCulture) } };
}
protected override IReadOnlyList<RouteEntry> GetActionDirectRoutes(
ActionDescriptor actionDescriptor,
IReadOnlyList<IDirectRouteFactory> factories,
IInlineConstraintResolver constraintResolver)
{
var originalEntries = base.GetActionDirectRoutes(actionDescriptor, factories, constraintResolver);
var finalEntries = new List<RouteEntry>();
foreach (RouteEntry originalEntry in originalEntries)
{
var localizedRoute = CreateLocalizedRoute(originalEntry.Route, _urlPrefix, _constraints);
var localizedRouteEntry = CreateLocalizedRouteEntry(originalEntry.Name, localizedRoute);
finalEntries.Add(localizedRouteEntry);
originalEntry.Route.Defaults.Add("culture", _defaultCulture);
finalEntries.Add(originalEntry);
}
return finalEntries;
}
private 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 RouteEntry CreateLocalizedRouteEntry(string name, Route route)
{
var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized";
return new RouteEntry(localizedRouteEntryName, route);
}
}
有基于此的解决方案,包括url这里翻译:https://github.com/boudinov/mvc-5-routing-localization
我翻译了我的 mvc 网站,效果很好。如果我 select 另一种语言(荷兰语或英语),内容会被翻译。 这是有效的,因为我在会话中设置了文化。
现在我想在 url 中展示 selected 文化(=culture)。 如果它是默认语言,则不应在 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 = "[a-z]{2}" }
);// or maybe: "[a-z]{2}-[a-z]{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");
}
}
}
这种方法有几个问题,但归结为工作流程问题。
- 您有一个
CultureController
,其唯一目的是将用户重定向到网站上的另一个页面。请记住RedirectToAction
将向用户的浏览器发送 HTTP 302 响应,这将告诉它在您的服务器上查找新位置。这是不必要的网络往返。 - 当 URL 中已经可用时,您正在使用会话状态来存储用户的文化。在这种情况下完全不需要会话状态。
- 您正在阅读用户的
HttpContext.Current.Request.UserLanguages
,这可能与他们在 URL 中请求的文化不同。
第三个问题主要是因为 Microsoft 和 Google 在如何处理全球化方面存在根本不同的观点。
Microsoft 的(原始)观点是相同的 URL 应该用于每种文化,并且浏览器的 UserLanguages
应该决定网站应该显示什么语言。
Google的观点是every culture should be hosted on a different URL。如果您考虑一下,这会更有意义。每个在搜索结果 (SERP) 中找到您网站的人都希望能够以他们的母语搜索内容。
网站的全球化应被视为 内容 而不是个性化 - 您正在向 群体 的人传播一种文化,不是一个人。因此,使用 ASP.NET 的任何个性化功能(例如会话状态或 cookie)来实现全球化通常没有意义 - 这些功能会阻止搜索引擎对本地化页面的内容编制索引。
如果您只需将用户路由到一个新的 URL 就可以将用户带到不同的文化,那么就不用担心了 - 您不需要单独的页面让用户 select 他们的文化,只需在页眉或页脚中包含 link 即可更改现有页面的文化,然后所有 link 将自动切换到用户选择的文化(因为MVC automatically reuses route values from the current request).
解决问题
首先,去掉CultureController
和Application_AcquireRequestState
方法中的代码。
文化过滤器
现在,由于文化是一个横切关注点,设置当前线程的文化应该在 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);
}
}
您可以通过将其注册为全局过滤器来全局设置过滤器。
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CultureFilter(defaultCulture: "nl"));
filters.Add(new HandleErrorAttribute());
}
}
语言选择
您可以通过 link 将语言 select 简化为当前页面的相同操作和控制器,并将其作为选项包含在 [=26] 的页眉或页脚中=].
@{
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>
如前所述,页面上的所有其他 link 将自动从当前上下文传递一种文化,因此它们将自动保持在同一文化中。在这些情况下没有理由明确传递文化。
@ActionLink("About", "About", "Home")
加上上面的link,如果当前的URL是/Home/Contact
,生成的link就是/Home/About
。如果当前URL为/en/Home/Contact
,则link将生成为/en/Home/About
。
默认文化
终于到了您问题的核心。您的默认区域性未正确生成的原因是因为路由是双向映射,无论您是匹配传入请求还是生成传出请求 URL,第一个匹配项总是获胜。在构建 URL 时,第一个匹配项是 DefaultWithCulture
.
通常情况下,您只需颠倒路由的顺序即可解决此问题。但是,在您的情况下,这会导致传入路由失败。
因此,在您的情况下,最简单的选择是构建一个 custom route constraint 来处理生成 URL 时默认区域性的特殊情况。当提供默认区域性时,您只需 return 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 + "$");
}
}
}
剩下的就是将约束添加到路由配置中。您还应该删除 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: "[a-z]{2}") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
属性路由
NOTE: This section applies only if you are using MVC 5. You can skip this if you are using a previous version.
对于 AttributeRouting,您可以通过为每个操作自动创建 2 条不同的路由来简化操作。您需要稍微调整每条路线并将它们添加到 MapMvcAttributeRoutes
使用的相同 class 结构中。不幸的是,Microsoft 决定将类型设为内部类型,因此它需要反射来实例化和填充它们。
RouteCollectionExtensions
这里我们只是使用 MVC 的内置功能来扫描我们的项目并创建一组路由,然后在添加实例到我们的 MVC RouteTable。
还创建了一个单独的路由来解析 URLs(与 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: "[a-z]{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: "[a-z]{2}") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
默认文化修复
NightOwl888 的精彩 post。虽然缺少一些东西——正常的(非本地化的)URL-generation 属性路由,通过反射添加,也需要一个默认的文化参数,否则你会在 URL 中得到一个查询参数。
?culture=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: "[a-z]{2}") }
);
更好的解决方案
实际上,一段时间后,我需要添加 url 翻译,所以我深入挖掘,似乎没有必要进行所描述的反射黑客攻击。 ASP.NET 伙计们想到了,有更简洁的解决方案 - 相反,您可以像这样扩展 DefaultDirectRouteProvider:
public static class RouteCollectionExtensions
{
public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string defaultCulture)
{
var routeProvider = new LocalizeDirectRouteProvider(
"{culture}/",
defaultCulture
);
routes.MapMvcAttributeRoutes(routeProvider);
}
}
class LocalizeDirectRouteProvider : DefaultDirectRouteProvider
{
ILogger _log = LogManager.GetCurrentClassLogger();
string _urlPrefix;
string _defaultCulture;
RouteValueDictionary _constraints;
public LocalizeDirectRouteProvider(string urlPrefix, string defaultCulture)
{
_urlPrefix = urlPrefix;
_defaultCulture = defaultCulture;
_constraints = new RouteValueDictionary() { { "culture", new CultureConstraint(defaultCulture: defaultCulture) } };
}
protected override IReadOnlyList<RouteEntry> GetActionDirectRoutes(
ActionDescriptor actionDescriptor,
IReadOnlyList<IDirectRouteFactory> factories,
IInlineConstraintResolver constraintResolver)
{
var originalEntries = base.GetActionDirectRoutes(actionDescriptor, factories, constraintResolver);
var finalEntries = new List<RouteEntry>();
foreach (RouteEntry originalEntry in originalEntries)
{
var localizedRoute = CreateLocalizedRoute(originalEntry.Route, _urlPrefix, _constraints);
var localizedRouteEntry = CreateLocalizedRouteEntry(originalEntry.Name, localizedRoute);
finalEntries.Add(localizedRouteEntry);
originalEntry.Route.Defaults.Add("culture", _defaultCulture);
finalEntries.Add(originalEntry);
}
return finalEntries;
}
private 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 RouteEntry CreateLocalizedRouteEntry(string name, Route route)
{
var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized";
return new RouteEntry(localizedRouteEntryName, route);
}
}
有基于此的解决方案,包括url这里翻译:https://github.com/boudinov/mvc-5-routing-localization