wpfdata-bindingculturedynamicresource

What is the way to dynamically bind a language resource to a text property in WPF?


I know about DynamicResource and it works well. But in my program I have a TreeView and several DataTemplates (for categories and for items). And the element has its own name key(of resource). So items are created dynamically and I don't know which resource key they will use for name. The name text should change depending on the currently selected language.

In my program, I have a ListItemViewModel, here is a simplified code:

    internal class ListBlockViewModel : TreeViewItemViewModel
    {
        private readonly ListBlock _listBlock;

        public string? NameKey { get => _listBlock.NameKey; }

        public ListBlockViewModel(ListBlock listBlock)
        {
            _listBlock = listBlock;
        }
    }

And approximately the following XAML code in the DataTemplate in TreeView.Resources:

<DataTemplate DataType="{x:Type vmt:ListBlockViewModel}">
    <WrapPanel Width="200" HorizontalAlignment="Right">
        <TextBlock Text="{Binding NameKey, Converter={StaticResource ResKeyToValue}}" FontSize="14" Margin="5" Foreground="#b1b2b3"/>
        
    </WrapPanel>
</DataTemplate>

Now I use a simple key binding and use a converter to convert it to resource text. It works, but due to the fact that the binding occurs only once, and the NameKey property does not change, it turns out that when I change the language, the text will remain the same.
Code of converter:

    internal class ResourceKeyToValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(value is string key)
            {
                return Application.Current.TryFindResource(key) ?? $"[{key}]";
            }

            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

The language change is implemented in the MainWindowViewModel through a command call:


        public ICommand SwitchLanguageCommand { get; }

        private void OnSwitchLanguageCommandExecuted(object parameter)
        {
            if(parameter is not string cultureCode)
            {
                return;
            }

            Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureCode);
            Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(cultureCode);

            ResourceDictionary dict = new ResourceDictionary();
            dict.Source = new Uri($"Resources/Languages/Language-{cultureCode}.xaml", UriKind.Relative);

            Application.Current.Resources.MergedDictionaries.Clear();
            Application.Current.Resources.MergedDictionaries.Add(dict);
        }

So I'd like to know what other ways there are to implement changing the text language for elements for which I can't specify a dynamic resource name in advance? I need a way to get this resource based on the property's NameKey value and it would work like a regular DynamicResource.


Solution

  • There is no simple ready-made solution for your task.
    I also needed to solve a similar problem at one time.
    I created two helper classes class DynamicResourceBinding : Behavior<DependencyObject> and class DynamicResourceBindingExtension : MarkupExtension.
    You can take their source codes from here and copy them to your personal library: DynamicResource with a bindable key using the example of application localization

    With these classes, your XAML will look like this:

    <DataTemplate DataType="{x:Type vmt:ListBlockViewModel}">
        <WrapPanel Width="200" HorizontalAlignment="Right">
            <TextBlock Text="{local:DynamicResourceBinding {Binding NameKey, Mode=OneWay}}"
                       FontSize="14" Margin="5" Foreground="#b1b2b3"/>
            
        </WrapPanel>
    </DataTemplate>
    

    Other variant.
    If you have a key value change that only happens when you replace the entire dictionary, you can take a workaround.
    Add any key with a unique value for each dictionary to the dictionary. For example:

    <sys:String x:Key=lang>en-us</sys:String>
    

    In the App resources, you define any DependencyObject with a DependencyProperty that can take this unique value. For this example, the property must be of type string or object. And you set this property to the dynamic value of this unique key.

    <SomeObject x:Key="lang_proxy" SomeProperty="{DynamicResource lang}"/>
    

    Where you need to get the value of the bound keys, use MultiBinding with a multi converter that returns the value of the first binding. For your example, it will be enough to use StringFormat.

    <DataTemplate DataType="{x:Type vmt:ListBlockViewModel}">
        <WrapPanel Width="200" HorizontalAlignment="Right">
            <TextBlock FontSize="14" Margin="5" Foreground="#b1b2b3">
                <TextBlock.Text>
                    <MultiBinding StringFormat="{}{0}">
                        <Binding Path="NameKey"
                                 Converter="{StaticResource ResKeyToValue}"/>
                        <Binding Path="SomeProperty"
                                 Source="{StaticResource lang_proxy}"/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </WrapPanel>
    </DataTemplate>
    

    Explanation: MultiBinding is triggered when any of the bindings in its collection changes. In this case, the value of all bindings is re-evaluated. Therefore, when you replace a dictionary, it will have another unique value for the "lang" key. The value of this key dynamically gets the "SomeProperty" property, which means the value of this property will change. The value of this property is involved in all the evaluation MultiBindings that need to be changed when changing a dictionary, which causes all Bindings in these MultiBindings to be re-evaluated.