Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Runtime Locale Change, reevaluate ValueConverters UI

In a large WPF application, we have the possibility to change the language at runtime. We use WPF Localize Extension and resx-files for localization and it works great, except for converters used in UI. If in a binding a ValueConverter is culture-specific, the resulting text is not updated on the language change.

How can I make WPF update all converted bindings application-wide?

EDIT: At the moment we have experimented by making the ValueConverters MultiValueConverters and adding the locale as an extra value. This way the value source values change, and the result is updated. But this is cumbersome and ugly.

<MultiBinding Converter="{StaticResource _codeMultiConverter}" ConverterParameter="ZSLOOPAKT">
  <Binding Path="ActivityCode" />
  <Binding Source="{x:Static lex:LocalizeDictionary.Instance}" Path="Culture" />
  <Binding Source="{x:Static RIEnums:CodeTypeInfo+CodeDisplayMode.Both}" />
</MultiBinding>

Related: Run-time culture change and IValueConverter in a binding (I don't have the option to raise propertychanged for every field manually)

like image 705
Jeremy Avatar asked May 15 '17 08:05

Jeremy


2 Answers

This is our solution. I hope I understood your problem that you want to change for example DateTime?

The Converter is a simple IValueConverter which convert the value to the current Language. Translator is a static class which holds (for example) the CurrentLanguage (en-en / de-de) as string.

The Behavior is needed to update the Bindings if the language has changed. We only need this implementation 3-4 times in the hole program, because it is only for the DateTime formating. All other texts are hold in a dynamic Resource..

But I think for your needs the Behavior is the right one.

Converter

public class CultureConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null)
        {
            DateTime dateTime;
            if(DateTime.TryParse(value.ToString(), out dateTime))
            {
                if(parameter != null)
                {
                    return dateTime.ToString(parameter.ToString(), new CultureInfo(Translator.CurrentLanguage));
                }
                return dateTime.ToString(new CultureInfo(Translator.CurrentLanguage));
            }
            return null;
        }
        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}

Behavior

public class CultureConverter : Behavior<FrameworkElement>
{
    private FrameworkElement _HostingControl;
    private DependencyProperty _HostingControlDependencyProperty;

    protected override void OnAttached()
    {
        base.OnAttached();

        _HostingControl = AssociatedObject;

        _InitHostingControl();
        Translator.LanguageChanged += Translator_LanguageChanged;
    }

    protected override void OnDetaching()
    {
        Translator.LanguageChanged -= Translator_LanguageChanged;

        base.OnDetaching();
    }

    private void Translator_LanguageChanged(string languageCode)
    {
        if(_HostingControlDependencyProperty != null)
            _HostingControl.GetBindingExpression(_HostingControlDependencyProperty).UpdateTarget();
    }

    private void _InitHostingControl()
    {
        if(_HostingControl is TextBlock)
        {
            _HostingControlDependencyProperty = TextBlock.TextProperty;
        }
        else if (typeof(TextBox) == _HostingControl.GetType())
            _HostingControlDependencyProperty = TextBox.TextProperty;
    }

XAML

<Window.Resources>
    <XamlConverter:CultureConverter x:Key="CultureConverter"/>
<Window.Resources>


<DataGridTemplateColumn.CellTemplate>
    <DataTemplate>
        <TextBlock 
            Text="{Binding CreatedOn, ConverterParameter=f, Converter={StaticResource CultureConverter}, UpdateSourceTrigger=PropertyChanged}">
            <i:Interaction.Behaviors>
                <Behaviors:CultureConverter/>
            </i:Interaction.Behaviors>
        </TextBlock>
    </DataTemplate>
</DataGridTemplateColumn.CellTemplate>

Preview

preview

like image 81
Dominic Jonas Avatar answered Oct 24 '22 17:10

Dominic Jonas


As an option - you can create wrapper markup extension around Binding, like this:

public class LocBindingExtension : MarkupExtension {
    public BindingBase Binding { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider) {
        if (Binding == null)
            return null;

        // Binding is by itself MarkupExtension
        // Call its ProvideValue
        var expression = Binding.ProvideValue(serviceProvider) as BindingExpressionBase;
        if (expression != null) {                
            // if got expression - create weak reference
            // you don't want for this to leak memory by preventing binding from GC
            var wr = new WeakReference<BindingExpressionBase>(expression);
            PropertyChangedEventHandler handler = null;
            handler = (o, e) => {                    
                if (e.PropertyName == nameof(LocalizeDictionary.Instance.Culture)) {
                    BindingExpressionBase target;
                    // when culture changed and our binding expression is still alive - update target
                    if (wr.TryGetTarget(out target))
                        target.UpdateTarget();
                    else
                        // if dead - unsubsribe
                        LocalizeDictionary.Instance.PropertyChanged -= handler;
                }

            };
            LocalizeDictionary.Instance.PropertyChanged += handler;
            return expression;
        }
        // return self if there is no binding target (if we use extension inside a template for example)
        return this;  
    }
}

Use like this:

<TextBlock Text="{my:LocBinding Binding={Binding ActivityCode, Converter={StaticResource myConverter}}}" />

You can provide any binding (including MultiBinding) and use any property where binding can be applied.

If you think that even this is too verbose - you can wrap binding in a different way - by mirroring all properties of Binding class you need on your markup extension and forward them to underlying binding. In this case you will have to write a bit more code, and you will need to have separate classes for Binding and MultiBinding (in case you need MultiBinding too). Best way would be to inherit from Binding and override it's ProvideValue, but it's not virtual so not possible to do that, and I didn't find any other methods you can override to achieve the result. Here is a sketch with just 2 properties of binding:

public class LocBindingExtension : MarkupExtension {
    private readonly Binding _inner;
    public LocBindingExtension() {
        _inner = new Binding();
    }

    public LocBindingExtension(PropertyPath path) {
        _inner = new Binding();
        this.Path = path;
    }

    public IValueConverter Converter
    {
        get { return _inner.Converter; }
        set { _inner.Converter = value; }
    }

    public PropertyPath Path
    {
        get { return _inner.Path; }
        set { _inner.Path = value; }
    }

    public override object ProvideValue(IServiceProvider serviceProvider) {            
        // Binding is by itself MarkupExtension
        // Call its ProvideValue
        var expression = _inner.ProvideValue(serviceProvider) as BindingExpressionBase;
        if (expression != null) {                
            // if got expression - create weak reference
            // you don't want for this to leak memory by preventing binding from GC
            var wr = new WeakReference<BindingExpressionBase>(expression);
            PropertyChangedEventHandler handler = null;
            handler = (o, e) => {                    
                if (e.PropertyName == nameof(LocalizeDictionary.Instance.Culture)) {
                    BindingExpressionBase target;
                    // when culture changed and our binding expression is still alive - update target
                    if (wr.TryGetTarget(out target))
                        target.UpdateTarget();
                    else
                        // if dead - unsubsribe
                        LocalizeDictionary.Instance.PropertyChanged -= handler;
                }

            };
            LocalizeDictionary.Instance.PropertyChanged += handler;
            return expression;
        }
        // return self if there is no binding target (if we use extension inside a template for example)
        return this;  
    }
}

Then usage is simplified to just:

<TextBlock Text="{my:LocBinding ActivityCode, Converter={StaticResource myConverter}}" />

You can add more properties (like Mode and so on) as needed.

like image 34
Evk Avatar answered Oct 24 '22 17:10

Evk