diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 75b5b9da29..fd5aaf97aa 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -43,7 +43,7 @@ Properties\SharedAssemblyInfo.cs - + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e6b88c71f8..35dae50d27 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -379,12 +379,11 @@ namespace Avalonia } subscription = source - .Where(x => !(x is IValidationStatus)) .Select(x => CastOrDefault(x, property.PropertyType)) .Do(_ => { }, () => _directBindings.Remove(subscription)) .Subscribe(x => DirectBindingSet(property, x)); validationSubcription = source - .OfType() + .OfType() .Subscribe(x => DataValidationChanged(property, x)); _directBindings.Add(subscription); @@ -487,7 +486,7 @@ namespace Avalonia } /// - void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, IValidationStatus status) + void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, BindingNotification status) { var property = sender.Property; DataValidationChanged(property, status); @@ -623,14 +622,14 @@ namespace Avalonia /// /// 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 . + /// The cast value, or a . private static object CastOrDefault(object value, Type type) { - var error = value as BindingError; + var error = value as BindingNotification; if (error == null) { @@ -674,26 +673,29 @@ namespace Avalonia /// private void DirectBindingSet(AvaloniaProperty property, object value) { - var error = value as BindingError; + var notification = value as BindingNotification; - if (error == null) + if (notification == null) { SetValue(property, value); } else { - if (error.UseFallbackValue) + if (notification.HasValue) { - SetValue(property, error.FallbackValue); + SetValue(property, notification.Value); } - Logger.Error( - LogArea.Binding, - this, - "Error binding to {Target}.{Property}: {Message}", - this, - property, - error.Exception.Message); + if (notification.ErrorType == BindingErrorType.Error) + { + Logger.Error( + LogArea.Binding, + this, + "Error binding to {Target}.{Property}: {Message}", + this, + property, + notification.Error.Message); + } } } diff --git a/src/Avalonia.Base/Data/BindingError.cs b/src/Avalonia.Base/Data/BindingError.cs deleted file mode 100644 index 5157586a49..0000000000 --- a/src/Avalonia.Base/Data/BindingError.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Data -{ - /// - /// Represents a recoverable binding error. - /// - /// - /// When produced by a binding source observable, informs the binding system that an error - /// 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 - { - /// - /// Initializes a new instance of the class. - /// - /// An exception describing the binding error. - public BindingError(Exception exception) - { - 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/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs new file mode 100644 index 0000000000..60387778fc --- /dev/null +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -0,0 +1,169 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Data +{ + /// + /// Defines the types of binding errors for a . + /// + public enum BindingErrorType + { + /// + /// There was no error. + /// + None, + + /// + /// There was a binding error. + /// + Error, + + /// + /// There was a data validation error. + /// + DataValidationError, + } + + /// + /// Represents a binding notification that can be a valid binding value, or a binding or + /// data validation error. + /// + public class BindingNotification : IValidationStatus + { + /// + /// A binding notification representing the null value. + /// + public static readonly BindingNotification Null = + new BindingNotification(null); + + /// + /// A binding notification representing . + /// + public static readonly BindingNotification UnsetValue = + new BindingNotification(AvaloniaProperty.UnsetValue); + + /// + /// Initializes a new instance of the class. + /// + /// The binding value. + public BindingNotification(object value) + { + Value = value; + HasValue = true; + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding error. + /// The type of the binding error. + public BindingNotification(Exception error, BindingErrorType errorType) + { + if (errorType == BindingErrorType.None) + { + throw new ArgumentException($"'errorType' may not be None"); + } + + Error = error; + ErrorType = errorType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding error. + /// The type of the binding error. + /// The fallback value. + public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue) + : this(error) + { + Value = fallbackValue; + HasValue = true; + } + + /// + /// Gets the value that should be passed to the target when + /// is true. + /// + public object Value { get; } + + /// + /// Gets a value indicating whether should be pushed to the target. + /// + public bool HasValue { get; } + + /// + /// Gets the error that occurred on the source, if any. + /// + public Exception Error { get; } + + /// + /// Gets the type of error that represents, if any. + /// + public BindingErrorType ErrorType { get; } + + bool IValidationStatus.IsValid => ErrorType == BindingErrorType.None; + + public static bool operator ==(BindingNotification a, BindingNotification b) + { + if (object.ReferenceEquals(a, b)) + { + return true; + } + + if ((object)a == null || (object)b == null) + { + return false; + } + + return a.HasValue == b.HasValue && + a.ErrorType == b.ErrorType && + (!a.HasValue || object.Equals(a.Value, b.Value)) && + (a.ErrorType == BindingErrorType.None || object.Equals(a.Error, b.Error)); + } + + public static bool operator !=(BindingNotification a, BindingNotification b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + return Equals(obj as BindingNotification); + } + + public bool Equals(BindingNotification other) + { + return this == other; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public BindingNotification WithError(Exception e) + { + if (e == null) + { + return this; + } + + if (Error != null) + { + e = new AggregateException(Error, e); + } + + if (HasValue) + { + return new BindingNotification(e, BindingErrorType.Error, Value); + } + else + { + return new BindingNotification(e, BindingErrorType.Error, Value); + } + } + } +} diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 743eba4453..b5a0c0abc1 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -23,6 +23,6 @@ namespace Avalonia /// /// The source of the change. /// The validation status. - void DataValidationChanged(PriorityValue sender, IValidationStatus status); + void DataValidationChanged(PriorityValue sender, BindingNotification status); } } diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs index 3368f45b82..85c8c7e55c 100644 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ b/src/Avalonia.Base/PriorityBindingEntry.cs @@ -93,22 +93,28 @@ namespace Avalonia private void ValueChanged(object value) { - var bindingError = value as BindingError; + var notification = value as BindingNotification; - if (bindingError != null) + if (notification != null) { - _owner.Error(this, bindingError); + if (notification.ErrorType == BindingErrorType.Error) + { + _owner.Error(this, notification); + } + else if (notification.ErrorType == BindingErrorType.DataValidationError) + { + _owner.Validation(this, notification); + } + + if (notification.HasValue) + { + Value = notification.Value; + _owner.Changed(this); + } } - - var validationStatus = value as IValidationStatus; - - if (validationStatus != null) - { - _owner.Validation(this, validationStatus); - } - else if (bindingError == null || bindingError.UseFallbackValue) + else { - Value = bindingError == null ? value : bindingError.FallbackValue; + Value = value; _owner.Changed(this); } } diff --git a/src/Avalonia.Base/PriorityLevel.cs b/src/Avalonia.Base/PriorityLevel.cs index c0460a775b..b056dd38b7 100644 --- a/src/Avalonia.Base/PriorityLevel.cs +++ b/src/Avalonia.Base/PriorityLevel.cs @@ -159,7 +159,7 @@ namespace Avalonia /// /// The entry that completed. /// The error. - public void Error(PriorityBindingEntry entry, BindingError error) + public void Error(PriorityBindingEntry entry, BindingNotification error) { _owner.LevelError(this, error); } @@ -169,7 +169,7 @@ namespace Avalonia /// /// The entry that completed. /// The validation status. - public void Validation(PriorityBindingEntry entry, IValidationStatus validationStatus) + public void Validation(PriorityBindingEntry entry, BindingNotification validationStatus) { _owner.LevelValidation(this, validationStatus); } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 20434bc97d..715fca3915 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -184,7 +184,7 @@ namespace Avalonia /// /// The priority level of the changed entry. /// The validation status. - public void LevelValidation(PriorityLevel priorityLevel, IValidationStatus validationStatus) + public void LevelValidation(PriorityLevel priorityLevel, BindingNotification validationStatus) { _owner.DataValidationChanged(this, validationStatus); } @@ -194,7 +194,7 @@ namespace Avalonia /// /// The priority level of the changed entry. /// The binding error. - public void LevelError(PriorityLevel level, BindingError error) + public void LevelError(PriorityLevel level, BindingNotification error) { Logger.Log( LogEventLevel.Error, @@ -203,7 +203,7 @@ namespace Avalonia "Error binding to {Target}.{Property}: {Message}", _owner, Property, - error.Exception.Message); + error.Error.Message); } /// diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs index be4287605c..84ba432753 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs @@ -57,7 +57,7 @@ namespace Avalonia.Markup.Xaml.Templates { return null; } - else if (result is BindingError) + else if (result is BindingNotification) { return null; } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index 90a654d9e6..b61261365b 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs @@ -108,14 +108,14 @@ namespace Avalonia.Markup.Data protected virtual void SendValidationStatus(IValidationStatus status) { //Even if elements only bound to sub-values, send validation changes along so they will be surfaced to the UI level. - if (_subject != null) - { - _subject.OnNext(status); - } - else - { - Next?.SendValidationStatus(status); - } + //if (_subject != null) + //{ + // _subject.OnNext(status); + //} + //else + //{ + // Next?.SendValidationStatus(status); + //} } protected virtual void Unsubscribe(object target) diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs index 0a3be26c18..49c959cbfe 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs @@ -126,16 +126,16 @@ namespace Avalonia.Markup.Data converted = TypeUtilities.Default(type); _inner.SetValue(converted, _priority); } - else if (converted is BindingError) + else if (converted is BindingNotification) { - var error = converted as BindingError; + var error = converted as BindingNotification; Logger.Error( LogArea.Binding, this, "Error binding to {Expression}: {Message}", _inner.Expression, - error.Exception.Message); + error.Error.Message); if (_fallbackValue != AvaloniaProperty.UnsetValue) { @@ -174,7 +174,7 @@ namespace Avalonia.Markup.Data private object ConvertValue(object value) { var converted = - value as BindingError ?? + value as BindingNotification ?? value as IValidationStatus ?? Converter.Convert( value, @@ -184,9 +184,9 @@ namespace Avalonia.Markup.Data if (_fallbackValue != AvaloniaProperty.UnsetValue && (converted == AvaloniaProperty.UnsetValue || - converted is BindingError)) + converted is BindingNotification)) { - var error = converted as BindingError; + var error = converted as BindingNotification; if (TypeUtilities.TryConvert( _targetType, @@ -196,14 +196,15 @@ namespace Avalonia.Markup.Data { if (error != null) { - converted = new BindingError(error.Exception, converted); + converted = new BindingNotification(error.Error, BindingErrorType.Error, converted); } } else { - converted = new BindingError( + converted = new BindingNotification( new InvalidCastException( - $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'")); + $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"), + BindingErrorType.Error); } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs index b2d73ab8fc..c46d3571dc 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -56,7 +56,7 @@ namespace Avalonia.Markup.Data.Plugins { var message = $"Could not find AvaloniaProperty '{propertyName}' on '{instance}'"; var exception = new MissingMemberException(message); - return new PropertyError(new BindingError(exception)); + return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } else { diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs index 36046dd742..2596cc77a2 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -61,7 +61,7 @@ namespace Avalonia.Markup.Data.Plugins { var message = $"Could not find CLR property '{propertyName}' on '{instance}'"; var exception = new MissingMemberException(message); - return new PropertyError(new BindingError(exception)); + return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs index f73bb1fc94..f0c0e3fb1a 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs @@ -8,13 +8,13 @@ namespace Avalonia.Markup.Data.Plugins /// public class PropertyError : IPropertyAccessor { - private BindingError _error; + private BindingNotification _error; /// /// Initializes a new instance of the class. /// /// The error to report. - public PropertyError(BindingError error) + public PropertyError(BindingNotification error) { _error = error; } diff --git a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs index 469b00d3ad..cde73b67a1 100644 --- a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs +++ b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs @@ -44,7 +44,7 @@ namespace Avalonia.Markup if (value != null) { var message = $"Could not convert '{value}' to '{targetType}'"; - return new BindingError(new InvalidCastException(message)); + return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error); } return AvaloniaProperty.UnsetValue; diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject index 30815b1937..b5cd70b13f 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + LongTestTimesWithoutParallelExecution \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 2e7db2da05..5c3459e3db 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -273,26 +273,31 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.QuxProperty, source); source.OnNext(6.7); - source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error)); Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); } [Fact] - public void BindingError_With_FallbackValue_Causes_Target_Update() + public void BindingNotification_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)); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error, + 8.9)); Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); } [Fact] - public void Bind_Logs_BindingError() + public void Bind_Logs_Binding_Error() { var target = new Class1(); var source = new Subject(); @@ -313,7 +318,9 @@ namespace Avalonia.Base.UnitTests { target.Bind(Class1.QuxProperty, source); source.OnNext(6.7); - source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error)); Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); Assert.True(called); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 6f1f59b77b..cc3e5c4052 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -406,7 +406,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.Error)); Assert.Equal("initial", target.GetValue(Class1.FooProperty)); } @@ -419,7 +419,10 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingError(new InvalidOperationException("Foo"), "fallback")); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error, + "fallback")); Assert.Equal("fallback", target.GetValue(Class1.FooProperty)); } @@ -449,7 +452,7 @@ namespace Avalonia.Base.UnitTests { target.Bind(Class1.FooProperty, source); source.OnNext("baz"); - source.OnNext(new BindingError(new InvalidOperationException("Binding Error Message"))); + source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error)); } Assert.True(called); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index 643b8fccab..7c0dbe27a5 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -81,11 +81,11 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var result = await target.Take(1); - Assert.IsType(result); + Assert.IsType(result); - var error = result as BindingError; - Assert.IsType(error.Exception); - Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Exception.Message); + var error = result as BindingNotification; + Assert.IsType(error.Error); + Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Error.Message); } [Fact] @@ -216,7 +216,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(3, result.Count); Assert.Equal("bar", result[0]); - Assert.IsType(result[1]); + Assert.IsType(result[1]); Assert.Equal("baz", result[2]); sub.Dispose(); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs index 0b6a507274..449f402850 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs @@ -55,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); - Assert.IsType(result); + Assert.IsType(result); } [Fact] diff --git a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs index 2e52dff087..fd28f2e900 100644 --- a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs +++ b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs @@ -115,7 +115,7 @@ namespace Avalonia.Markup.UnitTests null, CultureInfo.InvariantCulture); - Assert.IsType(result); + Assert.IsType(result); } private enum TestEnum