Nathan Nathan - 22 days ago 7
C# Question

Re-evaluate all values in xaml page calculated by a markup-extension

In a xamarin app on a xaml page I am loading localized strings using a xaml extension (the details are described here). For example:

<Label Text={i18n:Translate Label_Text}/>


Now, I want the user to be able to change the language of the app at runtime (using a picker). If that happens, I want to change the language immediately.

Can I somehow reload all translated texts?

I could delete all pages and recreate them, but I am trying to avoid that.

I could also bind all localised texts to strings in the pages model. But that is a lot of unnecessary code for truly static strings.

Answer

Unfortunately you cannot force controls set up with markup extensions in XAML to reevaluate their properties using those extensions - the evaluation is only done once upon parsing XAML file. What basically happens behind the scenes is this:

  1. Your extension is instantiated
  2. ProvideValue method is called on the created instance and the returned value is used on the target control
  3. The reference to the created instance is not stored (or is a weak reference, I'm not sure), so your extension is ready for GC

You can confirm that your extension is only used once by defining a finalizer (desctructor) and setting a breakpoint in it. It will be hit soon after your page is loaded (at least it was in my case - you may need to call GC.Collect() explicitly). So I think the problem is clear - you cannot call ProvideValue on your extension again at an arbitrary time, because it possibly no longer exists.

However, there is a solution to your problem, which doesn't even need making any changes to your XAML files - you only need to modify the TranslateExtension class. The idea is that under the hood it will setup proper binding rather than simply return a value.

First off we need a class that will serve as a source for all the bindings (we'll use singleton design pattern):

public class Translator : INotifyPropertyChanged
{
    public string this[string text]
    {
        get
        {
            //return translation of "text" for current language settings
        }
    }

    public static Translator Instance { get; } = new Translator();

    public event PropertyChangedEventHandler PropertyChanged;

    public void Invalidate()
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Binding.IndexerName));
    }
}

The goal here is that Translator.Instance["Label_Text"] should return the translation that your current extension returns for "Label_Text". Then the extension should setup the binding in the ProvideValue method:

public class TranslateExtension : MarkupExtension
{
    public TranslateExtension(string text)
    {
        Text = text;
    }

    public string Text { get; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var binding = new Binding
        {
            Mode = BindingMode.OneWay,
            Path = new PropertyPath($"[{Text}]"),
            Source = Translator.Instance,
        };
        return binding.ProvideValue(serviceProvider);
    }
}

Now all you need to do is to call Translator.Instance.Invalidate() every time the language is changed.

Note that using {i18n:Translate Label_Text} will be equivalent to using {Binding [Label_Text], Source={x:Static i18n:Translator.Instance}}, but is more concise and saves you the effort of revising your XAML files.

Comments