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 <noreply@anthropic.com>
pull/20448/head
Steven Kirk 4 weeks ago
committed by GitHub
parent
commit
5d3a9c167a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  2. 28
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.GetValue.cs
  3. 2
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

9
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);
}

28
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);
}
}
}

2
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

Loading…
Cancel
Save