From 5d3a9c167a96f9b60c85e160971b2f330c898c7a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Jan 2026 14:43:55 +0100 Subject: [PATCH] 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 --- .../Data/Core/BindingExpression.cs | 9 +++--- .../Core/BindingExpressionTests.GetValue.cs | 28 +++++++++++++++++++ .../Data/Core/BindingExpressionTests.cs | 2 +- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index dc55184404..6ad1a99f41 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -138,7 +138,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I get { var b = new StringBuilder(); - LeafNode.BuildString(b, _nodes); + LeafNode?.BuildString(b, _nodes); return b.ToString(); } } @@ -149,7 +149,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture; public object? ConverterParameter => _uncommon?._converterParameter; 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 object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue; 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 // 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; try @@ -515,7 +515,8 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I if (TryGetTarget(out var target) && TargetProperty is not null && target.GetValue(TargetProperty) is var value && - !TypeUtilities.IdentityEquals(value, LeafNode.Value, TargetType)) + LeafNode is { } leafNode && + !TypeUtilities.IdentityEquals(value, leafNode.Value, TargetType)) { WriteValueToSource(value); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs index 789df31a77..c37c00767b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs @@ -1,5 +1,8 @@ using System; using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.UnitTests; using Xunit; #nullable enable @@ -163,4 +166,29 @@ public abstract partial class BindingExpressionTests 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); + } + } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 15b414c407..251be3fc27 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -451,7 +451,7 @@ public abstract partial class BindingExpressionTests return value; var s = value?.ToString() ?? string.Empty; - + if (s.StartsWith(prefix)) return s.Substring(prefix.Length); else