重新计算由标记扩展计算的xaml页面中的所有值

本文关键字:计算 xaml 新计算 扩展 | 更新日期: 2023-09-27 18:17:26

在xaml页面上的xamarin应用程序中,我使用xaml扩展加载本地化字符串(详细信息在这里描述)。例如:

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

现在,我希望用户能够在运行时更改应用程序的语言(使用选择器)。如果发生这种情况,我想立即更改语言。

我可以重新加载所有翻译的文本吗?

我可以删除所有页面并重新创建它们,但我试图避免这种情况。

我还可以将所有本地化文本绑定到pages模型中的字符串。但是对于真正的静态字符串来说,这是很多不必要的代码。

重新计算由标记扩展计算的xaml页面中的所有值

不幸的是,您不能强制在XAML中使用标记扩展名设置的控件使用这些扩展名重新评估它们的属性—评估只在解析XAML文件时完成一次。在幕后发生的事情是这样的:

  1. 您的扩展被实例化
  2. 在创建的实例上调用
  3. ProvideValue方法,并在目标控件
  4. 上使用返回值。
  5. 对创建实例的引用不存储(或者是弱引用,我不确定),所以您的扩展已准备好GC

你可以通过定义终结器(析构函数)并在其中设置断点来确认你的扩展只被使用一次。它将在页面加载后不久被击中(至少在我的情况下是这样的—您可能需要显式地调用GC.Collect())。所以我认为问题很清楚-你不能在任意时间再次调用ProvideValue,因为它可能不再存在。

然而,有一个解决您的问题,它甚至不需要对您的XAML文件进行任何更改-您只需要修改TranslateExtension类。其思想是,在底层,它将设置正确的绑定,而不是简单地返回一个值。

首先,我们需要一个类作为所有绑定的源(我们将使用单例设计模式):

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));
    }
}

这里的目标是Translator.Instance["Label_Text"]应该返回当前扩展为"Label_Text"返回的翻译。然后扩展应该在ProvideValue方法中设置绑定:

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);
    }
}

现在所需要做的就是在每次语言改变时调用Translator.Instance.Invalidate()

请注意,使用{i18n:Translate Label_Text}将等同于使用{Binding [Label_Text], Source={x:Static i18n:Translator.Instance}},但更简洁,并节省了修改XAML文件的工作。

我曾尝试实现@Grx70提出的伟大解决方案,但示例中使用的一些类和属性是Xamarin内部的,因此不能以这种方式使用。参考他们最后的评论,这是让它工作的线索,虽然不像最初提议的那么优雅,我们可以这样做:

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;
    }        
}

,这是最初建议的Translator类,但为了清晰起见,这里用GetString调用复制:

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));
    }
}

然后按照原文的建议,不要用:

来绑定文本
{i18n:Translate Label_Text}

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

我在项目结束时就会这样做(添加多种语言),但是使用Visual Studio Community和Search/Replace with RegEx,可以在整个项目中替换绑定,替换:

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

:

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

注意:Regex假设'resources'命名空间用于原始的Translate宏,'core'命名空间用于Translator类,您可能需要根据需要进行更新。我很欣赏这是@Grx70的一个小调整,否则很好的解决方案(我站在巨人的肩膀上),但我在这里发布这篇文章是为了让任何有同样问题的人得到这个工作。