diff --git a/src/Markup/Perspex.Markup/DefaultValueConverter.cs b/src/Markup/Perspex.Markup/DefaultValueConverter.cs index 9f535081a7..8a24b53612 100644 --- a/src/Markup/Perspex.Markup/DefaultValueConverter.cs +++ b/src/Markup/Perspex.Markup/DefaultValueConverter.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.Linq; using System.Reflection; +using Perspex.Data; using Perspex.Logging; using Perspex.Utilities; @@ -42,12 +43,8 @@ namespace Perspex.Markup if (value != null) { - Logger.Error( - LogArea.Binding, - this, - "Could not convert {Value} to {Type}", - value, - targetType); + var message = $"Could not convert {value} to {targetType}"; + return new BindingError(new InvalidCastException(message)); } return PerspexProperty.UnsetValue; diff --git a/src/Perspex.Base/Data/BindingError.cs b/src/Perspex.Base/Data/BindingError.cs index d29b06fea9..b9330bd278 100644 --- a/src/Perspex.Base/Data/BindingError.cs +++ b/src/Perspex.Base/Data/BindingError.cs @@ -10,8 +10,12 @@ namespace Perspex.Data /// /// /// When produced by a binding source observable, informs the binding system that an error - /// occurred. It causes a binding error to be logged: the value of the bound property will not - /// change. + /// occurred. It can also provide an optional fallback value to be pushed to the binding + /// target. + /// + /// Instead of using , one could simply not push a value (in the + /// case of a no fallback value) or push a fallback value, but BindingError also causes an + /// error to be logged with the correct binding target. /// public class BindingError { @@ -24,9 +28,32 @@ namespace Perspex.Data Exception = exception; } + /// + /// Initializes a new instance of the class. + /// + /// An exception describing the binding error. + /// The fallback value. + public BindingError(Exception exception, object fallbackValue) + { + Exception = exception; + FallbackValue = fallbackValue; + UseFallbackValue = true; + } + /// /// Gets the exception describing the binding error. /// public Exception Exception { get; } + + /// + /// Get the fallback value. + /// + public object FallbackValue { get; } + + /// + /// Get a value indicating whether the fallback value should be pushed to the binding + /// target. + /// + public bool UseFallbackValue { get; } } } diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 86ebb45921..34bf428cc1 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -672,6 +672,11 @@ namespace Perspex } else { + if (error.UseFallbackValue) + { + SetValue(property, error.FallbackValue); + } + Logger.Error( LogArea.Binding, this, diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index 097614013d..78292a5940 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -267,6 +267,32 @@ namespace Perspex.Base.UnitTests Assert.Equal("first", target2.GetValue(Class1.FooProperty)); } + [Fact] + public void BindingError_Does_Not_Cause_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + } + + [Fact] + public void BindingError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(new BindingError(new InvalidOperationException("Foo"), 8.9)); + + Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); + } + [Fact] public void Bind_Logs_BindingError() { diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index 48c6d2c9da..ceca0cdc38 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -396,6 +396,32 @@ namespace Perspex.Base.UnitTests Assert.True(raised); } + [Fact] + public void BindingError_Does_Not_Cause_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void BindingError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(new BindingError(new InvalidOperationException("Foo"), "fallback")); + + Assert.Equal("fallback", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Binding_To_Direct_Property_Logs_BindingError() { diff --git a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs index 55034a3c53..4b203974db 100644 --- a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs +++ b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs @@ -3,6 +3,7 @@ using System.Globalization; using Perspex.Controls; +using Perspex.Data; using Xunit; namespace Perspex.Markup.UnitTests @@ -114,7 +115,7 @@ namespace Perspex.Markup.UnitTests null, CultureInfo.InvariantCulture); - Assert.Equal(PerspexProperty.UnsetValue, result); + Assert.IsType(result); } private enum TestEnum