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".
[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
Name a BindableProperty so it can come from your view modelTranslatedName property so that the markup extension can react to changes to the rebuilt stringX0 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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With