Repository navigation
Short-circuit the binding chain on a null-conditional operator - #22082
Conversation
The null-conditional operator in a binding path was only applied to the node it was attached to: a null source produced a null value which was then passed to the next node in the chain, which raised "Value is null.". C# instead short-circuits the remainder of the expression, so `a?.b.c` evaluates to null when `a` is null. Do the same for binding paths. When a null-conditional node has a null source it now sets its own value and that of all subsequent nodes to null, and the binding publishes null rather than an error. Publishing null rather than UnsetValue means TargetNullValue still applies. Attached properties in reflection bindings never honoured the operator at all: the grammar parses `?.(Foo.Bar)` and sets AttachedPropertyNameNode.AcceptsNull, but ExpressionNodeFactory discarded the flag and AvaloniaPropertyAccessorNode had no way to accept it. Pass it through. Compiled bindings were unaffected as they route attached properties through PropertyAccessorNode. Fixes #18949. Co-Authored-By: Claude Opus 5Claude-Session: https://claude.ai/code/session_01Atuuu4jtp14QXXoCzkp2A6
There was a problem hiding this comment.
Pull request overview
This pull request updates Avalonia’s binding-path evaluation so the null-conditional operator (?.) short-circuits the remainder of the binding chain (matching C# semantics), preventing misleading “Value is null.” errors and allowing TargetNullValue to apply as expected. It also fixes ?. handling for attached properties in reflection bindings.
Changes:
- Add null-short-circuit propagation to
ExpressionNode/BindingExpression, publishingnullwithout evaluating later nodes. - Update CLR/attached property accessor nodes to invoke the new short-circuit behavior when
AcceptsNullis set and the source isnull. - Add unit tests covering short-circuiting for deeper CLR paths, stream (
^) usage, and attached properties.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Avalonia.Base.UnitTests/Data/Core/NullConditionalBindingTests.cs | Adds coverage for null-conditional short-circuiting across CLR paths, stream operator usage, and attached properties. |
| src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs | Preserves/passes AcceptsNull into the attached-property accessor node construction. |
| src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs | Uses short-circuit behavior instead of SetValue(null) when null-conditional is present and source is null. |
| src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs | Uses short-circuit behavior instead of SetValue(null) when null-conditional is present and source is null. |
| src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs | Introduces short-circuit APIs (ShortCircuitNull, PropagateNullShortCircuitValue) to stop evaluation past ?.. |
| src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs | Adds acceptsNull support and triggers short-circuiting for attached properties when the source is null. |
| src/Avalonia.Base/Data/Core/BindingExpression.cs | Implements OnNodeNullShortCircuit to unsubscribe subsequent nodes and publish null as the binding value. |
Suppressed comments (1)
tests/Avalonia.Base.UnitTests/Data/Core/NullConditionalBindingTests.cs:117
- Test name ends with "_2", which doesn’t convey what scenario differs from the other CLR null-conditional test. Renaming to a descriptive name will make failures easier to interpret.
public void Should_Not_Report_Error_With_Null_Conditional_Operator_For_Clr_Property_2(bool compileBindings)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
You can test this PR using the following package version. |
|
Yes I can see chaining binding path is fixed, the direct binding to nullable observables still reports error. this PR works as expected. |
MrJul
left a comment
There was a problem hiding this comment.
Extending the Should_Use_TargetNullValue... tests for A?.B.C would be nice, since TargetNullValue is now used instead of FallbackValue.
Aside from that, this looks good. The implementation is much simpler than I first expected.
Covers `A?.B.C` where A is null, for both CLR and Avalonia properties. Co-Authored-By: Claude Opus 5 (1M context)Claude-Session: https://claude.ai/code/session_014aahFHMmgxZtZ5EH3H5Xcc
|
@MrJul tests added! |
|
You can test this PR using the following package version. |
…niaUI#22082) * Add failing test for issue described in AvaloniaUI#22069. AvaloniaUI#22069 (comment) * Add failing test for AvaloniaUI#18949. * Add failing tests for null conditional on attached property. * Short-circuit the binding chain on a null-conditional operator. The null-conditional operator in a binding path was only applied to the node it was attached to: a null source produced a null value which was then passed to the next node in the chain, which raised "Value is null.". C# instead short-circuits the remainder of the expression, so `a?.b.c` evaluates to null when `a` is null. Do the same for binding paths. When a null-conditional node has a null source it now sets its own value and that of all subsequent nodes to null, and the binding publishes null rather than an error. Publishing null rather than UnsetValue means TargetNullValue still applies. Attached properties in reflection bindings never honoured the operator at all: the grammar parses `?.(Foo.Bar)` and sets AttachedPropertyNameNode.AcceptsNull, but ExpressionNodeFactory discarded the flag and AvaloniaPropertyAccessorNode had no way to accept it. Pass it through. Compiled bindings were unaffected as they route attached properties through PropertyAccessorNode. Fixes AvaloniaUI#18949. Co-Authored-By: Claude Opus 5Claude-Session: https://claude.ai/code/session_01Atuuu4jtp14QXXoCzkp2A6 * Add TargetNullValue tests for short-circuited chains. Covers `A?.B.C` where A is null, for both CLR and Avalonia properties. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014aahFHMmgxZtZ5EH3H5Xcc --------- Co-authored-by: Claude Opus 5
) Follow-up to AvaloniaUI#22082, which added the short-circuit to the property accessor nodes. The cast nodes still called ValidateNonNullSource, so a cast reported "Value is null" before the following ?. could suppress it. Casting null now produces null, as in C#, leaving any error to the member access that follows.
) Follow-up to AvaloniaUI#22082, which added the short-circuit to the property accessor nodes. The cast nodes still called ValidateNonNullSource, so a cast reported "Value is null" before the following ?. could suppress it. Casting null now produces null, as in C#, leaving any error to the member access that follows.
What does the pull request do?
Makes the null-conditional operator in a binding path short-circuit the rest of the chain, as it does in C#.
This came out of the discussion in #22069, which proposed a new
?^operator for null tasks/observables. The underlying problem there turned out to be the same one already reported in #18949:?.doesn't short-circuit, so any node after it still sees a null and reports an error.What is the current behavior?
The null-conditional operator only applies to the node it's attached to. A null source produces a null value, which is then handed to the next node in the chain, which raises
Value is null..Given the viewmodel from #18949, where
Modelis null andInfois never null:reports:
Infois never null, so the error names a confusing node. The binding then falls back toFallbackValueinstead of usingTargetNullValue.The same thing happens with a stream operator, which is the case from #22069:
errors at
TaskwhenSecondis null.Separately, attached properties in reflection bindings never honoured the operator at all.
{Binding Second?.(Grid.Row)}reportsValue is null.even though the grammar parses the?.correctly.What is the updated/expected behavior with this PR?
a?.b.cevaluates to null whenais null, and no error is reported. This matches C#, where the null-conditional operator short-circuits the remainder of the expression.Because the short-circuited chain publishes null rather than
UnsetValue,TargetNullValueapplies as one would expect.Note this does not make a null value at the stream operator legal:
Second?.Task^still errors ifSecondis non-null andTaskis null, in the same way that C# won't let you await a null task. The?.guardsSecondonly.How was the solution implemented (if it's not obvious)?
ExpressionNodegains two methods:ShortCircuitNull(), called by a node with a null-conditional operator when its source is null.PropagateNullShortCircuitValue(), called on each node after it.The first notifies the owning
BindingExpressionvia the newOnNodeNullShortCircuit, which unsubscribes every subsequent node, sets their values to null, and publishes null as the value of the binding.The three property accessor nodes then call
ShortCircuitNull()instead ofSetValue(null)on a null source:PropertyAccessorNode(compiled bindings)DynamicPluginPropertyAccessorNode(reflection bindings)AvaloniaPropertyAccessorNode(attached properties in reflection bindings)The last of those had no
acceptsNullat all. The grammar parses?.(Foo.Bar)and setsAttachedPropertyNameNode.AcceptsNull— there's an existing grammar test for it — butExpressionNodeFactorydiscarded the flag. It's now passed through. Compiled bindings were already correct here as they route attached properties throughPropertyAccessorNode.No other node has a null-conditional form to honour:
^has no?^, and the indexer nodes have no?[]because the grammar doesn't parse one.The tests were added as separate commits ahead of the fix, so they can be checked out to see the failures.
Checklist
Breaking changes
A binding path that previously produced a binding error will now produce a null value, if the path contains a null-conditional operator before the point at which the null was encountered. Any binding relying on
FallbackValuebeing applied in that case will now getTargetNullValue(or null) instead.Obsoletions / Deprecations
None.
Fixed issues
Fixes #18949
🤖 Generated with Claude Code
https://claude.ai/code/session_01Atuuu4jtp14QXXoCzkp2A6