Bläddra i källkod

Fix bindings without property path (#16729)

* Added failing binding tests

* Fix bindings without property path

---------

Co-authored-by: Benedikt Stebner <[email protected]>
Julien Lebosquain 1 år sedan
förälder
incheckning
20f77a3490

+ 5 - 0
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -231,6 +231,11 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
 
         if (nodeIndex == _nodes.Count - 1)
         {
+            // If the binding source is a data context without any path and is currently null, treat it as an invalid
+            // value. This allows bindings to DataContext and DataContext.Property to share the same behavior.
+            if (value is null && _nodes[nodeIndex] is DataContextNodeBase)
+                value = AvaloniaProperty.UnsetValue;
+
             // The leaf node has changed. If the binding mode is not OneWayToSource, publish the
             // value to the target.
             if (_mode != BindingMode.OneWayToSource)

+ 10 - 0
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs

@@ -94,6 +94,16 @@ public abstract partial class BindingExpressionTests
         GC.KeepAlive(data);
     }
 
+    [Fact]
+    public void TargetNullValue_Should_Not_Be_Used_When_Source_Is_Data_Context_And_Null()
+    {
+        var target = CreateTarget<string?, string?>(
+            o => o,
+            targetNullValue: "bar");
+
+        Assert.Equal(null, target.String);
+    }
+
     [Fact]
     public void Can_Use_UpdateTarget_To_Update_From_Non_INPC_Data()
     {

+ 25 - 0
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs

@@ -69,6 +69,31 @@ public partial class BindingExpressionTests
         Assert.Equal(0.5, target.Double);
     }
 
+    [Fact]
+    public void OneTime_Binding_Waits_For_DataContext_Without_Property_Path()
+    {
+        var target = CreateTarget<string?, string?>(
+            x => x,
+            mode: BindingMode.OneTime);
+
+        target.DataContext = "foo";
+
+        Assert.Equal("foo", target.String);
+    }
+
+    [Fact]
+    public void OneTime_Binding_Waits_For_DataContext_Without_Property_Path_With_StringFormat()
+    {
+        var target = CreateTarget<string?, string?>(
+            x => x,
+            mode: BindingMode.OneTime,
+            stringFormat: "bar: {0}");
+
+        target.DataContext = "foo";
+
+        Assert.Equal("bar: foo", target.String);
+    }
+
     [Fact]
     public void OneWayToSource_Binding_Updates_Source_When_Target_Changes()
     {

+ 13 - 2
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@@ -33,6 +33,7 @@ public abstract partial class BindingExpressionTests
             RelativeSource? relativeSource,
             Optional<TIn> source,
             object? targetNullValue,
+            string? stringFormat,
             UpdateSourceTrigger updateSourceTrigger)
         {
             var target = new TargetClass { DataContext = dataContext };
@@ -66,6 +67,7 @@ public abstract partial class BindingExpressionTests
                 mode: mode,
                 targetNullValue: targetNullValue,
                 targetTypeConverter: TargetTypeConverter.GetReflectionConverter(),
+                stringFormat: stringFormat,
                 updateSourceTrigger: updateSourceTrigger);
 
             target.GetValueStore().AddBinding(targetProperty, bindingExpression);
@@ -87,6 +89,7 @@ public abstract partial class BindingExpressionTests
             RelativeSource? relativeSource,
             Optional<TIn> source,
             object? targetNullValue,
+            string? stringFormat,
             UpdateSourceTrigger updateSourceTrigger)
         {
             var target = new TargetClass { DataContext = dataContext };
@@ -112,6 +115,7 @@ public abstract partial class BindingExpressionTests
                 mode: mode,
                 targetNullValue: targetNullValue,
                 targetTypeConverter: TargetTypeConverter.GetReflectionConverter(),
+                stringFormat: stringFormat,
                 updateSourceTrigger: updateSourceTrigger);
             target.GetValueStore().AddBinding(targetProperty, bindingExpression);
             return (target, bindingExpression);
@@ -129,7 +133,8 @@ public abstract partial class BindingExpressionTests
         BindingMode mode = BindingMode.OneWay,
         RelativeSource? relativeSource = null,
         Optional<TIn> source = default,
-        object? targetNullValue = null)
+        object? targetNullValue = null,
+        string? stringFormat = null)
             where TIn : class?
     {
         var (target, _) = CreateTargetAndExpression(
@@ -143,7 +148,8 @@ public abstract partial class BindingExpressionTests
             mode,
             relativeSource,
             source,
-            targetNullValue);
+            targetNullValue,
+            stringFormat);
         return target;
     }
 
@@ -158,6 +164,7 @@ public abstract partial class BindingExpressionTests
         BindingMode mode = BindingMode.OneWay,
         RelativeSource? relativeSource = null,
         object? targetNullValue = null,
+        string? stringFormat = null,
         UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
             where TIn : class?
     {
@@ -173,6 +180,7 @@ public abstract partial class BindingExpressionTests
             relativeSource,
             source,
             targetNullValue,
+            stringFormat,
             updateSourceTrigger);
         return target;
     }
@@ -189,6 +197,7 @@ public abstract partial class BindingExpressionTests
         RelativeSource? relativeSource = null,
         Optional<TIn> source = default,
         object? targetNullValue = null,
+        string? stringFormat = null,
         UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
             where TIn : class?
     {
@@ -213,6 +222,7 @@ public abstract partial class BindingExpressionTests
             relativeSource,
             source,
             targetNullValue,
+            stringFormat,
             updateSourceTrigger);
     }
 
@@ -228,6 +238,7 @@ public abstract partial class BindingExpressionTests
         RelativeSource? relativeSource,
         Optional<TIn> source,
         object? targetNullValue,
+        string? stringFormat,
         UpdateSourceTrigger updateSourceTrigger)
             where TIn : class?;
 

+ 3 - 3
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Logging.cs

@@ -305,8 +305,8 @@ namespace Avalonia.Markup.UnitTests.Data
             [InlineData(false)]
             public void Should_Log_Invalid_TargetNullValue(bool rooted)
             {
-                var target = new Decorator { };
-                var binding = new Binding() { TargetNullValue = "foo" };
+                var target = new Decorator { DataContext = new { Bar = (string?) null }  };
+                var binding = new Binding("Bar") { TargetNullValue = "foo" };
 
                 if (rooted)
                     new TestRoot(target);
@@ -314,7 +314,7 @@ namespace Avalonia.Markup.UnitTests.Data
                 // An invalid target null value is invalid whether the control is rooted or not.
                 using (AssertLog(
                     target,
-                    "",
+                    binding.Path,
                     "Could not convert TargetNullValue 'foo' to 'System.Double'.",
                     level: LogEventLevel.Error,
                     property: Visual.OpacityProperty))