Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Localization with expression-based bindings

Tags:

c#

maui

I'm following Gerald Versluis tutorial on Localization in .NET MAUI. He use the following extension for localizing texts in XAML:

[ContentProperty(nameof(Name))]
public class TranslateExtension : IMarkupExtension<BindingBase> {
    public string Name { get; set; }

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

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

But this gives the warning:

IL2026: Using member 'Microsoft.Maui.Controls.Binding.Binding()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Using bindings with string paths is not trim safe. Use expression-based binding instead.

How can I convert the binding in the code above to an expression-based binding?

I tried using BindingBase.Create:

return BindingBase.Create<LocalizationResourceManager, string> (
    lm => lm[Name], 
    BindingMode.OneWay, 
    source: LocalizationResourceManager.Instance);     

But I then get a compile error saying "The lambda must be static".

like image 380
torof Avatar asked Oct 24 '25 16:10

torof


1 Answers

[EDIT]

I think you're very close. You need to mark your expression with the static keyword. I believe the syntax you're looking for is:

return BindingBase.Create<LocalizationResourceManager, string> (
    static lm => lm[Name], 
    BindingMode.OneWay, 
    source: LocalizationResourceManager.Instance);  

To reproduce the trimming problem, I temporarily added the following lines to Gerald Versluis's tutorial project:

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
    <WarningsAsErrors>IL2026</WarningsAsErrors>
</PropertyGroup>

I have contributed the following improvements to TranslateExtension

  • Make Name a BindableProperty so it can come from your view model
  • Add a TranslatedName property so that the markup extension can react to changes to the rebuilt string
  • Optional (bonus) add X0 and X1 so that we can have string resources with 0, 1, or 2 parameters
[ContentProperty(nameof(Name))]
public class TranslateExtension : BindableObject, IMarkupExtension<BindingBase> {
    public static BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(TranslateExtension), null,
            propertyChanged: (b, o, n) => ((TranslateExtension)b).OnTranslatedNameChanged());
    public string Name
    {
        get => (string)GetValue(NameProperty);
        set => SetValue(NameProperty, value);
    }

    public static BindableProperty X0Property = BindableProperty.Create(nameof(X0), typeof(object), typeof(TranslateExtension), null,
            propertyChanged: (b, o, n) => ((TranslateExtension)b).OnTranslatedNameChanged());
    public object X0
    {
        get => GetValue(X0Property);
        set => SetValue(X0Property, value);
    }

    public static BindableProperty X1Property = BindableProperty.Create(nameof(X1), typeof(object), typeof(TranslateExtension), null,
            propertyChanged: (b, o, n) => ((TranslateExtension)b).OnTranslatedNameChanged());
    public object X1
    {
        get => GetValue(X1Property);
        set => SetValue(X1Property, value);
    }

    public string? TranslatedName
        => (Name is string name && LocalizationResourceManager.Instance[name] is string translatedName)
            ? String.Format(translatedName, new object[] { X0, X1 })
            : null;

    public void OnTranslatedNameChanged() => OnPropertyChanged(nameof(TranslatedName));

    public TranslateExtension()
    {
        LocalizationResourceManager.Instance.PropertyChanged += (s, e) => OnTranslatedNameChanged();
    }

    public BindingBase ProvideValue(IServiceProvider serviceProvider)
        => Binding.Create<TranslateExtension, string?>(
            static source => source.TranslatedName,
            mode: BindingMode.OneWay,
            source: this
        );

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
        => ProvideValue(serviceProvider);
}

With these additional changes, the following will be possible:

<Label Text="{local:Translate HelloWorld, X0={Binding User}, X1={Binding Weather}}" />
<!-- HelloWorld can now be a string resource with parameters, e.g. "Hello {0}, today's weather is {1}!" -->
<!-- User and Weather are strings from the view model -->

<!-- By making Translate.Name Bindable I can store translatable strings in the view model. -->
<CollectionView ItemsSources="{Binding Fruits}">
    <CollectionView.ItemTemplate>
         <DataTemplate>
             <Label Text="{local:Translate {Binding .}}" />
         </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

[MORE EDITS]

Here's another version of TranslateExtension that has a different implementation to how it detects and binds to the X0, X1 parameters which doesn't have the BindingContext issues.

[ContentProperty(nameof(Name))]
public class TranslateExtension : IMarkupExtension<BindingBase>, IMultiValueConverter {
    public string Name { get; set; }

    public object X0 { get; set; }

    public object X1 { get; set; }

    public BindingBase ProvideValue(IServiceProvider serviceProvider)
        => new MultiBinding
        {
            Bindings = new List<BindingBase> {
                BindingBase.Create<TranslateExtension, string?>(static source => source.Name, mode: BindingMode.OneWay, source: this),
                BindingBase.Create<LocalizationResourceManager, CultureInfo>(static lm => lm.Culture, mode: BindingMode.OneWay, source: LocalizationResourceManager.Instance),
                X0 is BindingBase x0Binding ? x0Binding : BindingBase.Create<object?, object?>(static obj  => obj, mode: BindingMode.OneWay, source: X0),
                X1 is BindingBase x1Binding ? x1Binding : BindingBase.Create<object?, object?>(static obj  => obj, mode: BindingMode.OneWay, source: X1),
            },
            Converter = this
        };

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
        => ProvideValue(serviceProvider);

    public object? Convert(object?[] values, Type targetType, object parameter, CultureInfo culture)
        => (values.Length > 1 && values[0] is string name && LocalizationResourceManager.Instance[name] is string translatedName)
        ? string.Format(translatedName, values.Skip(2).ToArray())
        : null;
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        => throw new NotImplementedException();
}

References:

  • https://github.com/jfversluis/MauiLocalizationSample/pull/1
like image 110
Stephen Quan Avatar answered Oct 26 '25 05:10

Stephen Quan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!