diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 7065e5bc32..7c6846484d 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -29,7 +29,7 @@ namespace Avalonia.Data.Core /// The . /// The type to convert the value to. public BindingExpression(ExpressionObserver inner, Type targetType) - : this(inner, targetType, DefaultValueConverter.Instance) + : this(inner, targetType, DefaultValueConverter.Instance, CultureInfo.InvariantCulture) { } @@ -39,6 +39,7 @@ namespace Avalonia.Data.Core /// The . /// The type to convert the value to. /// The value converter to use. + /// The converter culture to use. /// /// A parameter to pass to . /// @@ -47,9 +48,17 @@ namespace Avalonia.Data.Core ExpressionObserver inner, Type targetType, IValueConverter converter, + CultureInfo converterCulture, object? converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + : this( + inner, + targetType, + AvaloniaProperty.UnsetValue, + AvaloniaProperty.UnsetValue, + converter, + converterCulture, + converterParameter, priority) { } @@ -65,6 +74,7 @@ namespace Avalonia.Data.Core /// The value to use when the binding result is null. /// /// The value converter to use. + /// The converter culture to use. /// /// A parameter to pass to . /// @@ -75,6 +85,7 @@ namespace Avalonia.Data.Core object? fallbackValue, object? targetNullValue, IValueConverter converter, + CultureInfo converterCulture, object? converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) { @@ -85,6 +96,7 @@ namespace Avalonia.Data.Core _inner = inner; _targetType = targetType; Converter = converter; + ConverterCulture = converterCulture; ConverterParameter = converterParameter; _fallbackValue = fallbackValue; _targetNullValue = targetNullValue; @@ -96,6 +108,11 @@ namespace Avalonia.Data.Core /// public IValueConverter Converter { get; } + /// + /// Gets or sets the culture in which to evaluate the converter. + /// + public CultureInfo ConverterCulture { get; set; } + /// /// Gets a parameter to pass to . /// @@ -132,7 +149,7 @@ namespace Avalonia.Data.Core value, type, ConverterParameter, - CultureInfo.CurrentCulture); + ConverterCulture); if (converted == BindingOperations.DoNothing) { @@ -159,7 +176,7 @@ namespace Avalonia.Data.Core if (TypeUtilities.TryConvert( type, _fallbackValue, - CultureInfo.InvariantCulture, + ConverterCulture, out converted)) { _inner.SetValue(converted, _priority); @@ -214,7 +231,7 @@ namespace Avalonia.Data.Core value, _targetType, ConverterParameter, - CultureInfo.CurrentCulture); + ConverterCulture); if (converted == BindingOperations.DoNothing) { @@ -271,7 +288,7 @@ namespace Avalonia.Data.Core if (TypeUtilities.TryConvert( _targetType, _fallbackValue, - CultureInfo.InvariantCulture, + ConverterCulture, out converted)) { return new BindingNotification(converted); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 55aa262619..0b7b488b4b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -24,6 +24,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { Path = Path, Converter = Converter, + ConverterCulture = ConverterCulture, ConverterParameter = ConverterParameter, TargetNullValue = TargetNullValue, FallbackValue = FallbackValue, diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs index 6e7a65f258..c32804886d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs @@ -3,6 +3,8 @@ using System; using Avalonia.Controls; using Avalonia.Data.Converters; using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; +using System.Globalization; namespace Avalonia.Markup.Xaml.MarkupExtensions { @@ -24,6 +26,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { TypeResolver = serviceProvider.ResolveType, Converter = Converter, + ConverterCulture = ConverterCulture, ConverterParameter = ConverterParameter, ElementName = ElementName, FallbackValue = FallbackValue, @@ -41,6 +44,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IValueConverter? Converter { get; set; } + [TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))] + public CultureInfo? ConverterCulture { get; set; } + public object? ConverterParameter { get; set; } public string? ElementName { get; set; } diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 1d82725e57..129022aa2d 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -1,5 +1,7 @@ using System; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; @@ -35,6 +37,16 @@ namespace Avalonia.Data /// public IValueConverter? Converter { get; set; } + /// + /// Gets or sets the culture in which to evaluate the converter. + /// + /// The default value is null. + /// + /// If this property is not set then will be used. + /// + [TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))] + public CultureInfo? ConverterCulture { get; set; } + /// /// Gets or sets a parameter to pass to . /// @@ -120,6 +132,7 @@ namespace Avalonia.Data fallback, TargetNullValue, converter ?? DefaultValueConverter.Instance, + ConverterCulture ?? CultureInfo.CurrentCulture, ConverterParameter, Priority); diff --git a/src/Markup/Avalonia.Markup/Data/CultureInfoIetfLanguageTagConverter.cs b/src/Markup/Avalonia.Markup/Data/CultureInfoIetfLanguageTagConverter.cs new file mode 100644 index 0000000000..affda8b907 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/CultureInfoIetfLanguageTagConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using Avalonia.Metadata; + +namespace Avalonia.Data; + +[PrivateApi] +public class CultureInfoIetfLanguageTagConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string cultureName) + { + return CultureInfo.GetCultureInfoByIetfLanguageTag(cultureName); + } + + throw GetConvertFromException(value); + } +} diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 924e844ec5..23d8207dd0 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -125,7 +125,8 @@ namespace Avalonia.Base.UnitTests.Data.Core typeof(int), 42, AvaloniaProperty.UnsetValue, - DefaultValueConverter.Instance); + DefaultValueConverter.Instance, + CultureInfo.InvariantCulture); var result = await target.Take(1); Assert.Equal( @@ -147,7 +148,8 @@ namespace Avalonia.Base.UnitTests.Data.Core typeof(int), 42, AvaloniaProperty.UnsetValue, - DefaultValueConverter.Instance); + DefaultValueConverter.Instance, + CultureInfo.InvariantCulture); var result = await target.Take(1); Assert.Equal( @@ -169,7 +171,8 @@ namespace Avalonia.Base.UnitTests.Data.Core typeof(int), "bar", AvaloniaProperty.UnsetValue, - DefaultValueConverter.Instance); + DefaultValueConverter.Instance, + CultureInfo.InvariantCulture); var result = await target.Take(1); Assert.Equal( @@ -192,7 +195,8 @@ namespace Avalonia.Base.UnitTests.Data.Core typeof(int), "bar", AvaloniaProperty.UnsetValue, - DefaultValueConverter.Instance); + DefaultValueConverter.Instance, + CultureInfo.InvariantCulture); var result = await target.Take(1); Assert.Equal( @@ -228,7 +232,8 @@ namespace Avalonia.Base.UnitTests.Data.Core typeof(string), "9.8", AvaloniaProperty.UnsetValue, - DefaultValueConverter.Instance); + DefaultValueConverter.Instance, + CultureInfo.InvariantCulture); target.OnNext("foo"); @@ -260,6 +265,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, + CultureInfo.CurrentCulture, converterParameter: "foo"); target.Subscribe(_ => { }); @@ -278,6 +284,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, + CultureInfo.CurrentCulture, converterParameter: "foo"); target.OnNext("bar"); @@ -340,7 +347,8 @@ namespace Avalonia.Base.UnitTests.Data.Core typeof(string), AvaloniaProperty.UnsetValue, "bar", - DefaultValueConverter.Instance); + DefaultValueConverter.Instance, + CultureInfo.InvariantCulture); object result = null; target.Subscribe(x => result = x); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs index 680c49d098..6638a08496 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -1,8 +1,10 @@ using System; +using System.Globalization; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Data.Core; +using Moq; using Xunit; namespace Avalonia.Markup.UnitTests.Data @@ -135,6 +137,60 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("Hello True", textBlock.Text); } + [Fact] + public void ConverterCulture_Should_Be_Passed_To_Converter_Convert() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var culture = new CultureInfo("ar-SA"); + var converter = new Mock(); + var target = new Binding(nameof(Class1.Foo)) + { + Converter = converter.Object, + ConverterCulture = culture, + }; + + textBlock.Bind(TextBlock.TextProperty, target); + + converter.Verify(converter => converter.Convert( + "foo", + typeof(string), + null, + culture), + Times.Once); + } + + [Fact] + public void ConverterCulture_Should_Be_Passed_To_Converter_ConvertBack() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var culture = new CultureInfo("ar-SA"); + var converter = new Mock(); + var target = new Binding(nameof(Class1.Foo)) + { + Converter = converter.Object, + ConverterCulture = culture, + Mode = BindingMode.TwoWay, + }; + + textBlock.Bind(TextBlock.TextProperty, target); + textBlock.Text = "bar"; + + converter.Verify(converter => converter.ConvertBack( + "bar", + typeof(string), + null, + culture), + Times.Once); + } + private class Class1 { public string Foo { get; set; } = "foo"; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 9d82674fb6..673076d46a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1189,7 +1189,29 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions window.DataContext = new TestDataContext() { StringProperty = "Foo" }; - Assert.Equal("Foo+Bar", textBlock.Text); + Assert.Equal($"Foo+Bar+{CultureInfo.CurrentCulture}", textBlock.Text); + } + } + + [Fact] + public void SupportConverterWithCulture() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new TestDataContext() { StringProperty = "Foo" }; + + Assert.Equal($"Foo++ar-SA", textBlock.Text); } } @@ -1876,7 +1898,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public static IValueConverter Instance { get; } = new AppendConverter(); public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - => string.Format("{0}+{1}", value, parameter); + => string.Format("{0}+{1}+{2}", value, parameter, culture); public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index bcda3f855a..38b286fa39 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -1,5 +1,8 @@ +using System; +using System.Globalization; using System.Reactive.Subjects; using Avalonia.Controls; +using Avalonia.Data.Converters; using Avalonia.UnitTests; using Xunit; @@ -402,13 +405,49 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void ConverterCulture_Can_Be_Specified_By_Ietf_Language_Tag() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = Assert.IsType(window.Content); + + window.DataContext = new WindowViewModel(); + window.ApplyTemplate(); + + Assert.Equal("Hello+ar-SA", textBlock.Text); + } + } + private class WindowViewModel { public bool ShowInTaskbar { get; set; } public string Greeting1 { get; set; } = "Hello"; public string Greeting2 { get; set; } = "World"; } - + + public class CultureAppender : IValueConverter + { + public static CultureAppender Instance { get; } = new CultureAppender(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return $"{value}+{culture}"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + [Fact] public void Binding_Classes_Works() {