问题描述
我已经翻译了我的 mvc 网站,效果很好.如果我选择另一种语言(荷兰语或英语),内容就会被翻译.这是有效的,因为我在会话中设置了文化.
现在我想在 url 中显示选定的文化(=文化).如果是默认语言,则不应在 url 中显示,只有在不是默认语言时,才应在 url 中显示.
例如:
对于默认文化(荷兰语):
site.com/foosite.com/foo/barsite.com/foo/bar/5
对于非默认文化(英语):
site.com/en/foosite.com/en/foo/barsite.com/en/foo/bar/5
我的问题是我总是看到这个:
site.com/nl/foo/bar/5即使我点击了英语(参见 _Layout.cs).我的内容被翻译成英文,但 url 中的路由参数保持在nl"而不是en".
我该如何解决这个问题或者我做错了什么?
我尝试在 global.asax 中设置 RouteData 但没有帮助.
公共类 RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");route.IgnoreRoute("favicon.ico");路线.LowercaseUrls = true;路线.MapRoute(名称:错误",url: "错误/{操作}/{代码}",默认值:新 { 控制器 = 错误",动作 = 其他",代码 = RouteParameter.Optional });路线.MapRoute(name: "DefaultWithCulture",url: "{culture}/{controller}/{action}/{id}",默认值:new {culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional },约束:新{文化=[a-z]{2}"});//或者:[a-z]{2}-[a-z]{2}路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值: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){如果(HttpContext.Current.Session != null){CultureInfo ci = (CultureInfo)this.Session["Culture"];如果(ci == null){字符串 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["文化"] = 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" })//...
CultureController:(=我设置在 GlobalAsax 中使用的会话以更改 CurrentCulture 和 CurrentUICulture)
public class CultureController : 控制器{//获取:文化公共 ActionResult 索引(){return RedirectToAction("Index", "Home");}公共 ActionResult ChangeCulture(string lang, string returnUrl){Session["Culture"] = new CultureInfo(lang);如果 (Url.IsLocalUrl(returnUrl)){返回重定向(returnUrl);}别的{return RedirectToAction("Index", "Home");}}}
这种方法有几个问题,但归结为工作流程问题.
- 您有一个
CultureController
,其唯一目的是将用户重定向到站点上的另一个页面.请记住,RedirectToAction
会向用户的浏览器发送一个 HTTP 302 响应,它会告诉它在您的服务器上查找新位置.这是不必要的网络往返. - 当 URL 中已经存在用户的文化时,您正在使用会话状态来存储用户的文化.在这种情况下,会话状态完全没有必要.
- 您正在读取用户的
HttpContext.Current.Request.UserLanguages
,这可能与他们在 URL 中请求的区域性不同.
第三个问题主要是因为微软和谷歌在如何处理全球化问题上存在根本不同的观点.
Microsoft 的(原始)观点是,每种文化都应使用相同的 URL,并且浏览器的 UserLanguages
应确定网站应显示的语言.
Google 的观点是每种文化都应托管在不同的网址上.如果你仔细想想,这更有意义.每个在搜索结果 (SERP) 中找到您网站的人都希望能够以他们的母语搜索内容.
网站的全球化应被视为内容而不是个性化 - 您是在向群体而不是个人传播一种文化.因此,使用 ASP.NET 的任何个性化功能(例如会话状态或 cookie)来实现全球化通常没有意义 - 这些功能会阻止搜索引擎为您的本地化页面的内容编制索引.
如果您只需将用户路由到一个新的 URL 就可以将他们发送到不同的文化,那么就不用担心了 - 您不需要一个单独的页面让用户选择他们的文化,只需包含一个链接在页眉或页脚中更改现有页面的文化,然后所有链接将自动切换到用户选择的文化(因为 MVC 自动重用当前请求中的路由值).
解决问题
首先去掉CultureController
和Application_AcquireRequestState
方法中的代码.
文化过滤器
现在,由于文化是一个横切关注点,设置当前线程的文化应该在 IAuthorizationFilter
中完成.这可确保在 MVC 中使用 ModelBinder
之前设置区域性.
使用 System.Globalization;使用 System.Threading;使用 System.Web.Mvc;公共类 CultureFilter : IAuthorizationFilter{私有只读字符串 defaultCulture;公共文化过滤器(字符串默认文化){this.defaultCulture = defaultCulture;}public void OnAuthorization(AuthorizationContext filterContext){var values = filterContext.RouteData.Values;字符串文化=(字符串)值[文化"] ??this.defaultCulture;CultureInfo ci = new CultureInfo(culture);Thread.CurrentThread.CurrentCulture = ci;Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name);}}
您可以通过将过滤器注册为全局过滤器来全局设置过滤器.
公共类 FilterConfig{public static void RegisterGlobalFilters(GlobalFilterCollection 过滤器){过滤器.添加(新文化过滤器(默认文化:nl"));过滤器.添加(新的处理错误属性());}}
语言选择
您可以通过链接到当前页面的相同操作和控制器并将其作为选项包含在 _Layout.cshtml
的页眉或页脚中来简化语言选择.
@{var routeValues = this.ViewContext.RouteData.Values;var controller = routeValues["controller"] 作为字符串;var action = routeValues["action"] 作为字符串;}<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>
如前所述,页面上的所有其他链接将自动从当前上下文传递一种文化,因此它们将自动保持在相同的文化中.在这些情况下,没有理由明确地传递文化.
@ActionLink("关于", "关于", "首页")
有了上面的链接,如果当前的URL是/Home/Contact
,生成的链接将是/Home/About
.如果当前 URL 为 /en/Home/Contact
,则链接将生成为 /en/Home/About
.
默认文化
最后,我们进入了您问题的核心.您的默认区域性未正确生成的原因是路由是一种 2 向映射,无论您是匹配传入请求还是生成传出 URL,第一个匹配始终获胜.构建 URL 时,第一个匹配项是 DefaultWithCulture
.
通常,您可以通过颠倒路由顺序来解决此问题.但是,在您的情况下,这会导致传入路由失败.
因此,在您的情况下,最简单的选择是构建一个 自定义路由约束 来处理生成 URL 时默认文化的特殊情况.您只需在提供默认区域性时返回 false,这将导致 .NET 路由框架跳过 DefaultWithCulture
路由并移动到下一个注册路由(在本例中为 Default
).
使用 System.Text.RegularExpressions;使用 System.Web;使用 System.Web.Routing;公共类 CultureConstraint : IRouteConstraint{私有只读字符串 defaultCulture;私有只读字符串模式;公共文化约束(字符串默认文化,字符串模式){this.defaultCulture = defaultCulture;this.pattern = 模式;}公共布尔匹配(HttpContextBase httpContext,路线路线,字符串参数名称,RouteValueDictionary 值,RouteDirection routeDirection){if (routeDirection == RouteDirection.UrlGeneration &&this.defaultCulture.Equals(values[parameterName])){返回假;}别的{return Regex.IsMatch((string)values[parameterName], "^" + pattern + "$");}}}
剩下的就是将约束添加到您的路由配置中.您还应该删除 DefaultWithCulture
路由中文化的默认设置,因为您只希望它在 URL 中提供文化时匹配.另一方面,Default
路由应该有一种文化,因为无法通过 URL 传递它.
routes.LowercaseUrls = true;路线.MapRoute(名称:错误",url: "错误/{操作}/{代码}",默认值:new { controller = "Error", action = "Other", code = UrlParameter.Optional });路线.MapRoute(name: "DefaultWithCulture",url: "{culture}/{controller}/{action}/{id}",默认值:新 { 控制器 = 家",动作 = 索引",id = UrlParameter.Optional },约束:新{文化=新文化约束(默认文化:nl",模式:[a-z]{2}")});路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:new {culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional });
属性路由
注意:此部分仅适用于您使用 MVC 5 的情况.如果您使用的是以前的版本,则可以跳过此部分.
对于 AttributeRouting,您可以通过为每个操作自动创建 2 个不同的路由来简化事情.您需要稍微调整每个路由并将它们添加到 MapMvcAttributeRoutes
使用的相同类结构中.不幸的是,Microsoft 决定将类型设置为内部类型,因此需要使用反射来实例化和填充它们.
RouteCollectionExtensions
这里我们只是使用 MVC 的内置功能来扫描我们的项目并创建一组路由,然后在将实例添加到我们的MVC 路由表.
还有一个单独的路由用于解析 URL(与 AttributeRouting 的方法相同).
使用系统;使用 System.Collections;使用 System.Linq;使用 System.Reflection;使用 System.Web.Mvc;使用 System.Web.Mvc.Routing;使用 System.Web.Routing;公共静态类 RouteCollectionExtensions{public static void MapLocalizedMvcAttributeRoutes(这个RouteCollection路由,字符串urlPrefix,对象约束){MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(constraints));}public static void MapLocalizedMvcAttributeRoutes(这个RouteCollection路由,字符串urlPrefix,RouteValueDictionary约束){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);//首先将路由条目集合添加到路由集合中路线.添加((RouteBase)routeEntries);var localizedRouteTable = new RouteCollection();//获取属性路由的副本localizedRouteTable.MapMvcAttributeRoutes();foreach(localizedRouteTable 中的 var routeBase){如果 (routeBase.GetType().Equals(routeCollectionRouteType)){//获取_subRoutes 字段的值var tempSubRoutes = subRoutesInfo.GetValue(routeBase);//获取 Entries 属性的 PropertyInfoPropertyInfo entryInfo = subRouteCollectionType.GetProperty("Entries");if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable))){foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes)){var route = routeEntry.Route;//创建本地化路由var localizedRoute = CreateLocalizedRoute(路由,urlPrefix,约束);//添加本地化的路由条目var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute);AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry);//添加默认路由入口AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry);//添加本地化链接生成路由var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute);路线.添加(localizedLinkGenerationRoute);//添加默认链接生成路由var linkGenerationRoute = CreateLinkGenerationRoute(route);路由.添加(链接生成路由);}}}}}私有静态路由 CreateLocalizedRoute(路由路由,字符串 urlPrefix,RouteValueDictionary 约束){//添加 URL 前缀var routeUrl = urlPrefix + route.Url;//合并约束var routeConstraints = new RouteValueDictionary(constraints);foreach(route.Constraints 中的 var 约束){routeConstraints.Add(constraint.Key,constraint.Value);}返回新路由(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);}私有静态 RouteEntry CreateLocalizedRouteEntry(字符串名称,路由路由){var localizedRouteEntryName = string.IsNullOrEmpty(name) ?null : 名称 + "_Localized";返回新的 RouteEntry(localizedRouteEntryName, route);}私有静态无效AddRouteEntry(类型subRouteCollectionType,对象subRoutes,RouteEntry newEntry){var addMethodInfo = subRouteCollectionType.GetMethod("Add");addMethodInfo.Invoke(subRoutes, new[] { newEntry });}私有静态RouteBase CreateLinkGenerationRoute(路由innerRoute){var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc");返回(RouteBase)Activator.CreateInstance(linkGenerationRouteType,innerRoute);}}
然后就是调用这个方法而不是MapMvcAttributeRoutes
.
公共类RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");//调用以注册您的本地化和默认属性路由routes.MapLocalizedMvcAttributeRoutes(urlPrefix: "{culture}/",约束:新{文化=新文化约束(默认文化:nl",模式:[a-z]{2}")});路线.MapRoute(name: "DefaultWithCulture",url: "{culture}/{controller}/{action}/{id}",默认值:新 { 控制器 = 家",动作 = 索引",id = UrlParameter.Optional },约束:新{文化=新文化约束(默认文化:nl",模式:[a-z]{2}")});路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:new {culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional });}}
I've translated my mvc website, which is working great. If I select another language (Dutch or English) the content gets translated.This works because I set the culture in the session.
Now I want to show the selected culture(=culture) in the url.If it is the default language it should not be showed in the url, only if it is not the default language it should show it in the url.
e.g.:
For default culture (dutch):
site.com/foo
site.com/foo/bar
site.com/foo/bar/5
For non-default culture (english):
site.com/en/foo
site.com/en/foo/bar
site.com/en/foo/bar/5
My problem is that I always see this:
site.com/nl/foo/bar/5even if I clicked on English (see _Layout.cs). My content is translated in English but the route parameter in the url stays on "nl" instead of "en".
How can I solve this or what am I doing wrong?
I tried in the global.asax to set the RouteData but doesn't help.
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 (where I let user change language)
// ...
<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: (=where I set the Session that I use in GlobalAsax to change the CurrentCulture and 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");
}
}
}
There are several issues with this approach, but it boils down to being a workflow issue.
- You have a
CultureController
whose only purpose is to redirect the user to another page on the site. Keep in mindRedirectToAction
will send an HTTP 302 response to the user's browser, which will tell it to lookup the new location on your server. This is an unnecessary round-trip across the network. - You are using session state to store the culture of the user when it is already available in the URL. Session state is totally unnecessary in this case.
- You are reading the
HttpContext.Current.Request.UserLanguages
from the user, which might be different from the culture they requested in the URL.
The third issue is primarily because of a fundamentally different view between Microsoft and Google about how to handle globalization.
Microsoft's (original) view was that the same URL should be used for every culture and that the UserLanguages
of the browser should determine what language the website should display.
Google's view is that every culture should be hosted on a different URL. This makes more sense if you think about it. It is desirable for every person who finds your website in the search results (SERPs) to be able to search for the content in their native language.
Globalization of a web site should be viewed as content rather than personalization - you are broadcasting a culture to a group of people, not an individual person. Therefore, it typically doesn't make sense to use any personalization features of ASP.NET such as session state or cookies to implement globalization - these features prevent search engines from indexing the content of your localized pages.
If you can send the user to a different culture simply by routing them to a new URL, there is far less to worry about - you don't need a separate page for the user to select their culture, simply include a link in the header or footer to change the culture of the existing page and then all of the links will automatically switch to the culture the user has chosen (because MVC automatically reuses route values from the current request).
Fixing the Issues
First of all, get rid of the CultureController
and the code in the Application_AcquireRequestState
method.
CultureFilter
Now, since culture is a cross-cutting concern, setting the culture of the current thread should be done in an IAuthorizationFilter
. This ensures the culture is set before the ModelBinder
is used in MVC.
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);
}
}
You can set the filter globally by registering it as a global filter.
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CultureFilter(defaultCulture: "nl"));
filters.Add(new HandleErrorAttribute());
}
}
Language Selection
You can simplify the language selection by linking to the same action and controller for the current page and including it as an option in the page header or footer in your _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>
As mentioned previously, all other links on the page will automatically be passed a culture from the current context, so they will automatically stay within the same culture. There is no reason to pass the culture explicitly in those cases.
@ActionLink("About", "About", "Home")
With the above link, if the current URL is /Home/Contact
, the link that is generated will be /Home/About
. If the current URL is /en/Home/Contact
, the link will be generated as /en/Home/About
.
Default Culture
Finally, we get to the heart of your question. The reason your default culture is not being generated correctly is because routing is a 2-way map and regardless of whether you are matching an incoming request or generating an outgoing URL, the first match always wins. When building your URL, the first match is DefaultWithCulture
.
Normally, you can fix this simply by reversing the order of the routes. However, in your case that would cause the incoming routes to fail.
So, the simplest option in your case is to build a custom route constraint to handle the special case of the default culture when generating the URL. You simply return false when the default culture is supplied and it will cause the .NET routing framework to skip the DefaultWithCulture
route and move to the next registered route (in this case 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 + "$");
}
}
}
All that is left is to add the constraint to your routing configuration. You also should remove the default setting for culture in the DefaultWithCulture
route since you only want it to match when there is a culture supplied in the URL anyway. The Default
route on the other hand should have a culture because there is no way to pass it through the 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 }
);
AttributeRouting
For AttributeRouting, you can simplify things by automating the creation of 2 different routes for each action. You need to tweak each route a little bit and add them to the same class structure that MapMvcAttributeRoutes
uses. Unfortunately, Microsoft decided to make the types internal so it requires Reflection to instantiate and populate them.
RouteCollectionExtensions
Here we just use the built in functionality of MVC to scan our project and create a set of routes, then insert an additional route URL prefix for the culture and the CultureConstraint
before adding the instances to our MVC RouteTable.
There is also a separate route that is created for resolving the URLs (the same way that AttributeRouting does it).
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);
}
}
Then it is just a matter of calling this method instead of 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 }
);
}
}
这篇关于路由和 url 中的 ASP.NET MVC 5 文化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!