c# - 路由和 url 中的 ASP.NET MVC 5 文化

标签 c# .net asp.net-mvc asp.net-mvc-5 asp.net-mvc-routing

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

现在我想在 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/荷兰 /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 中设置 session 以更改 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 中已经可用时,您正在使用 session 状态来存储用户的文化。在这种情况下, session 状态是完全没有必要的。
  • 您正在阅读 HttpContext.Current.Request.UserLanguages来自用户,这可能与他们在 URL 中请求的区域性不同。

  • 第三个问题主要是因为微软和谷歌在如何处理全局化问题上存在根本不同的观点。

    Microsoft 的(原始)观点是,每种文化都应使用相同的 URL,并且 UserLanguages浏览器应该决定网站应该显示什么语言。

    谷歌的观点是every culture should be hosted on a different URL .如果你仔细想想,这更有意义。每个在搜索结果 (SERP) 中找到您网站的人都希望能够以他们的母语搜索内容。

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

    如果您只需将用户路由到一个新的 URL 就可以将他们发送到不同的文化,那么就不用担心了 - 您不需要一个单独的页面让用户选择他们的文化,只需在标题中包含一个链接或页脚更改现有页面的文化,然后所有链接将自动切换到用户选择的文化(因为 MVC automatically reuses route values from the current request)。

    解决问题

    首先,去掉CultureController以及 Application_AcquireRequestState 中的代码方法。

    文化过滤器

    现在,由于文化是一个跨领域的关注点,设置当前线程的文化应该在 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());
        }
    }
    

    语言选择

    您可以通过链接到当前页面的相同操作和 Controller 并将其作为选项包含在 _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 .

    默认文化

    最后,我们进入了您问题的核心。您的默认区域性未正确生成的原因是路由是一种 2 向映射,无论您是匹配传入请求还是生成传出 URL,第一个匹配始终获胜。构建 URL 时,第一个匹配项是 DefaultWithCulture .

    通常,您可以通过颠倒路由顺序来解决此问题。但是,在您的情况下,这会导致传入路由失败。

    因此,在您的情况下,最简单的选择是构建 custom route constraint在生成 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 }
    );
    

    属性路由

    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 相同的类结构中。使用。不幸的是,Microsoft 决定将类型设置为内部类型,因此需要使用反射来实例化和填充它们。

    路由集合扩展

    这里我们只是使用 MVC 的内置功能来扫描我们的项目并创建一组路由,然后为文化和 CultureConstraint 插入一个额外的路由 URL 前缀。在将实例添加到我们的 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 }
            );
        }
    }
    

    关于c# - 路由和 url 中的 ASP.NET MVC 5 文化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32764989/

    相关文章:

    c# - WPF UI 动画库

    c# - 为什么不能将迭代器 block 与 IOrderedEnumerable 一起使用

    c# - TextBox 在 '\0' 字符处截断字符串

    c# - System.ValueTuple 和 System.Tuple 有什么区别?

    c# - 如何创建具有未在类中指定的成员的对象?

    c# - 运行 azure 项目时出现 SEHException

    html - 加载时更改的默认文本框值

    c# - eBay Finding API .NET SDK 'findItemsAdvanced' 返回空响应

    c# - 如何通过分组将 <Entities> 列表转换为一个字符串?

    c# - 从表中删除一行似乎失败