diff --git a/src/Perspex.Base/Data/BindingError.cs b/src/Perspex.Base/Data/BindingError.cs new file mode 100644 index 0000000000..d29b06fea9 --- /dev/null +++ b/src/Perspex.Base/Data/BindingError.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Perspex.Data +{ + /// + /// Represents a recoverable binding error. + /// + /// + /// 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. + /// + public class BindingError + { + /// + /// Initializes a new instance of the class. + /// + /// An exception describing the binding error. + public BindingError(Exception exception) + { + Exception = exception; + } + + /// + /// Gets the exception describing the binding error. + /// + public Exception Exception { get; } + } +} diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index ae185b3abf..84dbd9a0d3 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -43,6 +43,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 32f8eedc91..86ebb45921 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -403,9 +403,9 @@ namespace Perspex IDisposable subscription = null; subscription = source - .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType)) + .Select(x => CastOrDefault(x, property.PropertyType)) .Do(_ => { }, () => s_directBindings.Remove(subscription)) - .Subscribe(x => SetValue(property, x)); + .Subscribe(x => DirectBindingSet(property, x)); s_directBindings.Add(subscription); @@ -587,6 +587,27 @@ namespace Perspex } } + /// + /// Tries to cast a value to a type, taking into account that the value may be a + /// . + /// + /// The value. + /// The type. + /// The cast value, or a . + private static object CastOrDefault(object value, Type type) + { + var error = value as BindingError; + + if (error == null) + { + return TypeUtilities.CastOrDefault(value, type); + } + else + { + return error; + } + } + /// /// Creates a for a . /// @@ -635,6 +656,33 @@ namespace Perspex return result; } + /// + /// Sets a property value for a direct property binding. + /// + /// The property. + /// The value. + /// + private void DirectBindingSet(PerspexProperty property, object value) + { + var error = value as BindingError; + + if (error == null) + { + SetValue(property, value); + } + else + { + Logger.Error( + LogArea.Binding, + this, + "Error binding to {Target}.{PropertyName}: {Message}", + property.Name, + property.PropertyType, + value, + value.GetType()); + } + } + /// /// Converts an unset value to the default value for a direct property. /// diff --git a/src/Perspex.Base/PriorityBindingEntry.cs b/src/Perspex.Base/PriorityBindingEntry.cs index a17080f0f4..3fcb77f950 100644 --- a/src/Perspex.Base/PriorityBindingEntry.cs +++ b/src/Perspex.Base/PriorityBindingEntry.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Perspex.Data; namespace Perspex { @@ -63,10 +64,12 @@ namespace Perspex /// The binding. /// Called when the binding changes. /// Called when the binding completes. + /// Called when a binding error occurs. public void Start( IObservable binding, Action changed, - Action completed) + Action completed, + Action error) { Contract.Requires(binding != null); Contract.Requires(changed != null); @@ -88,8 +91,17 @@ namespace Perspex _subscription = binding.Subscribe( value => { - Value = value; - changed(this); + var bindingError = value as BindingError; + + if (bindingError == null) + { + Value = value; + changed(this); + } + else if (error != null) + { + error(this, bindingError); + } }, () => completed(this)); } diff --git a/src/Perspex.Base/PriorityLevel.cs b/src/Perspex.Base/PriorityLevel.cs index 3c47199525..25cce3800b 100644 --- a/src/Perspex.Base/PriorityLevel.cs +++ b/src/Perspex.Base/PriorityLevel.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Perspex.Data; +using Perspex.Logging; namespace Perspex { @@ -52,6 +54,11 @@ namespace Perspex /// private readonly Action _changed; + /// + /// Method called when a binding error occurs. + /// + private readonly Action _error; + /// /// The current direct value. /// @@ -70,15 +77,18 @@ namespace Perspex /// The priority. /// The precedence mode. /// A method to be called when the current value changes. + /// A method to be called when a binding error occurs. public PriorityLevel( int priority, LevelPrecedenceMode mode, - Action changed) + Action changed, + Action error) { Contract.Requires(changed != null); _mode = mode; _changed = changed; + _error = error; Priority = priority; Value = _directValue = PerspexProperty.UnsetValue; ActiveBindingIndex = -1; @@ -135,7 +145,7 @@ namespace Perspex var entry = new PriorityBindingEntry(_nextIndex++); var node = Bindings.AddFirst(entry); - entry.Start(binding, Changed, Completed); + entry.Start(binding, Changed, Completed, Error); return Disposable.Create(() => { @@ -184,6 +194,16 @@ namespace Perspex } } + /// + /// Invoked when an entry in encounters a recoverable error. + /// + /// The entry that completed. + /// The error. + private void Error(PriorityBindingEntry entry, BindingError error) + { + _error(this, error); + } + /// /// Activates the first binding that has a value. /// diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index a8b72a376d..5c469003fb 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using System.Text; +using Perspex.Data; using Perspex.Logging; using Perspex.Utilities; @@ -210,7 +211,7 @@ namespace Perspex if (!_levels.TryGetValue(priority, out result)) { var mode = (LevelPrecedenceMode)(priority % 2); - result = new PriorityLevel(priority, mode, ValueChanged); + result = new PriorityLevel(priority, mode, ValueChanged, Error); _levels.Add(priority, result); } @@ -242,7 +243,7 @@ namespace Perspex else { Logger.Error( - LogArea.Property, + LogArea.Binding, _owner, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", _name, @@ -279,5 +280,22 @@ namespace Perspex } } } + + /// + /// Called when a priority level encounters an error. + /// + /// The priority level of the changed entry. + /// The binding error. + private void Error(PriorityLevel level, BindingError error) + { + Logger.Log( + LogEventLevel.Error, + LogArea.Binding, + _owner, + "Error binding to {Target}.{PropertyName}: {Message}", + _owner, + _name, + error.Exception.Message); + } } } diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index 879022611c..7e65cf4b1f 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -61,10 +61,6 @@ ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll True - - ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll - True - ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll @@ -124,6 +120,10 @@ {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Perspex.Base + + {88060192-33d5-4932-b0f9-8bd2763e857d} + Perspex.UnitTests + diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index caa36f38a8..097614013d 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -2,10 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Generic; using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.Reactive.Testing; using Perspex.Data; +using Perspex.Logging; +using Perspex.UnitTests; using Xunit; namespace Perspex.Base.UnitTests @@ -263,6 +267,35 @@ namespace Perspex.Base.UnitTests Assert.Equal("first", target2.GetValue(Class1.FooProperty)); } + [Fact] + public void Bind_Logs_BindingError() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + var expectedMessageTemplate = "Error binding to {Target}.{PropertyName}: {Message}"; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Error && + area == LogArea.Binding && + mt == expectedMessageTemplate) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + Assert.True(called); + } + } + /// /// Returns an observable that returns a single value but does not complete. /// diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index ebf0484ba8..48c6d2c9da 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; using Perspex.Data; +using Perspex.Logging; +using Perspex.UnitTests; using Xunit; namespace Perspex.Base.UnitTests @@ -394,6 +396,34 @@ namespace Perspex.Base.UnitTests Assert.True(raised); } + [Fact] + public void Binding_To_Direct_Property_Logs_BindingError() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + var expectedMessageTemplate = "Error binding to {Target}.{PropertyName}: {Message}"; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Error && + area == LogArea.Binding && + mt == expectedMessageTemplate) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.FooProperty, source); + source.OnNext("baz"); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + } + + Assert.True(called); + } + private class Class1 : PerspexObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Perspex.UnitTests/Perspex.UnitTests.csproj b/tests/Perspex.UnitTests/Perspex.UnitTests.csproj index d06a4e98da..44f6d01b26 100644 --- a/tests/Perspex.UnitTests/Perspex.UnitTests.csproj +++ b/tests/Perspex.UnitTests/Perspex.UnitTests.csproj @@ -63,6 +63,7 @@ + diff --git a/tests/Perspex.UnitTests/TestLogSink.cs b/tests/Perspex.UnitTests/TestLogSink.cs new file mode 100644 index 0000000000..d2ef00dce9 --- /dev/null +++ b/tests/Perspex.UnitTests/TestLogSink.cs @@ -0,0 +1,38 @@ +// 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.Reactive.Disposables; +using Perspex.Logging; + +namespace Perspex.UnitTests +{ + public delegate void LogCallback( + LogEventLevel level, + string area, + object source, + string messageTemplate, + params object[] propertyValues); + + public class TestLogSink : ILogSink + { + private LogCallback _callback; + + public TestLogSink(LogCallback callback) + { + _callback = callback; + } + + public static IDisposable Start(LogCallback callback) + { + var sink = new TestLogSink(callback); + Logger.Sink = sink; + return Disposable.Create(() => Logger.Sink = null); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + { + _callback(level, area, source, messageTemplate, propertyValues); + } + } +}