Browse Source

Improve broken binding diagnostic messages.

pull/691/head
Steven Kirk 10 years ago
parent
commit
f59c16bc00
  1. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  2. 15
      src/Avalonia.Base/Data/BindingBrokenException.cs
  3. 15
      src/Avalonia.Base/Data/BindingNotification.cs
  4. 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  5. 2
      src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs
  6. 9
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  7. 23
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  8. 2
      src/Markup/Avalonia.Markup/Data/IndexerNode.cs
  9. 2
      src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs
  10. 65
      src/Markup/Avalonia.Markup/Data/MarkupBindingBrokenException.cs
  11. 2
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  12. 2
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
  13. 16
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  14. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -43,6 +43,7 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\BindingBrokenException.cs" />
<Compile Include="Data\BindingNotification.cs" />
<Compile Include="Data\IndexerBinding.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />

15
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
{
/// <summary>
/// An exception returned through <see cref="BindingNotification"/> signalling that a
/// requested binding expression could not be evaluated.
/// </summary>
public class BindingBrokenException : Exception
{
}
}

15
src/Avalonia.Base/Data/BindingNotification.cs

@ -181,6 +181,21 @@ namespace Avalonia.Data
return notification != null ? notification.Value : o;
}
/// <summary>
/// Gets an exception from an object that may be a <see cref="BindingNotification"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The value.</returns>
/// <remarks>
/// If <paramref name="o"/> is a <see cref="BindingNotification"/> then returns the binding
/// notification's <see cref="Error"/>. If not, returns the object unchanged.
/// </remarks>
public static object ExtractError(object o)
{
var notification = o as BindingNotification;
return notification != null ? notification.Error : o;
}
/// <summary>
/// Compares an object to an instance of <see cref="BindingNotification"/> for equality.
/// </summary>

1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -41,6 +41,7 @@
<Compile Include="..\..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\MarkupBindingBrokenException.cs" />
<Compile Include="Data\CommonPropertyNames.cs" />
<Compile Include="Data\EmptyExpressionNode.cs" />
<Compile Include="Data\ExpressionNodeBuilder.cs" />

2
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<object> StartListening(WeakReference reference)
{
return Observable.Return(reference.Target);

9
src/Markup/Avalonia.Markup/Data/ExpressionNode.cs

@ -19,6 +19,7 @@ namespace Avalonia.Markup.Data
private IObserver<object> _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);
}
}
}

23
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()

2
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<object> StartListening(WeakReference reference)
{
var target = reference.Target;

2
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));

65
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<string> Nodes { get; } = new List<string>();
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}'.";
}
}
}
}

2
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)

2
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);

16
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),
},

2
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;

Loading…
Cancel
Save