Skip to content

Short-circuit the binding chain on a null-conditional operator - #22082

Merged
MrJul merged 6 commits into
mainfrom
fixes/22069-null-conditional-with-stream-operator
Aug 29, 2026
Merged

MrJul merged 6 commits into
mainfrom
fixes/22069-null-conditional-with-stream-operator

Conversation

@grokys

@grokys grokys commented Aug 27, 2026 •

Copy link
Copy Markdown
Member

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 Model is null and Info is never null:

<TextBlock Text="{Binding Model?.Info.Name, TargetNullValue=Unknown}" />

reports:

An error occurred binding 'Text' to 'Model.Info.Name' at 'Info': 'Value is null.'

Info is never null, so the error names a confusing node. The binding then falls back to FallbackValue instead of using TargetNullValue.

The same thing happens with a stream operator, which is the case from #22069:

<TextBox Text="{Binding Second?.Task^}" />

errors at Task when Second is null.

Separately, attached properties in reflection bindings never honoured the operator at all. {Binding Second?.(Grid.Row)} reports Value is null. even though the grammar parses the ?. correctly.

What is the updated/expected behavior with this PR?

a?.b.c evaluates to null when a is 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, TargetNullValue applies as one would expect.

Note this does not make a null value at the stream operator legal: Second?.Task^ still errors if Second is non-null and Task is null, in the same way that C# won't let you await a null task. The ?. guards Second only.

How was the solution implemented (if it's not obvious)?

ExpressionNode gains 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 BindingExpression via the new OnNodeNullShortCircuit, 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 of SetValue(null) on a null source:

  • PropertyAccessorNode (compiled bindings)
  • DynamicPluginPropertyAccessorNode (reflection bindings)
  • AvaloniaPropertyAccessorNode (attached properties in reflection bindings)

The last of those had no acceptsNull at all. The grammar parses ?.(Foo.Bar) and sets AttachedPropertyNameNode.AcceptsNull — there's an existing grammar test for it — but ExpressionNodeFactory discarded the flag. It's now passed through. Compiled bindings were already correct here as they route attached properties through PropertyAccessorNode.

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 FallbackValue being applied in that case will now get TargetNullValue (or null) instead.

Obsoletions / Deprecations

None.

Fixed issues

Fixes #18949

🤖 Generated with Claude Code

https://claude.ai/code/session_01Atuuu4jtp14QXXoCzkp2A6

grokys and others added 4 commits August 27, 2026 22:21
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 5 
Claude-Session: https://claude.ai/code/session_01Atuuu4jtp14QXXoCzkp2A6
@grokys
grokys requested a lite review from Copilot August 27, 2026 21:14
@MrJul MrJul added bug area-bindings backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Aug 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, publishing null without evaluating later nodes.
  • Update CLR/attached property accessor nodes to invoke the new short-circuit behavior when AcceptsNull is set and the source is null.
  • 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.

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0069039-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@rabbitism

Copy link
Copy Markdown
Contributor

Yes I can see chaining binding path is fixed, the direct binding to nullable observables still reports error. this PR works as expected.

MrJul
MrJul previously approved these changes Aug 28, 2026

@MrJul MrJul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@grokys

grokys commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

@MrJul tests added!

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0069119-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul
MrJul added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit 7527557 Aug 29, 2026
10 checks passed
@MrJul
MrJul deleted the fixes/22069-null-conditional-with-stream-operator branch August 29, 2026 09:18
MrJul pushed a commit to MrJul/Avalonia that referenced this pull request Sep 2, 2026
…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 5 
Claude-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 
@MrJul MrJul added backported-12.1.x and removed backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Sep 2, 2026
MrJul pushed a commit to MrJul/Avalonia that referenced this pull request Sep 22, 2026
)

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.
grokys pushed a commit to Evan260/Avalonia that referenced this pull request Oct 7, 2026
)

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Null-conditional operator in bindings doesn't behave as expected for deeply nested paths

5 participants