Multilingual WPF Applications: Part 2
In a previous post, I set up a way to bind a WPF interface to a class containing localised strings, so that the language of the entire application can be dynamically switched at runtime. This is unlike the standard .NET way of creating satellite assemblies, which gives you a separate version of your application for each different language / locale supported.
So far, this has been working nicely as I've been building up my WPF application, but one thing is becoming clear - it is tedious work repeating the same binding for every new TextBlock containing UI text (e.g. for labels and messages):
<TextBlock Text="{Binding Path=currentLanguage, Converter={StaticResource uiText}, ConverterParameter=ApplyAllLabel, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type uiCt:MainWindow}}}" />
There are two problems I see with this:
- It's long. Too long - especially how often these bindings are used throughout the entire application.
- The RelativeSource property setting is inefficient and fragile - traversing the hierarchy of visuals to find the parent window to retrieve the 'currentLanguage' property.
Firstly, avoiding the FindAncestor traversal. XAML has a nifty markup extension to reference a shared object, aptly named x:Static. This is perfect for the currentLanguage property as it is shared throughout the entire user interface. Using this also means that MainWindow is not the appropriate place to store such shared data.
Hence, the introduction of a ViewModel - in simple terms a 'Model' specific to the View.
The ViewModel I constructed for storing the language data that every part of the UI binds to uses the Singleton design pattern and implements the INotifyPropertyChanged interface. This part is crucial for enabling the UI to automatically update the bindings when the source is changed. Given that the shared instance of this ViewModel is called 'current', the binding becomes:
<TextBlock Text="{Binding Path=currentLanguage, Converter={StaticResource uiText}, ConverterParameter=ApplyAllLabel, Source={x:Static vm:CommonViewModel.current}}" />
where the 'vm' XML namespace is mapped to the namespace of the ViewModel in the solution.
Once the insides have been wired up to update the ViewModel, the application reassuringly runs the same as before, at least from a visual point of view.
However, the binding expression is still too long. I ended up making two attempts at making the XAML more succinct. As a learning exercise, the first attempt involved creating a custom markup extension (with inspiration from Philipp Sumi). To cut a long story short, attempting to return Binding objects to the Text property caused an invalid cast exception, since the Text property expects only a string. I learned that the Binding markup extension does not return a Binding object, but an object of the type expected by the DependencyProperty. (Philipp also posted an example of creating a custom Binding markup extension, alas I wasn't quite able to adapt that to my needs with success.)
In the end, what did work was using a custom attached property. This went into a static class called Extensions, and provided a callback handler for the PropertyChanged event. This handler method creates a Binding in code and assigns it to the Text property of the (cast) TextBlock.
So the DependencyProperty is defined as:
public static readonly DependencyProperty TextKeyProperty = DependencyProperty.RegisterAttached("TextKey", typeof(string), typeof(Extensions), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsMeasure, onTextKeyChanged));
and the callback effectively becomes:
private static void onTextKeyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { TextBlock castControl = obj as TextBlock; string key = e.NewValue as string; Binding lookupBinding = UITextLookupConverter.createBinding(key); castControl.SetBinding(TextBlock.TextProperty, lookupBinding); }
This is using the same value converter as before, except the static instance is defined in the UITextLookupConverter instead of XAML. The 'createBinding' method creates the Binding like so:
Binding languageBinding = new Binding("languageDefn") { Source = CommonViewState.current, Converter = _sharedConverter, ConverterParameter = key, };
Finally, with the appropriate XML namespaces mapped, the XAML becomes:
<TextBlock vm:Extensions.TextKey="ApplyAllLabel" />
Nice.
However, there is one drawback where creating a custom Binding markup extension would be a better strategy. This method is only useful for the Text property of a TextBlock. For other controls (e.g. custom controls) that display text in different properties, additional extended attributes would be needed for each unique DependencyProperty (even if similar custom controls can inherit from a common abstract base class, or implement a common interface to retrieve the appropriate DependencyProperty). A single custom (binding) markup extension would be able to be used for any property of any control that expects a string.
For now, this is progress.