Bläddra i källkod

Improve broken binding diagnostic messages.

Steven Kirk 9 år sedan
förälder
incheckning
f59c16bc00

+ 1 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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);

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

+ 20 - 3
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 - 0
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 - 0
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 - 0
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}'.";
+            }
+        }
+    }
+}

+ 1 - 1
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)

+ 1 - 1
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);

+ 8 - 8
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),
                 }, 

+ 1 - 1
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;