From 20f77a34909f60e65717bc3845a5f65414a6f1cc Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 15 Sep 2024 00:57:33 +0200 Subject: [PATCH] Fix bindings without property path (#16729) * Added failing binding tests * Fix bindings without property path --------- Co-authored-by: Benedikt Stebner --- .../Data/Core/BindingExpression.cs | 5 ++++ .../Core/BindingExpressionTests.GetValue.cs | 10 ++++++++ .../Data/Core/BindingExpressionTests.Mode.cs | 25 +++++++++++++++++++ .../Data/Core/BindingExpressionTests.cs | 15 +++++++++-- .../Data/BindingTests_Logging.cs | 6 ++--- 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index ba0ce7bb9e6..abf6f6cb887 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -231,6 +231,11 @@ internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataVa 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) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs index 8d7f5617593..f34b5dc132a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs @@ -94,6 +94,16 @@ public void TargetNullValue_Should_Be_Used_When_Source_String_Is_Null() GC.KeepAlive(data); } + [Fact] + public void TargetNullValue_Should_Not_Be_Used_When_Source_Is_Data_Context_And_Null() + { + var target = CreateTarget( + o => o, + targetNullValue: "bar"); + + Assert.Equal(null, target.String); + } + [Fact] public void Can_Use_UpdateTarget_To_Update_From_Non_INPC_Data() { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs index f78630bd077..0c5e23aa874 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs @@ -69,6 +69,31 @@ public void OneTime_Binding_Waits_For_DataContext_With_Matching_Property_Type() Assert.Equal(0.5, target.Double); } + [Fact] + public void OneTime_Binding_Waits_For_DataContext_Without_Property_Path() + { + var target = CreateTarget( + 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( + 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() { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index a8380291753..559b4d76c09 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -33,6 +33,7 @@ private protected override (TargetClass, BindingExpression) CreateTargetCore source, object? targetNullValue, + string? stringFormat, UpdateSourceTrigger updateSourceTrigger) { var target = new TargetClass { DataContext = dataContext }; @@ -66,6 +67,7 @@ private protected override (TargetClass, BindingExpression) CreateTargetCore source, object? targetNullValue, + string? stringFormat, UpdateSourceTrigger updateSourceTrigger) { var target = new TargetClass { DataContext = dataContext }; @@ -112,6 +115,7 @@ private protected override (TargetClass, BindingExpression) CreateTargetCore( BindingMode mode = BindingMode.OneWay, RelativeSource? relativeSource = null, Optional source = default, - object? targetNullValue = null) + object? targetNullValue = null, + string? stringFormat = null) where TIn : class? { var (target, _) = CreateTargetAndExpression( @@ -143,7 +148,8 @@ protected TargetClass CreateTarget( mode, relativeSource, source, - targetNullValue); + targetNullValue, + stringFormat); return target; } @@ -158,6 +164,7 @@ protected TargetClass CreateTargetWithSource( BindingMode mode = BindingMode.OneWay, RelativeSource? relativeSource = null, object? targetNullValue = null, + string? stringFormat = null, UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged) where TIn : class? { @@ -173,6 +180,7 @@ protected TargetClass CreateTargetWithSource( relativeSource, source, targetNullValue, + stringFormat, updateSourceTrigger); return target; } @@ -189,6 +197,7 @@ private protected (TargetClass, BindingExpression) CreateTargetAndExpression source = default, object? targetNullValue = null, + string? stringFormat = null, UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged) where TIn : class? { @@ -213,6 +222,7 @@ private protected (TargetClass, BindingExpression) CreateTargetAndExpression source, object? targetNullValue, + string? stringFormat, UpdateSourceTrigger updateSourceTrigger) where TIn : class?; diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Logging.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Logging.cs index 10a8b8855d6..a7a1e5a9e0a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Logging.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Logging.cs @@ -305,8 +305,8 @@ public void Should_Log_Invalid_FallbackValue(bool rooted) [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 @@ public void Should_Log_Invalid_TargetNullValue(bool rooted) // 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))