Browse Source

Added ConverterCulture to bindings. (#12876)

Similar to WPF's `Binding.ConverterCulture` property.
pull/13006/head
Steven Kirk 2 years ago
committed by GitHub
parent
commit
8948aa7e52
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  2. 1
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  3. 6
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs
  4. 13
      src/Markup/Avalonia.Markup/Data/BindingBase.cs
  5. 22
      src/Markup/Avalonia.Markup/Data/CultureInfoIetfLanguageTagConverter.cs
  6. 20
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  7. 56
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs
  8. 26
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  9. 41
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

29
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
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
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
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
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);
}
}

20
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
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";

26
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();

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

Loading…
Cancel
Save