diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 7c402f42f6..6d09befeba 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -12,8 +12,19 @@ namespace Avalonia.Data.Core base.NextValueChanged(Negate(value)); } - private static object Negate(object v) + private static object Negate(object value) { + var notification = value as BindingNotification; + var v = BindingNotification.ExtractValue(value); + + BindingNotification GenerateError(Exception e) + { + notification ??= new BindingNotification(AvaloniaProperty.UnsetValue); + notification.AddError(e, BindingErrorType.Error); + notification.ClearValue(); + return notification; + } + if (v != AvaloniaProperty.UnsetValue) { var s = v as string; @@ -28,9 +39,7 @@ namespace Avalonia.Data.Core } else { - return new BindingNotification( - new InvalidCastException($"Unable to convert '{s}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{s}' to bool.")); } } else @@ -38,24 +47,31 @@ namespace Avalonia.Data.Core try { var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); - return !boolean; + + if (notification is object) + { + notification.SetValue(!boolean); + return notification; + } + else + { + return !boolean; + } } catch (InvalidCastException) { // The error message here is "Unable to cast object of type 'System.Object' // to type 'System.IConvertible'" which is kinda useless so provide our own. - return new BindingNotification( - new InvalidCastException($"Unable to convert '{v}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{v}' to bool.")); } catch (Exception e) { - return new BindingNotification(e, BindingErrorType.Error); + return GenerateError(e); } } } - return AvaloniaProperty.UnsetValue; + return notification ?? AvaloniaProperty.UnsetValue; } public object Transform(object value) diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs index 757cd628b7..0be3bbbb9f 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.ComponentModel; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Data; @@ -89,6 +91,69 @@ namespace Avalonia.Markup.UnitTests.Parsers GC.KeepAlive(data); } + [Fact] + public async Task Should_Negate_BindingNotification_Value() + { + var data = new { Foo = true }; + var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal(new BindingNotification(false), result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Pass_Through_BindingNotification_Error() + { + var data = new { }; + var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new MissingMemberException("Could not find a matching property accessor for 'Foo' on '{ }'"), + BindingErrorType.Error), + result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Negate_BindingNotification_Error_FallbackValue() + { + var data = new Test { DataValidationError = "Test error" }; + var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new DataValidationException("Test error"), + BindingErrorType.DataValidationError, + true), + result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Add_Error_To_BindingNotification_For_FallbackValue_Not_Convertible_To_Boolean() + { + var data = new Test { Bar = new object(), DataValidationError = "Test error" }; + var target = ExpressionObserverBuilder.Build(data, "!Bar", enableDataValidation: true); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new AggregateException( + new DataValidationException("Test error"), + new InvalidCastException($"Unable to convert 'System.Object' to bool.")), + BindingErrorType.Error), + result); + + GC.KeepAlive(data); + } + [Fact] public void SetValue_Should_Return_False_For_Invalid_Value() { @@ -101,9 +166,20 @@ namespace Avalonia.Markup.UnitTests.Parsers GC.KeepAlive(data); } - private class Test + private class Test : INotifyDataErrorInfo { public bool Foo { get; set; } + public object Bar { get; set; } + + public string DataValidationError { get; set; } + public bool HasErrors => !string.IsNullOrWhiteSpace(DataValidationError); + + public event EventHandler ErrorsChanged; + + public IEnumerable GetErrors(string propertyName) + { + return DataValidationError is object ? new[] { DataValidationError } : null; + } } } }