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

2

10

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.

Were answered 11/11, 2016 at 19:4 Comment(0)
C
19

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.

Caber answered 12/11, 2016 at 21:6 Comment(3)
Great solution to update in real time all translation texts loaded through MarkupExtension. Thank you very much for sharing this.Jolynnjon
Good answer! but if you want to apply the same thing to .NET MAUI, you need to implement IMarkupExtension<BindingBase> instead of extending MarkupExtensionChromaticity
Great solution. A note for anyone trying, that the binding will return nul if you try to use an enum as the indexer property input parameter, even if the indexer property when called from code returns your string. Instead, use a string and Enum.TryParse in the getter.Terracotta
B
9

I'd tried to implement @Grx70's great proposed solution, but some of the classes and properties the example used are internal to Xamarin so couldn't be used in that way. Picking up on their last comment though, was the clue to get it working, though not quite as elegantly as initially proposed, we can do this:

public class TranslateExtension : IMarkupExtension<BindingBase>
{       
    public TranslateExtension(string text)
    {
        Text = text;            
    }

    public string Text { get; set; }

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
    {
    return ProvideValue(serviceProvider);
    }

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

and this the Translator class as initially proposed, but reproduced here for clarity with the GetString call:

public class Translator : INotifyPropertyChanged
{
    public string this[string text]
    {
    get
    {
        return Strings.ResourceManager.GetString(text, Strings.Culture);
    }
    }        

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

    public event PropertyChangedEventHandler PropertyChanged;

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

Then as the original post suggested, instead of binding text with:

{i18n:Translate Label_Text}

Bind

{Binding [Label_Text], Source={x:Static i18n:Translator.Instance}}

I'd hit this right at the end of a project (adding the multiple languages), but using Visual Studio Community and Search/Replace with RegEx, the binding can be replaced across the project, replacing:

\{resources:Translate (.*?)\}

with:

{Binding [$1], Source={x:Static core:Translator.Instance}}

NOTE: The Regex assumes the 'resources' namespace for the original Translate macro, and 'core' namespace for the Translator class, you may have to update as appropriate. I appreciate this is a small tweak to @Grx70's otherwise great solution (I'm standing on the shoulders of giants with this one), but I'm posting this here for any that follow with the same problem of getting this working.

Bricklaying answered 9/5, 2017 at 9:40 Comment(4)
I implemented it in the same way as you, however, I didn't need to alter the binding text. Why would you change the binding to include path? The path is already set in the markupextension when creating the bindingLoni
Would you please explain to me what is the relation between TranslateExtension and Translator. I can use Translator Class only to get what I want. So what is the use of TranslateExtension and does it give any additional flexibility?Reseat
@TarekSalah you can use TranslateExtension in order to simplify your XAML. You can just write {i18n:Translate Label_Text} and this will use TranslateExtension which, in turn, will set up the binding through the Translator class.Wagtail
Ah yes, my bad. At the time I worked with WPF and I shamefully admit I didn't pay enough attention to notice the question, while related to XAML, was not about WPF. I'm glad though you were able to make use of it to come to your own solution. I guess that's how SO works for me anyway - it seldom gives me a direct solution, usually it gives me just enough clues to mold them into a solution solving my exact problem.Caber

© 2022 - 2024 — McMap. All rights reserved.