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;