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

前端 未结 2 871
有刺的猬
有刺的猬 2020-12-09 13:36

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

相关标签:
2条回答
  • 2020-12-09 14:14

    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.

    0 讨论(0)
  • 2020-12-09 14:15

    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.

    0 讨论(0)
提交回复
热议问题