Skip to content

Support nullable stream binding operator (?^) - #22069

Closed
rabbitism wants to merge 1 commit into
AvaloniaUI:mainfrom
irihitech:fix/stream-binding-v2
Closed

Support nullable stream binding operator (?^)#22069
rabbitism wants to merge 1 commit into
AvaloniaUI:mainfrom
irihitech:fix/stream-binding-v2

Conversation

@rabbitism

Copy link
Copy Markdown
Contributor

Previously {Binding Header^} raised 'Value is null.' when the bound IObservable/Task was null. Add support for the '?^' operator so that a null source is propagated as a null value instead of an error.

  • BindingExpressionGrammar: parse '?^' and set StreamNode.AcceptsNull
  • StreamNode/DynamicPluginStreamNode: handle null source when acceptsNull is set
  • CompiledBindingPathBuilder: add acceptsNull overloads for StreamTask/StreamObservable
  • XamlIlBindingPathHelper: emit acceptsNull argument for compiled bindings
  • Add grammar, reflection and compiled binding tests

What does the pull request do?

What is the current behavior?

What is the updated/expected behavior with this PR?

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

Checklist

Breaking changes

Obsoletions / Deprecations

Fixed issues

Previously {Binding Header^} raised 'Value is null.' when the bound
IObservable<T>/Task was null. Add support for the '?^' operator so
that a null source is propagated as a null value instead of an error.

- BindingExpressionGrammar: parse '?^' and set StreamNode.AcceptsNull
- StreamNode/DynamicPluginStreamNode: handle null source when
  acceptsNull is set
- CompiledBindingPathBuilder: add acceptsNull overloads for
  StreamTask/StreamObservable
- XamlIlBindingPathHelper: emit acceptsNull argument for compiled
  bindings
- Add grammar, reflection and compiled binding tests
@avaloniaui-bot

Copy link
Copy Markdown

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

@MrJul MrJul added enhancement area-xaml area-bindings backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Aug 26, 2026
@grokys grokys self-assigned this Aug 27, 2026
@grokys

grokys commented Aug 27, 2026

Copy link
Copy Markdown
Member

Before I review this, are we sure that this is something we want? C# has no equivalent - for example you can't await a null task or observable. C# does have the ?. operator on the other hand.

We also don't support ? before array access currently IIRC (C# does have the ?[] operator).

Could we get an example of some use-cases for this feature?

(Note that I'm not against this, just trying to understand)

@rabbitism

rabbitism commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

My stream binding requests mostly from the real-world use case with Lingua https://github.com/irihitech/Irihi.Lingua.

We discovered this when building ursa demos. This is merged to main branch, you can try to build, check AutoCompleteBox page and navigate away.

image

This happens when I navigate away from this page, the data context is uninstalled firstly before binding evaluation stops. so it triggers an null error.

we don't have await? in C# yet (actually proposed for so long), but we can anyway use Task?.GetAwaiter().GetResult().

@grokys

grokys commented Aug 27, 2026

Copy link
Copy Markdown
Member

Hmm, OK, I need to investigate that. I'm a little rusty on exactly how the binding system works, but I don't think we should be logging errors when the data context changes. The nullable operator should in theory only be needed when the actual property is null (again, need to re-familiarize myself with it all so I may be mistaken).

@rabbitism

Copy link
Copy Markdown
Contributor Author

I get your point, but maybe skipping null task/observable should just be the default behavior?

@grokys

grokys commented Aug 27, 2026

Copy link
Copy Markdown
Member

but maybe skipping null task/observable should just be the default behavior?

But your observable/task properties don't actually return null right? I couldn't find the code from your screenshot (DemoSectionView.axaml) in https://github.com/irihitech/Irihi.Lingua.

Assuming that those properties don't return null, then I don't think that we do need to ignore null tasks/observables, no.

As far as I can see, this is a bug caused by ?. not correctly sort-circuiting the binding chain in the presence of a task/stream. I'm looking into fixing that right now.

@grokys

grokys commented Aug 27, 2026

Copy link
Copy Markdown
Member

I've opened #22082 to fix the issue (which was the same root cause as #18949).

Let me know if this fixes your issue. Whether to support ?^ is a separate question so I'll leave this PR open for now (though I'm not convinced it's needed).

@ptasev

ptasev commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Before I review this, are we sure that this is something we want? C# has no equivalent - for example you can't await a null task or observable. C# does have the ?. operator on the other hand.

C# null-conditional await may be coming in .NET 12.
https://github.com/dotnet/roslyn/blob/2a216e90ea4e495b368abf9c61b370c4af5979ce/docs/Language%20Feature%20Status.md

dotnet/roslyn#83237

@grokys

grokys commented Aug 28, 2026

Copy link
Copy Markdown
Member

the direct binding to nullable observables

Can you point me to where you have these in your code? I couldn't find them in the link you gave previously.

@grokys

grokys commented Aug 28, 2026

Copy link
Copy Markdown
Member

C# null-conditional await may be coming in .NET 12.

Yes but the proposal itself says:

This feature is not intended to encourage code that returns a null task from async-shaped methods. We are not trying to make patterns like Task DoSomethingAsync() { return null; } easier to consume; returning a null task from such a method would remain discouraged.

Our existing ?. operator should handle the case that C#'s null-conditional await is intended to solve (once #22082 is merged).

@rabbitism

Copy link
Copy Markdown
Contributor Author

the direct binding to nullable observables

Can you point me to where you have these in your code? I couldn't find them in the link you gave previously.

Oops it's in ursa demo https://github.com/irihitech/Ursa.Avalonia/blob/main/demo/Ursa.Demo/Controls/DemoSectionView.axaml

appel1 pushed a commit to appel1/Avalonia that referenced this pull request Aug 29, 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 <noreply@anthropic.com>
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aahFHMmgxZtZ5EH3H5Xcc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@grokys

grokys commented Aug 31, 2026

Copy link
Copy Markdown
Member

@rabbitism I've taken a look at the code, and honestly - I don't think this serves as a good argument for adding ?^:

By exposing a nullable IObservable<string?>? you have two independent null layers with different meanings: the observable being null ("no stream") vs. the stream emitting null ("no text right now"). This is similar to exposing a nullable enumerable e.g. IEnumerable<string?>? which is also strongly discouraged in .NET.

But the main issue with the code is that it's mixing two different notification systems: System.Reactive and Avalonia Properties - where a single mechanism would suffice:

  1. To use Rx you'd use the Switch operator:
tagChanges
      .Select(tag => tag switch
      {
          DemoSectionTag.Function => LanguageManager.Instance.DemoSection_Tag_Function,
          DemoSectionTag.Style    => LanguageManager.Instance.DemoSection_Tag_Style,
          DemoSectionTag.Others   => LanguageManager.Instance.DemoSection_Tag_Others,
          _                       => Observable.Return<string?>(null),
      })
      .Switch();
  1. Otherwise you would subscribe to the observable and update the Avalonia Property with each change. (Note that you should be using a direct property for this, because styled properties are writeable and styleable even if the CLR setter is private).

You'd have a problem trying to do this even with C#: Rx won't work all the time because the property is nullable, and GetObservable() would need a Switch anyway to multiplex the different observables exposed by the property.

What I'd say is: ?^ has nothing to do here. The nullability that ?^ exists to paper over is self-inflicted by the anti-pattern. And if you really do want to use the anti-pattern, nothing stops working - all you get are runtime warnings.

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 <noreply@anthropic.com>
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aahFHMmgxZtZ5EH3H5Xcc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@MrJul

MrJul commented Sep 4, 2026

Copy link
Copy Markdown
Member

I'm in full agreement with @grokys here: this is an anti-pattern, and we should avoid adding a feature that enables it - without blocking it. Especially since the "main" case is now fixed with ?. short-circuiting.

Thus, I'm going to close this PR.

@MrJul MrJul closed this Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-bindings area-xaml backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants