CurrentUICulture ignores Region and Language settings
Asked Answered
P

2

8

Assorted settings in the Windows 7 Region and Language dialog supply values to the properties of the CurrentCulture object. However, WPF controls seem to use CurrentUICulture instead, resulting in a total failure to respect the user's preferences.

On my workstation, for example, WPF controls seem to use CurrentUICulture which is en-US, causing them to display dates with the American format M/d/yyyy rather than the Australia format specified in the Region and Language dialog.

Explicitly specifying a culture of en-AU in a databinding causes the control in question to to use default Australian formats, but it continues to ignore the user specified formats. This is odd; stepping into the app I verified that DateTimeFormatInfo.CurrentInfo == Thread.CurrentThread.CurrentCulture.DateTimeFormat (same object) and DateTimeFormatInfo.CurrentInfo.ShortDatePattern == "yyyy-MM-dd" (a value I set so I could determine whether user preferences or defaults were being picked up). Everything was as expected, so on the face of things the big question is how to persuade WPF controls and databindings to use CurrentCulture rather than CurrentUICulture.

How are we supposed to get WPF apps to respect the Region and Language settings?


Building on Sphinxx's answer, I overrode both constructors of the Binding class to provide more complete compatibility with standard markup.

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;
    }
  }
}

Further experimentation reveals that you can use x:Static to reference System.Globalization.CultureInfo.CurrentCulture in markup. This is a complete success at run-time but a disaster at design-time because the binding editor keeps removing it. A better solution is a helper class to traverse the DOM of a window and fix up the ConverterCulture of every Binding it finds.

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);
        }
    }

  }
}
Pasteurize answered 4/1, 2013 at 1:46 Comment(0)
D
9

This SO post (WPF/Silverlight) has a link to this article (WPF only), explaining how to use CurrentCulture as the default for your application. Does that solve your problem?

EDIT:

Making bindings use the custom settings from "Region and Language" instead of the current language's default settings requires some more trickery. This post concludes that every binding's ConverterCulture also has to be explicitly set to CultureInfo.CurrentCulture. Here are some DateTime tests:

<!-- 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://mcmap.net/q/364847/-use-quot-real-quot-cultureinfo-currentculture-in-wpf-binding-not-cultureinfo-from-ietflanguagetag#5937477 -->
    <TextBlock Text="{local:CultureAwareBinding Path=.}" />

</StackPanel>

The custom binding class:

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

It all ends up looking like this on a norwegian machine:

WPF DateTime Bindings

Drizzle answered 4/1, 2013 at 19:16 Comment(6)
Looked directly at the WPF article since I already have high regard for Strahl. This directly addresses my specific issue, thanks.Pasteurize
Or perhaps not. It's certainly a big improvement on being stuck with en-US but it's still not respecting the individual settings. I'm slaving the UICulture language to the Culture language, so it has the correct language, date format etc for Australia (en-AU) but it's using the defaults and ignoring customisations.Pasteurize
Let's call that.. "interesting"? I have done some more tests and updated my post.Drizzle
That's a bloody brilliant response. The only part of your additional info that I hadn't at least guessed at is the option to use a custom binding class, but that is something I never would have guessed you could do, and it's a real gift. If I could mark your answer up again I would. But I can't so I've settled for marking one of your comments up.Pasteurize
Any thoughts on why it doesn't work at design-time, given that your solution does use the nil constructor? I speculate that it does work but for some reason CurrentCulture in the designer is en-US.Pasteurize
Much appreciated :) No, I don't know why it doesn't work in the designer. My guess would be the same as yours..Drizzle
T
2

There is one very dirty way to do it in WPF, but as far as I could find it is the best, because it works without any other extra code or having specific culture aware bindings. The only thing you have to do is call SetFrameworkElementLanguageDirty method (below in the answer) in your application startup or even better in constructor of the App.

The method comments are self-explanatory, but in short the method overrides the default metadata of LanguageProperty of FrameworkElement with CurrentCulture including user's specific modification from windows, if there are any. The downsize/dirty part is that it is using reflection to set a private field of XmlLanguage object.

    /// <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));
    }

The SetPrivateField method can look like this.

    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);
    }
Tensor answered 25/2, 2016 at 11:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.