Browse Source

Fix BindingExpression.LeafNode throwing when nodes list is empty (#20442)

Make LeafNode property nullable and return null when the binding expression
has no nodes (e.g., when using a constant source with a converter but no path).
This fixes an ArgumentOutOfRangeException that occurred when accessing
LeafNode or Description properties on such bindings.

Fixes #20441

Co-authored-by: Claude Sonnet 4.5 <[email protected]>
Steven Kirk 1 week ago
parent
commit
5d3a9c167a

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

@@ -138,7 +138,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
         get
         get
         {
         {
             var b = new StringBuilder();
             var b = new StringBuilder();
-            LeafNode.BuildString(b, _nodes);
+            LeafNode?.BuildString(b, _nodes);
             return b.ToString();
             return b.ToString();
         }
         }
     }
     }
@@ -149,7 +149,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
     public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture;
     public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture;
     public object? ConverterParameter => _uncommon?._converterParameter;
     public object? ConverterParameter => _uncommon?._converterParameter;
     public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue;
     public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue;
-    public ExpressionNode LeafNode => _nodes[_nodes.Count - 1];
+    public ExpressionNode? LeafNode => _nodes.Count > 0 ? _nodes[_nodes.Count - 1] : null;
     public string? StringFormat => _uncommon?._stringFormat;
     public string? StringFormat => _uncommon?._stringFormat;
     public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue;
     public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue;
     public UpdateSourceTrigger UpdateSourceTrigger => _uncommon?._updateSourceTrigger ?? UpdateSourceTrigger.PropertyChanged;
     public UpdateSourceTrigger UpdateSourceTrigger => _uncommon?._updateSourceTrigger ?? UpdateSourceTrigger.PropertyChanged;
@@ -360,7 +360,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
 
 
         // Don't set the value if it's unchanged. If there is a binding error, we still have to set the value
         // Don't set the value if it's unchanged. If there is a binding error, we still have to set the value
         // in order to clear the error.
         // in order to clear the error.
-        if (TypeUtilities.IdentityEquals(LeafNode.Value, value, type) && ErrorType == BindingErrorType.None)
+        if (TypeUtilities.IdentityEquals(LeafNode!.Value, value, type) && ErrorType == BindingErrorType.None)
             return true;
             return true;
 
 
         try
         try
@@ -515,7 +515,8 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
         if (TryGetTarget(out var target) &&
         if (TryGetTarget(out var target) &&
             TargetProperty is not null &&
             TargetProperty is not null &&
             target.GetValue(TargetProperty) is var value &&
             target.GetValue(TargetProperty) is var value &&
-            !TypeUtilities.IdentityEquals(value, LeafNode.Value, TargetType))
+            LeafNode is { } leafNode &&
+            !TypeUtilities.IdentityEquals(value, leafNode.Value, TargetType))
         {
         {
             WriteValueToSource(value);
             WriteValueToSource(value);
         }
         }

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

@@ -1,5 +1,8 @@
 using System;
 using System;
 using Avalonia.Data;
 using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Data.Core;
+using Avalonia.UnitTests;
 using Xunit;
 using Xunit;
 
 
 #nullable enable
 #nullable enable
@@ -163,4 +166,29 @@ public abstract partial class BindingExpressionTests
 
 
         Assert.Equal("foo", target.String);
         Assert.Equal("foo", target.String);
     }
     }
+
+    [Fact]
+    public void LeafNode_Should_Be_Null_When_Nodes_List_Is_Empty()
+    {
+        using (UnitTestApplication.Start(TestServices.StyledWindow))
+        {
+            // Reproduces issue #20441
+            // Create a binding expression with no nodes (e.g., {Binding Source='Elements', Converter={...}})
+            var bindingExpression = new BindingExpression(
+                source: "Elements",
+                nodes: null,  // This results in an empty nodes list
+                fallbackValue: AvaloniaProperty.UnsetValue,
+                converter: new PrefixConverter("Prefix"),
+                mode: BindingMode.OneWay,
+                targetProperty: TargetClass.StringProperty,
+                targetTypeConverter: TargetTypeConverter.GetReflectionConverter());
+
+            // These should not throw
+            var leafNode = bindingExpression.LeafNode;
+            var description = bindingExpression.Description;
+
+            // LeafNode should be null when there are no nodes
+            Assert.Null(leafNode);
+        }
+    }
 }
 }

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

@@ -451,7 +451,7 @@ public abstract partial class BindingExpressionTests
                 return value;
                 return value;
 
 
             var s = value?.ToString() ?? string.Empty;
             var s = value?.ToString() ?? string.Empty;
-            
+
             if (s.StartsWith(prefix))
             if (s.StartsWith(prefix))
                 return s.Substring(prefix.Length);
                 return s.Substring(prefix.Length);
             else
             else