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()
{