From 62b8c5a40c0812c946f9b7ce1462f877088a2a9f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Oct 2015 00:56:28 +0200 Subject: [PATCH] Added default type converter for bindings. To convert between basic types. Closes #256. Closes #270. --- samples/BindingTest/MainWindow.paml | 41 +++++++----- .../ViewModels/MainWindowViewModel.cs | 7 ++ .../Binding/XamlBinding.cs | 4 +- .../Binding/ExpressionObserver.cs | 34 ++++++++++ .../Binding/ExpressionSubject.cs | 66 +++++++++++++++++-- .../Binding/PropertyAccessorNode.cs | 2 + .../Perspex.Markup/DefaultValueConverter.cs | 56 ++++++++++++++++ src/Markup/Perspex.Markup/IValueConverter.cs | 34 ++++++++++ .../Perspex.Markup/Perspex.Markup.csproj | 2 + src/Perspex.Base/PerspexObject.cs | 3 +- src/Perspex.Base/Utilities/TypeUtilities.cs | 36 ++++++++-- .../PerspexObjectTests_Direct.cs | 13 ---- .../ExpressionObserverTests_Property.cs | 27 ++++++++ .../DefaultValueConverterTests.cs | 47 +++++++++++++ .../Perspex.Markup.UnitTests.csproj | 1 + 15 files changed, 330 insertions(+), 43 deletions(-) create mode 100644 src/Markup/Perspex.Markup/DefaultValueConverter.cs create mode 100644 src/Markup/Perspex.Markup/IValueConverter.cs create mode 100644 tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml index 739651b21e..bb60094494 100644 --- a/samples/BindingTest/MainWindow.paml +++ b/samples/BindingTest/MainWindow.paml @@ -1,22 +1,29 @@  - - - - - - - + + + + + + + + + + + + + + + + + + !BooleanString + !!BooleanString + - - - - - - - - - !BooleanString - !!BooleanString + + + + \ No newline at end of file diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index 8722568aee..02e8d9d65d 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -7,6 +7,7 @@ namespace BindingTest.ViewModels public class MainWindowViewModel : ReactiveObject { private string _booleanString = "True"; + private double _doubleValue = 5.0; private string _stringValue = "Simple Binding"; public MainWindowViewModel() @@ -35,6 +36,12 @@ namespace BindingTest.ViewModels set { this.RaiseAndSetIfChanged(ref _booleanString, value); } } + public double DoubleValue + { + get { return _doubleValue; } + set { this.RaiseAndSetIfChanged(ref _doubleValue, value); } + } + public string StringValue { get { return _stringValue; } diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs index cb2c999f7a..a77b4b4c2a 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -29,7 +29,9 @@ namespace Perspex.Markup.Xaml.Binding public void Bind(IObservablePropertyBag instance, PerspexProperty property) { - var subject = new ExpressionSubject(CreateExpressionObserver(instance, property)); + var subject = new ExpressionSubject( + CreateExpressionObserver(instance, property), + property.PropertyType); if (subject != null) { diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index 2e76d58e24..e3141c5d4c 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -79,9 +79,43 @@ namespace Perspex.Markup.Binding } } + /// + /// Gets the type of the expression result or null if the expression could not be + /// evaluated. + /// + public Type ResultType + { + get + { + IncrementCount(); + + try + { + return (Leaf as PropertyAccessorNode)?.PropertyType; + } + finally + { + DecrementCount(); + } + } + } + /// string IDescription.Description => Expression; + /// + /// Gets the leaf node. + /// + private ExpressionNode Leaf + { + get + { + var node = _node; + while (node.Next != null) node = node.Next; + return node; + } + } + /// protected override IDisposable SubscribeCore(IObserver observer) { diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs index b8dd316b36..7cddb93cca 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs @@ -2,25 +2,43 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Globalization; using System.Reactive.Linq; using System.Reactive.Subjects; namespace Perspex.Markup.Binding { /// - /// Turns an into a subject that can be bound two-ways. + /// Turns an into a subject that can be bound two-way with + /// a value converter. /// public class ExpressionSubject : ISubject, IDescription { + private IValueConverter _converter; private ExpressionObserver _inner; + private Type _targetType; /// /// Initializes a new instance of the class. /// /// The . - public ExpressionSubject(ExpressionObserver inner) + /// The type to convert the value to. + public ExpressionSubject(ExpressionObserver inner, Type targetType) + : this(inner, targetType, DefaultValueConverter.Instance) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. + /// The value converter to use. + public ExpressionSubject(ExpressionObserver inner, Type targetType, IValueConverter converter) + { + _converter = converter; _inner = inner; + _targetType = targetType; } /// @@ -39,13 +57,53 @@ namespace Perspex.Markup.Binding /// public void OnNext(object value) { - _inner.SetValue(value); + var type = _inner.ResultType; + + if (type != null) + { + object converted; + + if (ConvertBack(value, type, out converted)) + { + _inner.SetValue(converted); + } + } } /// public IDisposable Subscribe(IObserver observer) { - return _inner.Subscribe(observer); + return _inner + .Select(x => Convert(x, _targetType)) + .Subscribe(observer); + } + + private object Convert(object value, Type type) + { + try + { + return _converter.Convert(value, type, null, CultureInfo.CurrentUICulture); + } + catch + { + // TODO: Log something. + return PerspexProperty.UnsetValue; + } + } + + private bool ConvertBack(object value, Type type, out object result) + { + try + { + result = _converter.ConvertBack(value, type, null, CultureInfo.CurrentUICulture); + return true; + } + catch + { + // TODO: Log something. + result = null; + return false; + } } } } diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index b4efbc324c..f5c6c293a2 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -23,6 +23,8 @@ namespace Perspex.Markup.Binding public string PropertyName { get; } + public Type PropertyType => _propertyInfo?.PropertyType; + public override bool SetValue(object value) { if (Next != null) diff --git a/src/Markup/Perspex.Markup/DefaultValueConverter.cs b/src/Markup/Perspex.Markup/DefaultValueConverter.cs new file mode 100644 index 0000000000..cc24753ae6 --- /dev/null +++ b/src/Markup/Perspex.Markup/DefaultValueConverter.cs @@ -0,0 +1,56 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using Perspex.Utilities; + +namespace Perspex.Markup +{ + /// + /// Provides a default set of value conversions for bindings that do not specify a value + /// converter. + /// + public class DefaultValueConverter : IValueConverter + { + /// + /// Gets an instance of a . + /// + public static readonly DefaultValueConverter Instance = new DefaultValueConverter(); + + /// + /// Converts a value. + /// + /// The value to convert. + /// The type of the target. + /// A user-defined parameter. + /// The culture to use. + /// The converted value. + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + object result; + + if (TypeUtilities.TryConvert(targetType, value, culture, out result)) + { + return result; + } + else + { + throw new InvalidCastException($"Cannot convert value from {value.GetType()} to {targetType}"); + } + } + + /// + /// Converts a value. + /// + /// The value to convert. + /// The type of the target. + /// A user-defined parameter. + /// The culture to use. + /// The converted value. + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return Convert(value, targetType, parameter, culture); + } + } +} diff --git a/src/Markup/Perspex.Markup/IValueConverter.cs b/src/Markup/Perspex.Markup/IValueConverter.cs new file mode 100644 index 0000000000..5d8a2e1962 --- /dev/null +++ b/src/Markup/Perspex.Markup/IValueConverter.cs @@ -0,0 +1,34 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; + +namespace Perspex.Markup +{ + /// + /// Converts a binding value. + /// + public interface IValueConverter + { + /// + /// Converts a value. + /// + /// The value to convert. + /// The type of the target. + /// A user-defined parameter. + /// The culture to use. + /// The converted value. + object Convert(object value, Type targetType, object parameter, CultureInfo culture); + + /// + /// Converts a value. + /// + /// The value to convert. + /// The type of the target. + /// A user-defined parameter. + /// The culture to use. + /// The converted value. + object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture); + } +} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index e16d0f1deb..2e812d1bc3 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -49,6 +49,8 @@ + + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 27476c881d..6cb0c39bab 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -596,6 +596,7 @@ namespace Perspex { Contract.Requires(property != null); VerifyAccess(); + if (property.IsDirect) { property = GetRegistered(property); @@ -611,7 +612,7 @@ namespace Perspex GetDescription(source)); return source - .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType, false)) + .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType)) .Subscribe(x => SetValue(property, x)); } else diff --git a/src/Perspex.Base/Utilities/TypeUtilities.cs b/src/Perspex.Base/Utilities/TypeUtilities.cs index d026efc622..f1eddf5b94 100644 --- a/src/Perspex.Base/Utilities/TypeUtilities.cs +++ b/src/Perspex.Base/Utilities/TypeUtilities.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; @@ -11,7 +12,7 @@ namespace Perspex.Utilities /// /// Provides utilities for working with types at runtime. /// - internal static class TypeUtilities + public static class TypeUtilities { private static readonly Dictionary> Conversions = new Dictionary>() { @@ -32,9 +33,8 @@ namespace Perspex.Utilities /// The type to cast to. /// The value to cast. /// If sucessful, contains the cast value. - /// Allow . /// True if the cast was sucessful, otherwise false. - public static bool TryCast(Type to, object value, out object result, bool allowUnset = true) + public static bool TryCast(Type to, object value, out object result) { Contract.Requires(to != null); @@ -47,7 +47,7 @@ namespace Perspex.Utilities var from = value.GetType(); - if (allowUnset && value == PerspexProperty.UnsetValue) + if (value == PerspexProperty.UnsetValue) { result = value; return true; @@ -79,20 +79,42 @@ namespace Perspex.Utilities return false; } + /// + /// Try to convert a value to a type, using if possible, + /// otherwise using . + /// + /// The type to cast to. + /// The value to cast. + /// The culture to use. + /// If sucessful, contains the cast value. + /// True if the cast was sucessful, otherwise false. + public static bool TryConvert(Type to, object value, CultureInfo culture, out object result) + { + if ((value.GetType() == typeof(string) && Conversions.ContainsKey(to)) || + (to == typeof(string) && Conversions.ContainsKey(value.GetType()))) + { + result = Convert.ChangeType(value, to, culture); + return true; + } + else + { + return TryCast(to, value, out result); + } + } + /// /// Casts a value to a type, returning the default for that type if the value could not be /// cast. /// /// The value to cast. /// The type to cast to.. - /// Allow . /// A value of . - public static object CastOrDefault(object value, Type type, bool allowUnset = true) + public static object CastOrDefault(object value, Type type) { var typeInfo = type.GetTypeInfo(); object result; - if (TypeUtilities.TryCast(type, value, out result, allowUnset)) + if (TypeUtilities.TryCast(type, value, out result)) { return result; } diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index a9a2d5c1fc..ea1fecb513 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -174,19 +174,6 @@ namespace Perspex.Base.UnitTests Assert.Equal(0, target.Baz); } - [Fact] - public void Bind_Handles_UnsetValue() - { - var target = new Class1(); - var source = new Subject(); - - var sub = target.Bind(Class1.BazProperty, source); - - source.OnNext(PerspexProperty.UnsetValue); - - Assert.Equal(0, target.Baz); - } - [Fact] public void ReadOnly_Property_Cannot_Be_Set() { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs index a782578505..2e1608a4b6 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs @@ -21,6 +21,15 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal("foo", result); } + [Fact] + public void Should_Get_Simple_Property_Value_Type() + { + var data = new { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + + Assert.Equal(typeof(string), target.ResultType); + } + [Fact] public async void Should_Get_Simple_Property_From_Base_Class() { @@ -41,6 +50,15 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal("baz", result); } + [Fact] + public void Should_Get_Simple_Property_Chain_Type() + { + var data = new { Foo = new { Bar = new { Baz = "baz" } } }; + var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + + Assert.Equal(typeof(string), target.ResultType); + } + [Fact] public async void Should_Not_Have_Value_For_Broken_Chain() { @@ -51,6 +69,15 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal(PerspexProperty.UnsetValue, result); } + [Fact] + public void Should_Have_Null_ResultType_For_Broken_Chain() + { + var data = new { Foo = new { Bar = 1 } }; + var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + + Assert.Null(target.ResultType); + } + [Fact] public void Should_Track_Simple_Property_Value() { diff --git a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs new file mode 100644 index 0000000000..59b0d1c422 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Globalization; +using Xunit; + +namespace Perspex.Markup.UnitTests +{ + public class DefaultValueConverterTests + { + [Fact] + public void Can_Convert_String_To_Int() + { + var result = DefaultValueConverter.Instance.Convert( + "5", + typeof(int), + null, + CultureInfo.InvariantCulture); + + Assert.Equal(5, result); + } + + [Fact] + public void Can_Convert_String_To_Double() + { + var result = DefaultValueConverter.Instance.Convert( + "5", + typeof(double), + null, + CultureInfo.InvariantCulture); + + Assert.Equal(5.0, result); + } + + [Fact] + public void Can_Convert_Double_To_String() + { + var result = DefaultValueConverter.Instance.Convert( + 5.0, + typeof(string), + null, + CultureInfo.InvariantCulture); + + Assert.Equal("5", result); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 71cc52aef1..76e988d0e6 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -81,6 +81,7 @@ +