Browse Source

Added ConverterCulture to bindings. (#12876)

Similar to WPF's `Binding.ConverterCulture` property.
Steven Kirk 2 years ago
parent
commit
8948aa7e52

+ 23 - 6
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -29,7 +29,7 @@ namespace Avalonia.Data.Core
         /// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
         /// <param name="targetType">The type to convert the value to.</param>
         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
         /// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
         /// <param name="targetType">The type to convert the value to.</param>
         /// <param name="converter">The value converter to use.</param>
+        /// <param name="converterCulture">The converter culture to use.</param>
         /// <param name="converterParameter">
         /// A parameter to pass to <paramref name="converter"/>.
         /// </param>
@@ -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.
         /// </param>
         /// <param name="converter">The value converter to use.</param>
+        /// <param name="converterCulture">The converter culture to use.</param>
         /// <param name="converterParameter">
         /// A parameter to pass to <paramref name="converter"/>.
         /// </param>
@@ -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
         /// </summary>
         public IValueConverter Converter { get; }
 
+        /// <summary>
+        /// Gets or sets the culture in which to evaluate the converter.
+        /// </summary>
+        public CultureInfo ConverterCulture { get; set; }
+
         /// <summary>
         /// Gets a parameter to pass to <see cref="Converter"/>.
         /// </summary>
@@ -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);

+ 1 - 0
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,

+ 6 - 0
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; }

+ 13 - 0
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
         /// </summary>
         public IValueConverter? Converter { get; set; }
 
+        /// <summary>
+        /// Gets or sets the culture in which to evaluate the converter.
+        /// </summary>
+        /// <value>The default value is null.</value>
+        /// <remarks>
+        /// If this property is not set then <see cref="CultureInfo.CurrentCulture"/> will be used.
+        /// </remarks>
+        [TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
+        public CultureInfo? ConverterCulture { get; set; }
+
         /// <summary>
         /// Gets or sets a parameter to pass to <see cref="Converter"/>.
         /// </summary>
@@ -120,6 +132,7 @@ namespace Avalonia.Data
                 fallback,
                 TargetNullValue,
                 converter ?? DefaultValueConverter.Instance,
+                ConverterCulture ?? CultureInfo.CurrentCulture,
                 ConverterParameter,
                 Priority);
 

+ 22 - 0
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);
+    }
+}

+ 14 - 6
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);

+ 56 - 0
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<IValueConverter>();
+            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<IValueConverter>();
+            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";

+ 24 - 2
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 = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
+        x:DataType='local:TestDataContext' x:CompileBindings='True'>
+    <TextBlock Name='textBlock' Text='{Binding StringProperty, Converter={x:Static local:AppendConverter.Instance}, ConverterCulture=ar-SA}'/>
+</Window>";
+
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var textBlock = window.FindControl<TextBlock>("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();

+ 40 - 1
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 = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+  <TextBlock Name='textBlock' Text='{Binding Greeting1, Converter={x:Static local:BindingTests+CultureAppender.Instance}, ConverterCulture=ar-SA}'/>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var textBlock = Assert.IsType<TextBlock>(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()
         {