diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index ca29dd6ffa..2847af4b08 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -138,7 +138,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri get { var b = new StringBuilder(); - LeafNode.BuildString(b, _nodes); + LeafNode?.BuildString(b, _nodes); return b.ToString(); } } @@ -149,7 +149,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri 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; @@ -357,7 +357,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri // 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 @@ -512,7 +512,8 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri 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 559b4d76c0..292307c053 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