From a13a484fdd2447d1cdfeeead30b84e6ece13a434 Mon Sep 17 00:00:00 2001 From: JonKAS Date: Fri, 14 Jan 2022 10:33:05 +0100 Subject: [PATCH] Fix input for decimal/float/double and nullable (#11815) * Fix input for decimal/float/double and nullable This commit fixes the input of decimal/float/double and their nullable equivalents in different cultures. Issue #7996 * Update UI test Makes the UI test more understandable. It shows now a label with the actual resolved binding value. In the entry you can now see the value you provided. * Fix unit test * Fix more unit tests Co-authored-by: Gerald Versluis Co-authored-by: Gerald Versluis --- .../Github7996.cs | 111 ++++++++++++++++++ ...rin.Forms.Controls.Issues.Shared.projitems | 1 + .../BindingExpressionTests.cs | 48 ++++++++ .../BindingUnitTests.cs | 13 +- .../TypedBindingUnitTests.cs | 13 +- Xamarin.Forms.Core/BindingExpression.cs | 10 +- 6 files changed, 179 insertions(+), 17 deletions(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github7996.cs diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github7996.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github7996.cs new file mode 100644 index 00000000000..97dd0732689 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Github7996.cs @@ -0,0 +1,111 @@ +using System.ComponentModel; +using System.Globalization; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +#if UITEST +using Xamarin.Forms.Core.UITests; +using Xamarin.UITest; +using NUnit.Framework; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Github, 7996, "Xamarin.Forms.Entry does not enter decimal when binding a float/double and decimal to it", PlatformAffected.Default)] + public class Issue7996 : TestContentPage + { + protected override void Init() + { + var vm = new ViewModelIssue7996(); + BindingContext = vm; + var textLabel1 = new Label + { + Text = "Select culture:" + }; + var picker = new Picker(); + picker.ItemsSource = new[] + { + new CultureInfo("de"), + new CultureInfo("en"), + }; + picker.SelectedIndexChanged += (s, e) => + { + if (picker.SelectedItem is CultureInfo culture) + { + CultureInfo.CurrentUICulture = culture; + } + }; + + var textLabel2 = new Label + { + Text = "Resolved Binding Value:" + }; + var bindingLable = new Label(); + bindingLable.SetBinding(Label.TextProperty, new Binding(nameof(ViewModelIssue7996.MyDecimal), BindingMode.TwoWay)); + + var textLabel3 = new Label + { + Text = "Actual Value:" + }; + var bindingLable2 = new Label(); + bindingLable2.SetBinding(Label.TextProperty, new Binding(nameof(ViewModelIssue7996.MyDecimal), BindingMode.TwoWay)); + + var textLabel4 = new Label + { + Text = "Enter a number and watch result:" + }; + var entry = new Entry(); + entry.Text = vm.MyDecimal.ToString(); + entry.TextChanged += (s, e) => + { + bindingLable.Text = e.NewTextValue; + }; + + var stackLayout = new StackLayout + { + Children = + { + textLabel1, + picker, + textLabel2, + bindingLable, + textLabel3, + bindingLable2, + textLabel4, + entry, + } + }; + + Content = stackLayout; + } + } + + [Preserve(AllMembers = true)] + public class ViewModelIssue7996 : INotifyPropertyChanged + { + + decimal? myDecimal = 4.2m; + + public event PropertyChangedEventHandler PropertyChanged; + + public decimal? MyDecimal + { + get + { + return myDecimal; + } + set + { + myDecimal = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyDecimal))); + } + } + + public ViewModelIssue7996() + { + + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index ced26a05eba..ad21e27748e 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -973,6 +973,7 @@ Code + _TemplateMarkup.xaml diff --git a/Xamarin.Forms.Core.UnitTests/BindingExpressionTests.cs b/Xamarin.Forms.Core.UnitTests/BindingExpressionTests.cs index 9d38b99fb07..290b7222d89 100644 --- a/Xamarin.Forms.Core.UnitTests/BindingExpressionTests.cs +++ b/Xamarin.Forms.Core.UnitTests/BindingExpressionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -80,5 +81,52 @@ public void ValidPaths( var binding = new Binding(path); Assert.DoesNotThrow(() => new BindingExpression(binding, path)); } + + static object[] TryConvertWithNumbersAndCulturesCases => new object[] + { + new object[]{ "4.2", new CultureInfo("en"), 4.2m }, + new object[]{ "4,2", new CultureInfo("de"), 4.2m }, + new object[]{ "-4.2", new CultureInfo("en"), -4.2m }, + new object[]{ "-4,2", new CultureInfo("de"), -4.2m }, + + new object[]{ "4.2", new CultureInfo("en"), new decimal?(4.2m)}, + new object[]{ "4,2", new CultureInfo("de"), new decimal?(4.2m) }, + new object[]{ "-4.2", new CultureInfo("en"), new decimal?(-4.2m)}, + new object[]{ "-4,2", new CultureInfo("de"), new decimal?(-4.2m) }, + + new object[]{ "4.2", new CultureInfo("en"), 4.2d }, + new object[]{ "4,2", new CultureInfo("de"), 4.2d }, + new object[]{ "-4.2", new CultureInfo("en"), -4.2d }, + new object[]{ "-4,2", new CultureInfo("de"), -4.2d }, + + new object[]{ "4.2", new CultureInfo("en"), new double?(4.2d)}, + new object[]{ "4,2", new CultureInfo("de"), new double?(4.2d) }, + new object[]{ "-4.2", new CultureInfo("en"), new double?(-4.2d)}, + new object[]{ "-4,2", new CultureInfo("de"), new double?(-4.2d) }, + + new object[]{ "4.2", new CultureInfo("en"), 4.2f }, + new object[]{ "4,2", new CultureInfo("de"), 4.2f }, + new object[]{ "-4.2", new CultureInfo("en"), -4.2f }, + new object[]{ "-4,2", new CultureInfo("de"), -4.2f }, + + new object[]{ "4.2", new CultureInfo("en"), new float?(4.2f)}, + new object[]{ "4,2", new CultureInfo("de"), new float?(4.2f) }, + new object[]{ "-4.2", new CultureInfo("en"), new float?(-4.2f)}, + new object[]{ "-4,2", new CultureInfo("de"), new float?(-4.2f) }, + + new object[]{ "4.", new CultureInfo("en"), "4." }, + new object[]{ "4,", new CultureInfo("de"), "4," }, + new object[]{ "-0", new CultureInfo("en"), "-0" }, + new object[]{ "-0", new CultureInfo("de"), "-0" }, + }; + + [TestCaseSource(nameof(TryConvertWithNumbersAndCulturesCases))] + public void TryConvertWithNumbersAndCultures(object inputString, CultureInfo culture, object expected) + { + CultureInfo.CurrentUICulture = culture; + BindingExpression.TryConvert(ref inputString, Entry.TextProperty, expected.GetType(), false); + + Assert.AreEqual(expected, inputString); + } } } diff --git a/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs b/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs index 143ab416e7d..fec90d7588f 100644 --- a/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs +++ b/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs @@ -1998,21 +1998,22 @@ public void Convert() } #if !WINDOWS_PHONE - [TestCase("en-US"), TestCase("pt-PT")] - public void ConvertIsCultureInvariant(string culture) + [TestCase("en-US", "0.5", 0.5, 0.9, "0.9")] + [TestCase("pt-PT", "0,5", 0.5, 0.9, "0,9")] + public void ConvertIsCultureInvariant(string culture, string sliderSetStringValue, double sliderExpectedDoubleValue, double sliderSetDoubleValue, string sliderExpectedStringValue) { System.Threading.Thread.CurrentThread.CurrentCulture = System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); var slider = new Slider(); - var vm = new MockViewModel { Text = "0.5" }; + var vm = new MockViewModel { Text = sliderSetStringValue }; slider.BindingContext = vm; slider.SetBinding(Slider.ValueProperty, "Text", BindingMode.TwoWay); - Assert.That(slider.Value, Is.EqualTo(0.5)); + Assert.That(slider.Value, Is.EqualTo(sliderExpectedDoubleValue)); - slider.Value = 0.9; + slider.Value = sliderSetDoubleValue; - Assert.That(vm.Text, Is.EqualTo("0.9")); + Assert.That(vm.Text, Is.EqualTo(sliderExpectedStringValue)); } #endif diff --git a/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs b/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs index c9d23a63b55..79f69e4177b 100644 --- a/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs +++ b/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs @@ -1270,21 +1270,22 @@ public void Convert() } #if !WINDOWS_PHONE - [TestCase("en-US"), TestCase("pt-PT")] - public void ConvertIsCultureInvariant(string culture) + [TestCase("en-US", "0.5", 0.5, 0.9, "0.9")] + [TestCase("pt-PT", "0,5", 0.5, 0.9, "0,9")] + public void ConvertIsCultureInvariant(string culture, string sliderSetStringValue, double sliderExpectedDoubleValue, double sliderSetDoubleValue, string sliderExpectedStringValue) { System.Threading.Thread.CurrentThread.CurrentCulture = System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); var slider = new Slider(); - var vm = new MockViewModel { Text = "0.5" }; + var vm = new MockViewModel { Text = sliderSetStringValue }; slider.BindingContext = vm; slider.SetBinding(Slider.ValueProperty, new TypedBinding(mvm => mvm.Text, (mvm, s) => mvm.Text = s, null) { Mode = BindingMode.TwoWay }); - Assert.That(slider.Value, Is.EqualTo(0.5)); + Assert.That(slider.Value, Is.EqualTo(sliderExpectedDoubleValue)); - slider.Value = 0.9; + slider.Value = sliderSetDoubleValue; - Assert.That(vm.Text, Is.EqualTo("0.9")); + Assert.That(vm.Text, Is.EqualTo(sliderExpectedStringValue)); } #endif diff --git a/Xamarin.Forms.Core/BindingExpression.cs b/Xamarin.Forms.Core/BindingExpression.cs index 10a2aa13f01..c9b527cfbcf 100644 --- a/Xamarin.Forms.Core/BindingExpression.cs +++ b/Xamarin.Forms.Core/BindingExpression.cs @@ -455,13 +455,14 @@ internal static bool TryConvert(ref object value, BindableProperty targetPropert } object original = value; - try + try { + convertTo = Nullable.GetUnderlyingType(convertTo) ?? convertTo; + var stringValue = value as string ?? string.Empty; // see: https://bugzilla.xamarin.com/show_bug.cgi?id=32871 // do not canonicalize "*.[.]"; "1." should not update bound BindableProperty - if (stringValue.EndsWith(".", StringComparison.Ordinal) && DecimalTypes.Contains(convertTo)) - { + if (stringValue.EndsWith(CultureInfo.CurrentUICulture.NumberFormat.NumberDecimalSeparator, StringComparison.Ordinal) && DecimalTypes.Contains(convertTo)) { value = original; return false; } @@ -473,9 +474,8 @@ internal static bool TryConvert(ref object value, BindableProperty targetPropert return false; } - convertTo = Nullable.GetUnderlyingType(convertTo) ?? convertTo; + value = Convert.ChangeType(value, convertTo, CultureInfo.CurrentUICulture); - value = Convert.ChangeType(value, convertTo, CultureInfo.InvariantCulture); return true; } catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is InvalidOperationException || ex is OverflowException)