diff --git a/src/ReactiveUI.Blazor/Registrations.cs b/src/ReactiveUI.Blazor/Registrations.cs index b6dec1a6e8..4d93187441 100644 --- a/src/ReactiveUI.Blazor/Registrations.cs +++ b/src/ReactiveUI.Blazor/Registrations.cs @@ -23,6 +23,10 @@ public void Register(Action, Type> registerFunction) throw new ArgumentNullException(nameof(registerFunction)); } + registerFunction(() => new StringConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new PlatformOperations(), typeof(IPlatformOperations)); if (Type.GetType("Mono.Runtime") is not null) diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net472.approved.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net472.approved.txt index 5f77e2c749..f0b4c73aff 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net472.approved.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net472.approved.txt @@ -766,6 +766,12 @@ namespace ReactiveUI { public SingleInstanceViewAttribute() { } } + public class SingleToStringTypeConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger + { + public SingleToStringTypeConverter() { } + public int GetAffinityForObjects(System.Type fromType, System.Type toType) { } + public bool TryConvert(object? from, System.Type toType, object? conversionHint, out object result) { } + } public class StringConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger { public StringConverter() { } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net5.0.approved.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net5.0.approved.txt index 629b667142..92bf24096d 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net5.0.approved.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.net5.0.approved.txt @@ -761,6 +761,12 @@ namespace ReactiveUI { public SingleInstanceViewAttribute() { } } + public class SingleToStringTypeConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger + { + public SingleToStringTypeConverter() { } + public int GetAffinityForObjects(System.Type fromType, System.Type toType) { } + public bool TryConvert(object? from, System.Type toType, object? conversionHint, out object result) { } + } public class StringConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger { public StringConverter() { } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.netcoreapp3.1.approved.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.netcoreapp3.1.approved.txt index 2df92e6f7b..0e0bf80eac 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.netcoreapp3.1.approved.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.netcoreapp3.1.approved.txt @@ -759,6 +759,12 @@ namespace ReactiveUI { public SingleInstanceViewAttribute() { } } + public class SingleToStringTypeConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger + { + public SingleToStringTypeConverter() { } + public int GetAffinityForObjects(System.Type fromType, System.Type toType) { } + public bool TryConvert(object? from, System.Type toType, object? conversionHint, out object result) { } + } public class StringConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger { public StringConverter() { } diff --git a/src/ReactiveUI.Tests/Mocks/PropertyBindViewModel.cs b/src/ReactiveUI.Tests/Mocks/PropertyBindViewModel.cs index 08432ae329..17f608f1e9 100644 --- a/src/ReactiveUI.Tests/Mocks/PropertyBindViewModel.cs +++ b/src/ReactiveUI.Tests/Mocks/PropertyBindViewModel.cs @@ -17,6 +17,7 @@ public class PropertyBindViewModel : ReactiveObject private string? _property1; private PropertyBindModel? _model; private int _property2; + private float _justASingle; private double _justADouble; private decimal _justADecimal; private double? _nullableDouble; @@ -100,12 +101,30 @@ public double? NullableDouble set => this.RaiseAndSetIfChanged(ref _nullableDouble, value); } + /// + /// Gets or sets the just a visibility. + /// + /// + /// The just a visibility. + /// public Visibility JustAVisibility { get => _justAVisibility; set => this.RaiseAndSetIfChanged(ref _justAVisibility, value); } + /// + /// Gets or sets the just a single. + /// + /// + /// The just a single. + /// + public float JustASingle + { + get => _justASingle; + set => this.RaiseAndSetIfChanged(ref _justASingle, value); + } + /// /// Gets some collection of strings. /// diff --git a/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs b/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs index e75342899d..1a7e7fc152 100644 --- a/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs +++ b/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs @@ -695,7 +695,7 @@ public void BindWithFuncToTriggerUpdateTestViewModelToView() } [Fact] - public void BindWithFuncToTriggerUpdateTestViewModelToViewWithConverter() + public void BindWithFuncToTriggerUpdateTestViewModelToViewWithDecimalConverter() { CompositeDisposable dis = new(); PropertyBindViewModel? vm = new(); @@ -787,5 +787,101 @@ public void BindWithFuncToTriggerUpdateTestViewToViewModel() dis.Dispose(); Assert.True(dis.IsDisposed); } + + [Fact] + public void BindWithFuncToTriggerUpdateTestViewModelToViewWithDoubleConverter() + { + CompositeDisposable dis = new(); + PropertyBindViewModel? vm = new(); + var view = new PropertyBindView { ViewModel = vm }; + var update = new Subject(); + + vm.JustADouble = 123.45; + Assert.NotEqual(vm.JustADouble.ToString(CultureInfo.InvariantCulture), view.SomeTextBox.Text); + + var doubleToStringTypeConverter = new DoubleToStringTypeConverter(); + + view.Bind(vm, x => x.JustADouble, x => x.SomeTextBox.Text, update.AsObservable(), 2, doubleToStringTypeConverter, doubleToStringTypeConverter, TriggerUpdate.ViewModelToView).DisposeWith(dis); + + vm.JustADouble = 1.0; + + // value should have pre bind value + Assert.Equal(view.SomeTextBox.Text, "123.45"); + + // trigger UI update + update.OnNext(true); + Assert.Equal(view.SomeTextBox.Text, "1.00"); + + vm.JustADouble = 2.0; + Assert.Equal(view.SomeTextBox.Text, "1.00"); + + update.OnNext(true); + Assert.Equal(view.SomeTextBox.Text, "2.00"); + + // test reverse bind no trigger required + view.SomeTextBox.Text = "3.00"; + Assert.Equal(vm.JustADouble, 3.0); + + view.SomeTextBox.Text = "4.00"; + Assert.Equal(vm.JustADouble, 4.0); + + // test forward bind to ensure trigger is still honoured. + vm.JustADouble = 2.0; + Assert.Equal(view.SomeTextBox.Text, "4.00"); + + update.OnNext(true); + Assert.Equal(view.SomeTextBox.Text, "2.00"); + + dis.Dispose(); + Assert.True(dis.IsDisposed); + } + + [Fact] + public void BindWithFuncToTriggerUpdateTestViewModelToViewWithSingleConverter() + { + CompositeDisposable dis = new(); + PropertyBindViewModel? vm = new(); + var view = new PropertyBindView { ViewModel = vm }; + var update = new Subject(); + + vm.JustASingle = 123.45f; + Assert.NotEqual(vm.JustASingle.ToString(CultureInfo.InvariantCulture), view.SomeTextBox.Text); + + var singleToStringTypeConverter = new SingleToStringTypeConverter(); + + view.Bind(vm, x => x.JustASingle, x => x.SomeTextBox.Text, update.AsObservable(), 2, singleToStringTypeConverter, singleToStringTypeConverter, TriggerUpdate.ViewModelToView).DisposeWith(dis); + + vm.JustASingle = 1.0f; + + // value should have pre bind value + Assert.Equal(view.SomeTextBox.Text, "123.45"); + + // trigger UI update + update.OnNext(true); + Assert.Equal(view.SomeTextBox.Text, "1.00"); + + vm.JustASingle = 2.0f; + Assert.Equal(view.SomeTextBox.Text, "1.00"); + + update.OnNext(true); + Assert.Equal(view.SomeTextBox.Text, "2.00"); + + // test reverse bind no trigger required + view.SomeTextBox.Text = "3.00"; + Assert.Equal(vm.JustASingle, 3.0f); + + view.SomeTextBox.Text = "4.00"; + Assert.Equal(vm.JustASingle, 4.0f); + + // test forward bind to ensure trigger is still honoured. + vm.JustASingle = 2.0f; + Assert.Equal(view.SomeTextBox.Text, "4.00"); + + update.OnNext(true); + Assert.Equal(view.SomeTextBox.Text, "2.00"); + + dis.Dispose(); + Assert.True(dis.IsDisposed); + } } } diff --git a/src/ReactiveUI.Uno/Registrations.cs b/src/ReactiveUI.Uno/Registrations.cs index df8dc694d4..e371eab331 100644 --- a/src/ReactiveUI.Uno/Registrations.cs +++ b/src/ReactiveUI.Uno/Registrations.cs @@ -26,6 +26,10 @@ public void Register(Action, Type> registerFunction) registerFunction(() => new PlatformOperations(), typeof(IPlatformOperations)); registerFunction(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); registerFunction(() => new DependencyObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); + registerFunction(() => new StringConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); diff --git a/src/ReactiveUI.Winforms/Registrations.cs b/src/ReactiveUI.Winforms/Registrations.cs index 934e5636ce..b4287324c5 100644 --- a/src/ReactiveUI.Winforms/Registrations.cs +++ b/src/ReactiveUI.Winforms/Registrations.cs @@ -32,6 +32,10 @@ public void Register(Action, Type> registerFunction) registerFunction(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); registerFunction(() => new PanelSetMethodBindingConverter(), typeof(ISetMethodBindingConverter)); registerFunction(() => new TableContentSetMethodBindingConverter(), typeof(ISetMethodBindingConverter)); + registerFunction(() => new StringConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); if (!ModeDetector.InUnitTestRunner()) diff --git a/src/ReactiveUI.Wpf/Registrations.cs b/src/ReactiveUI.Wpf/Registrations.cs index d15a223c2d..f0f0f031f1 100644 --- a/src/ReactiveUI.Wpf/Registrations.cs +++ b/src/ReactiveUI.Wpf/Registrations.cs @@ -26,6 +26,10 @@ public void Register(Action, Type> registerFunction) registerFunction(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); registerFunction(() => new DependencyObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); + registerFunction(() => new StringConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); registerFunction(() => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); diff --git a/src/ReactiveUI.XamForms/Registrations.cs b/src/ReactiveUI.XamForms/Registrations.cs index c10b3d0867..ab86a8d77d 100644 --- a/src/ReactiveUI.XamForms/Registrations.cs +++ b/src/ReactiveUI.XamForms/Registrations.cs @@ -26,6 +26,10 @@ public void Register(Action, Type> registerFunction) throw new ArgumentNullException(nameof(registerFunction)); } + registerFunction(() => new StringConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); } } diff --git a/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs index c4176add39..ce671fd052 100644 --- a/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs @@ -46,18 +46,19 @@ public bool TryConvert(object? from, Type toType, object? conversionHint, out ob if (from is string fromString) { - var outDecimal = decimal.Zero; - decimal.TryParse(fromString, out outDecimal); - - if (conversionHint is int decimalHint) + var success = decimal.TryParse(fromString, out var outDecimal); + if (success) { - result = Math.Round(outDecimal, decimalHint); - return true; - } + if (conversionHint is int decimalHint) + { + result = Math.Round(outDecimal, decimalHint); + return true; + } - result = outDecimal; + result = outDecimal; - return true; + return true; + } } result = null!; diff --git a/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs index 917af9b800..69470c9537 100644 --- a/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs @@ -46,18 +46,19 @@ public bool TryConvert(object? from, Type toType, object? conversionHint, out ob if (from is string fromString) { - var outDouble = double.NaN; - double.TryParse(fromString, out outDouble); - - if (conversionHint is int doubleHint) + var success = double.TryParse(fromString, out var outDouble); + if (success) { - result = Math.Round(outDouble, doubleHint); - return true; - } + if (conversionHint is int doubleHint) + { + result = Math.Round(outDouble, doubleHint); + return true; + } - result = outDouble; + result = outDouble; - return true; + return true; + } } result = null!; diff --git a/src/ReactiveUI/Bindings/Converter/SingleToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/SingleToStringTypeConverter.cs new file mode 100644 index 0000000000..350919cd01 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/SingleToStringTypeConverter.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; + +namespace ReactiveUI +{ + /// + /// Single To String Type Converter. + /// + /// + public class SingleToStringTypeConverter : IBindingTypeConverter + { + /// + public int GetAffinityForObjects(Type fromType, Type toType) + { + if (fromType == typeof(float) && toType == typeof(string)) + { + return 10; + } + + if (fromType == typeof(string) && toType == typeof(float)) + { + return 10; + } + + return 0; + } + + /// + public bool TryConvert(object? from, Type toType, object? conversionHint, out object result) + { + if (toType == typeof(string) && from is float fromSingle) + { + if (conversionHint is int singleHint) + { + result = fromSingle.ToString($"F{singleHint}"); + return true; + } + + result = fromSingle.ToString(); + return true; + } + + if (from is string fromString) + { + var success = float.TryParse(fromString, out var outSingle); + if (success) + { + if (conversionHint is int singleHint) + { + result = Convert.ToSingle(Math.Round(outSingle, singleHint)); + return true; + } + + result = outSingle; + + return true; + } + } + + result = null!; + return false; + } + } +} diff --git a/src/ReactiveUI/Platforms/uap/PlatformRegistrations.cs b/src/ReactiveUI/Platforms/uap/PlatformRegistrations.cs index 6de8aaaebd..b4ced61044 100644 --- a/src/ReactiveUI/Platforms/uap/PlatformRegistrations.cs +++ b/src/ReactiveUI/Platforms/uap/PlatformRegistrations.cs @@ -27,8 +27,6 @@ public void Register(Action, Type> registerFunction) registerFunction(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); registerFunction(() => new DependencyObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); registerFunction(() => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(() => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(() => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); if (!ModeDetector.InUnitTestRunner()) diff --git a/src/ReactiveUI/Registration/Registrations.cs b/src/ReactiveUI/Registration/Registrations.cs index 9c78896857..d4d4a6282a 100644 --- a/src/ReactiveUI/Registration/Registrations.cs +++ b/src/ReactiveUI/Registration/Registrations.cs @@ -30,6 +30,9 @@ public void Register(Action, Type> registerFunction) registerFunction(() => new POCOObservableForProperty(), typeof(ICreatesObservableForProperty)); registerFunction(() => new EqualityTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new StringConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); + registerFunction(() => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); registerFunction(() => new DefaultViewLocator(), typeof(IViewLocator)); registerFunction(() => new CanActivateViewFetcher(), typeof(IActivationForViewFetcher)); registerFunction(() => new CreatesCommandBindingViaEvent(), typeof(ICreatesCommandBinding));