一尘不染

路由和URL中的ASP.NET MVC 5文化

c#

我已经翻译了我的mvc网站,该网站运行良好。如果我选择其他语言(荷兰语或英语),则会翻译内容。之所以有效,是因为我在会议中设定了文化。

现在,我想在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)。我的内容已翻译成英文,但网址中的route参数保留在“ 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");
      }
    }
  }

阅读 166

收藏
2020-05-19

共1个答案

一尘不染

这种方法存在多个问题,但是归结为工作流程问题。

  1. CultureController的唯一目的是将用户重定向到网站上的另一个页面。请记住,RedirectToAction会将HTTP 302响应发送到用户的浏览器,这将告诉它在服务器上查找新位置。这是跨网络不必要的往返。
  2. 您正在使用会话状态存储用户的区域性(如果URL中已有该区域性)。在这种情况下,完全不需要会话状态。
  3. 您正在HttpContext.Current.Request.UserLanguages从用户那里读取,这可能与他们在URL中要求的区域性有所不同。

第三个问题主要是由于微软和Google在如何处理全球化方面存在根本不同的看法。

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

Google的观点是,每种文化都应托管在不同的URL上。如果您考虑一下,这更有意义。希望每个在搜索结果(SERP)中找到您的网站的人都能够以其母语搜索内容。

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

如果您可以简单地通过将用户路由到新的URL来使用户具有不同的文化,则无需担心-
您不需要单独的页面供用户选择其文化,只需在标题中包含一个链接或页脚更改现有页面的区域性,然后所有链接将自动切换到用户选择的区域性(因为MVC
自动重用当前请求中的路由值)。

解决问题

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

CultureFilter

现在,由于文化是一个跨领域的问题,因此应在中设置当前线程的文化IAuthorizationFilter。这样可以确保ModelBinder在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);
    }
}

您可以通过将其注册为全局过滤器来全局设置过滤器。

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

语言选择

您可以通过链接到当前页面的相同动作和控制器,并将其作为选项包含在页面的页眉或页脚中来简化语言选择_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。如果当前网址为/en/Home/Contact,则链接将生成为/en/Home/About

默认文化

最后,我们深入您的问题。无法正确生成默认区域性的原因是,路由是2向映射,并且无论您是匹配传入的请求还是生成传出的URL,始终都会赢得第一个匹配项。构建网址时,第一个匹配项是DefaultWithCulture

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

因此,您遇到的最简单的选择是建立一个自定义路由约束,以在生成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 + "$");
        }
    }
}

剩下的就是将约束添加到路由配置中。您还应该删除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 }
);

属性路由

注意: 本部分仅在使用MVC 5时适用。如果使用早期版本,则可以跳过此部分。

对于AttributeRouting,您可以通过自动为每个动作创建2条不同的路线来简化操作。您需要对每条路线进行一些微调,并将它们添加到使用的相同类结构中MapMvcAttributeRoutes。不幸的是,Microsoft决定将这些类型设为内部类型,因此需要Reflection实例化并填充它们。

RouteCollectionExtensions

在这里,我们只使用MVC的内置功能来扫描我们的项目并创建一组路由,然后为区域性插入一个附加的路由URL前缀,然后再将CultureConstraint实例添加到我们的MVC
RouteTable中。

还创建了一个单独的路由来解析URL(与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 }
        );
    }
}
2020-05-19