diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 38eb0d56bc..97dfd1230d 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -43,6 +43,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Avalonia.Base/Data/BindingBrokenException.cs b/src/Avalonia.Base/Data/BindingBrokenException.cs new file mode 100644 index 0000000000..057629edb8 --- /dev/null +++ b/src/Avalonia.Base/Data/BindingBrokenException.cs @@ -0,0 +1,15 @@ +// 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 +{ + /// + /// An exception returned through signalling that a + /// requested binding expression could not be evaluated. + /// + public class BindingBrokenException : Exception + { + } +} diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index e92ea8422a..0f587b969e 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -181,6 +181,21 @@ namespace Avalonia.Data return notification != null ? notification.Value : o; } + /// + /// Gets an exception from an object that may be a . + /// + /// The object. + /// The value. + /// + /// If is a then returns the binding + /// notification's . If not, returns the object unchanged. + /// + public static object ExtractError(object o) + { + var notification = o as BindingNotification; + return notification != null ? notification.Error : o; + } + /// /// Compares an object to an instance of for equality. /// diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 411effd212..334add960e 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -41,6 +41,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs index d0133f161e..feca16a4b4 100644 --- a/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs @@ -8,6 +8,8 @@ namespace Avalonia.Markup.Data { internal class EmptyExpressionNode : ExpressionNode { + public override string Description => "."; + protected override IObservable StartListening(WeakReference reference) { return Observable.Return(reference.Target); diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index 75c4422962..d6f2d66adf 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs @@ -19,6 +19,7 @@ namespace Avalonia.Markup.Data private IObserver _observer; private IDisposable _valuePluginSubscription; + public abstract string Description { get; } public ExpressionNode Next { get; set; } public WeakReference Target @@ -91,6 +92,8 @@ namespace Avalonia.Markup.Data protected virtual void NextValueChanged(object value) { + var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingBrokenException; + bindingBroken?.Nodes.Add(Description); _observer.OnNext(value); } @@ -177,8 +180,10 @@ namespace Avalonia.Markup.Data private BindingNotification TargetNullNotification() { - // TODO: Work out a way to give a more useful error message here. - return new BindingNotification(new NullReferenceException(), BindingErrorType.Error, AvaloniaProperty.UnsetValue); + return new BindingNotification( + new MarkupBindingBrokenException(this), + BindingErrorType.Error, + AvaloniaProperty.UnsetValue); } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index 486ee59469..f3f175e04c 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -182,7 +182,7 @@ namespace Avalonia.Markup.Data .Publish(UninitializedValue) .RefCount() .Where(x => x != UninitializedValue) - .Select(FromWeakReference); + .Select(Translate); } return _result.Subscribe(observer); @@ -205,9 +205,26 @@ namespace Avalonia.Markup.Data return o is BindingNotification ? o : new WeakReference(o); } - private static object FromWeakReference(object o) + private object Translate(object o) { - return o is WeakReference ? ((WeakReference)o).Target : o; + var weak = o as WeakReference; + + if (weak != null) + { + return weak.Target; + } + else + { + var notification = o as BindingNotification; + var broken = notification.Error as MarkupBindingBrokenException; + + if (broken != null) + { + broken.Expression = Expression; + } + + return notification; + } } private IDisposable StartRoot() diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs index f9615ee804..a88a673ca5 100644 --- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs @@ -21,6 +21,8 @@ namespace Avalonia.Markup.Data Arguments = arguments; } + public override string Description => "[" + string.Join(",", Arguments) + "]"; + protected override IObservable StartListening(WeakReference reference) { var target = reference.Target; diff --git a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs index 0d96830024..58a7915254 100644 --- a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs +++ b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs @@ -9,6 +9,8 @@ namespace Avalonia.Markup.Data { internal class LogicalNotNode : ExpressionNode { + public override string Description => "!"; + protected override void NextValueChanged(object value) { base.NextValueChanged(Negate(value)); diff --git a/src/Markup/Avalonia.Markup/Data/MarkupBindingBrokenException.cs b/src/Markup/Avalonia.Markup/Data/MarkupBindingBrokenException.cs new file mode 100644 index 0000000000..33aef139a0 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/MarkupBindingBrokenException.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Data; + +namespace Avalonia.Markup.Data +{ + public class MarkupBindingBrokenException : BindingBrokenException + { + private string _message; + + public MarkupBindingBrokenException() + { + } + + public MarkupBindingBrokenException(string message) + { + _message = message; + } + + internal MarkupBindingBrokenException(ExpressionNode node) + { + Nodes.Add(node.Description); + } + + public override string Message + { + get + { + if (_message != null) + { + return _message; + } + else + { + return _message = BuildMessage(); + } + } + } + + internal string Expression { get; set; } + internal IList Nodes { get; } = new List(); + + private string BuildMessage() + { + if (Nodes.Count == 0) + { + return "The binding chain was broken."; + } + else if (Nodes.Count == 1) + { + return $"'{Nodes[0]}' is null in expression '{Expression}'."; + } + else + { + var brokenPath = string.Join(".", Nodes.Skip(1).Reverse()) + .Replace(".!", "!") + .Replace(".[", "["); + return $"'{brokenPath}' is null in expression '{Expression}'."; + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs index ebeebcd07d..47da0c86b6 100644 --- a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs @@ -21,8 +21,8 @@ namespace Avalonia.Markup.Data _enableValidation = enableValidation; } + public override string Description => PropertyName; public string PropertyName { get; } - public Type PropertyType => _accessor?.PropertyType; public bool SetTargetValue(object value, BindingPriority priority) diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs index cdcaeda4cc..1815d82c12 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs @@ -143,7 +143,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Inner' is null in expression 'Inner.MustBePositive'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index b65dd091e0..bd7f28f620 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -66,7 +66,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Foo' is null in expression 'Foo'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); @@ -81,7 +81,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Foo' is null in expression 'Foo'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); @@ -96,7 +96,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Foo' is null in expression 'Foo'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); @@ -111,7 +111,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Foo' is null in expression 'Foo'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); @@ -166,7 +166,7 @@ namespace Avalonia.Markup.UnitTests.Data new[] { new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Foo' is null in expression 'Foo.Bar.Baz'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, @@ -284,7 +284,7 @@ namespace Avalonia.Markup.UnitTests.Data { "bar", new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Next' is null in expression 'Next.Bar'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), "baz" @@ -483,7 +483,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Foo' is null in expression 'Foo'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); @@ -511,7 +511,7 @@ namespace Avalonia.Markup.UnitTests.Data "foo", "bar", new BindingNotification( - new NullReferenceException(), + new MarkupBindingBrokenException("'Foo' is null in expression 'Foo'."), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs index 029285341b..ed7a6bddc3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs @@ -43,7 +43,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml pv.Length == 3 && pv[0] is ProgressBar && object.ReferenceEquals(pv[1], ProgressBar.ValueProperty) && - (string)pv[2] == "Object reference not set to an instance of an object. | " + + (string)pv[2] == "'Value' is null in expression 'Value'. | " + "Could not convert FallbackValue 'bar' to 'System.Double'") { called = true;