【问题标题】:CurrentUICulture ignores Region and Language settingsCurrentUICulture 忽略区域和语言设置
【发布时间】:2012-12-18 11:14:56
【问题描述】:

Windows 7 区域和语言对话框中的各种设置为 CurrentCulture 对象的属性提供值。但是,WPF 控件似乎改用 CurrentUICulture,导致完全无法尊重用户的偏好。

例如,在我的工作站上,WPF 控件似乎使用的是 en-US 的 CurrentUICulture,导致它们以美国格式 M/d/yyyy 显示日期,而不是在区域和语言对话框中指定的澳大利亚格式。

在数据绑定中明确指定 en-AU 文化会导致相关控件使用默认的澳大利亚格式,但它会继续忽略用户指定的格式。这很奇怪;进入应用程序我验证了 DateTimeFormatInfo.CurrentInfo == Thread.CurrentThread.CurrentCulture.DateTimeFormat (相同的对象)和 DateTimeFormatInfo.CurrentInfo.ShortDatePattern == "yyyy-MM-dd" (我设置的一个值,以便我可以确定用户偏好或正在选择默认值)。一切都如预期的那样,所以从表面上看,最大的问题是如何说服 WPF 控件和数据绑定使用 CurrentCulture 而不是 CurrentUICulture。

我们应该如何让 WPF 应用程序尊重区域和语言设置?


基于 Sphinxx 的回答,我重写了 Binding 类的 both 构造函数,以提供与标准标记的更完整兼容性。

using System.Globalization;
using System.Windows.Data;

namespace ScriptedRoutePlayback
{
  public class Bind : Binding
  {
    public Bind()
    {
      ConverterCulture = CultureInfo.CurrentCulture;
    }
    public Bind(string path) : base(path)
    {
      ConverterCulture = CultureInfo.CurrentCulture;
    }
  }
}

进一步的实验表明,您可以使用 x:Static 在标记中引用 System.Globalization.CultureInfo.CurrentCulture。这在运行时是完全成功的,但在设计时是灾难,因为绑定编辑器不断删除它。更好的解决方案是一个帮助类来遍历窗口的 DOM 并修复它找到的每个 Binding 的 ConverterCulture。

using System;
using System.Windows;
using System.Windows.Data;
using System.ComponentModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;

namespace ScriptedRoutePlayback
{
  public static class DependencyHelper
  {
    static Attribute[] __attrsForDP = new Attribute[] { new PropertyFilterAttribute(PropertyFilterOptions.SetValues | PropertyFilterOptions.UnsetValues | PropertyFilterOptions.Valid) };

    public static IList<DependencyProperty> GetProperties(Object element, bool isAttached = false)
    {
      if (element == null) throw new ArgumentNullException("element");

      List<DependencyProperty> properties = new List<DependencyProperty>();

      foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(element, __attrsForDP))
      {
        DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(pd);
        if (dpd != null && dpd.IsAttached == isAttached)
        {
          properties.Add(dpd.DependencyProperty);
        }
      }

      return properties;
    }

    public static IEnumerable<Binding> EnumerateBindings(DependencyObject dependencyObject)
    {
      if (dependencyObject == null) throw new ArgumentNullException("dependencyObject");

      LocalValueEnumerator lve = dependencyObject.GetLocalValueEnumerator();

      while (lve.MoveNext())
      {
        LocalValueEntry entry = lve.Current;

        if (BindingOperations.IsDataBound(dependencyObject, entry.Property))
        {
          Binding binding = (entry.Value as BindingExpression).ParentBinding;
          yield return binding;
        }
      }
    }

    /// <summary>
    /// Use in the constructor of each Window, after initialisation.
    /// Pass "this" as the dependency object and omit other parameters to have 
    /// all the bindings in the window updated to respect user customisations 
    /// of regional settings. If you want a specific culture then you can pass 
    /// values to recurse and cultureInfo. Setting recurse to false allows you 
    /// to update the bindings on a single dependency object.
    /// </summary>
    /// <param name="dependencyObject">Root dependency object for binding change treewalk</param>
    /// <param name="recurse">A value of true causes processing of child dependency objects</param>
    /// <param name="cultureInfo">Defaults to user customisations of regional settings</param>
    public static void FixBindingCultures(DependencyObject dependencyObject, bool recurse = true, CultureInfo cultureInfo = null)
    {
      if (dependencyObject == null) throw new ArgumentNullException("dependencyObject");
      try
      {
        foreach (object child in LogicalTreeHelper.GetChildren(dependencyObject))
        {
          if (child is DependencyObject)
          {
            //may have bound properties
            DependencyObject childDependencyObject = child as DependencyObject;
            var dProps = DependencyHelper.GetProperties(childDependencyObject);
            foreach (DependencyProperty dependencyProperty in dProps)
              RegenerateBinding(childDependencyObject, dependencyProperty, cultureInfo);
            //may have children
            if (recurse)
              FixBindingCultures(childDependencyObject, recurse, cultureInfo);
          }
        }
      }
      catch (Exception ex)
      {
        Trace.TraceError(ex.Message);
      }
    }

    public static void RegenerateBinding(DependencyObject dependencyObject, DependencyProperty dependencyProperty, CultureInfo cultureInfo = null)
    {
      Binding oldBinding = BindingOperations.GetBinding(dependencyObject, dependencyProperty);
      if (oldBinding != null)
        try
        {
          //Bindings cannot be changed after they are used.
          //But they can be regenerated with changes.
          Binding newBinding = new Binding()
          {
            Converter = oldBinding.Converter,
            ConverterCulture = cultureInfo ?? CultureInfo.CurrentCulture,
            ConverterParameter = oldBinding.ConverterParameter,
            FallbackValue = oldBinding.FallbackValue,
            IsAsync = oldBinding.IsAsync,
            Mode = oldBinding.Mode,
            NotifyOnSourceUpdated = oldBinding.NotifyOnSourceUpdated,
            NotifyOnTargetUpdated = oldBinding.NotifyOnValidationError,
            Path = oldBinding.Path,
            StringFormat = oldBinding.StringFormat,
            TargetNullValue = oldBinding.TargetNullValue,
            UpdateSourceExceptionFilter = oldBinding.UpdateSourceExceptionFilter,
            UpdateSourceTrigger = oldBinding.UpdateSourceTrigger,
            ValidatesOnDataErrors = oldBinding.ValidatesOnDataErrors,
            ValidatesOnExceptions = oldBinding.ValidatesOnExceptions,
            XPath = oldBinding.XPath
          };
          //set only one of ElementName, RelativeSource, Source
          if (oldBinding.ElementName != null)
            newBinding.ElementName = oldBinding.ElementName;
          else if (oldBinding.RelativeSource != null)
            newBinding.Source = oldBinding.Source;
          else
            newBinding.RelativeSource = oldBinding.RelativeSource;
          BindingOperations.ClearBinding(dependencyObject, dependencyProperty);
          BindingOperations.SetBinding(dependencyObject, dependencyProperty, newBinding);
        }
        catch (Exception ex)
        {
          Trace.TraceError(ex.Message);
        }
    }

  }
}

【问题讨论】:

    标签: wpf localization currentuiculture


    【解决方案1】:

    This SO post (WPF/Silverlight) 有一个指向 this article(仅限 WPF)的链接,说明如何使用 CurrentCulture 作为您的应用程序的默认设置。这能解决你的问题吗?

    编辑:

    使用“区域和语言”中的自定义设置而不是当前语言的默认设置进行绑定需要更多技巧。 This post 得出结论,每个绑定的ConverterCulture 也必须显式设置为CultureInfo.CurrentCulture。以下是一些 DateTime 测试:

    <!-- Culture-aware(?) bindings -->
    <StackPanel DataContext="{Binding Source={x:Static sys:DateTime.Now}}" >
    
        <!-- WPF's default en-US formatting (regardless of any culture/language settings) -->
        <TextBlock Text="{Binding Path=.}" />
    
        <!-- *Default* norwegian settings (dd.MM.YYY) -->
        <TextBlock Text="{Binding Path=., ConverterCulture=nb-NO}" />
    
        <!-- Norwegian settings from the "Region and Languague" dialog (d.M.YY) -->
        <TextBlock Text="{Binding Path=., ConverterCulture={x:Static sysglb:CultureInfo.CurrentCulture}}" />
    
        <!-- Hiding ConverterCulture initialization in our own custom Binding class as suggested here:
             https://stackoverflow.com/questions/5831455/use-real-cultureinfo-currentculture-in-wpf-binding-not-cultureinfo-from-ietfl#5937477 -->
        <TextBlock Text="{local:CultureAwareBinding Path=.}" />
    
    </StackPanel>
    

    自定义绑定类:

    public class CultureAwareBinding : Binding
    {
        public CultureAwareBinding()
        {
            this.ConverterCulture = System.Globalization.CultureInfo.CurrentCulture;
        }
    }
    

    这一切最终在挪威机器上看起来像这样:

    【讨论】:

    • 直接看WPF文章,因为我已经很看重Strahl了。这直接解决了我的具体问题,谢谢。
    • 或许不是。这对于卡在 en-US 中肯定是一个很大的改进,但它仍然不尊重个人设置。我将 UICulture 语言从属于文化语言,因此它具有适用于澳大利亚 (en-AU) 的正确语言、日期格式等,但它使用 defaults 并忽略自定义。
    • 让我们称之为..“有趣”?我做了更多测试并更新了我的帖子。
    • 这真是一个绝妙的回应。我至少没有猜到的附加信息的唯一部分是使用自定义绑定类的选项,但 that 是我永远猜不到你能做到的,而且它是真实的礼物。如果我能再次标记你的答案,我会的。但我不能,所以我决定标记你的一个 cmets。
    • 鉴于您的解决方案确实使用了 nil 构造函数,您有什么想法为什么它在设计时不起作用?我推测它确实可以工作,但由于某种原因,设计师中的 CurrentCulture 是 en-US。
    【解决方案2】:

    在 WPF 中有一种非常肮脏的方法,但据我所知,它是最好的,因为它无需任何其他额外代码或具有特定的文化感知绑定即可工作。您唯一需要做的就是在应用程序启动时调用 SetFrameworkElementLanguageDirty 方法(在答案下方),甚至在应用程序的构造函数中调用更好。

    方法 cmets 是不言自明的,但简而言之,该方法使用 CurrentCulture 覆盖 FrameworkElement 的 LanguageProperty 的默认元数据,包括用户从窗口进行的特定修改(如果有)。缩小/肮脏的部分是它使用反射来设置 XmlLanguage 对象的私有字段。

        /// <summary>
        ///   Sets the default language for all FrameworkElements in the application to the user's system's culture (rather than
        ///   the default "en-US").
        ///   The WPF binding will use that default language when converting types to their string representations (DateTime,
        ///   decimal...).
        /// </summary>        
        public static void SetFrameworkElementLanguageDirty()
        {
            // Note that the language you get from "XmlLanguage.GetLanguage(currentCulture.IetfLanguageTag)"
            // doesn't include specific user customizations, for example of date and time formats (Windows date and time settings).            
            var xmlLanguage = XmlLanguage.GetLanguage(Thread.CurrentThread.CurrentCulture.IetfLanguageTag);
            SetPrivateField(xmlLanguage, "_equivalentCulture", Thread.CurrentThread.CurrentCulture);
    
            FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(xmlLanguage));
        }
    

    SetPrivateField 方法可能如下所示。

        private static void SetPrivateField(object obj, string name, object value)
        {
            var privateField = obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
            if (privateField == null) throw new ArgumentException($"{obj.GetType()} doesn't have a private field called '{name}'.");
    
            privateField.SetValue(obj, value);
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-28
      • 1970-01-01
      • 2011-09-26
      • 2020-01-29
      相关资源
      最近更新 更多