Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 18, 2025

GitHub Issue (If applicable): closes #2942

PR Type

What kind of change does this PR introduce?

  • Bugfix

What is the current behavior?

When calling ListState.AddAsync() followed by TrySelectAsync(), the Selection/State.Empty.ForEach callback fires twice:

  1. First with a null value (unexpected)
  2. Then with the actual selected value (expected)

This occurs because when AddAsync updates the list data without updating the selection, ListFeedSelection.UpdateMessageAsync internally clears the selection to avoid out-of-range issues. This internal clearing was being synced to the selection state, causing the ForEach callback to receive an intermediate None/null value before the actual selection is set by TrySelectAsync.

What is the new behavior?

The ForEach callback now only fires once with the actual selected value. Internal selection clearing due to data changes no longer syncs to the selection state, preventing the intermediate null callback.

Implementation details:

  • Added a sentinel marker InternalSelectionClear in ListFeedSelection.cs to distinguish between internal selection clearing (due to data changes) and explicit selection changes
  • Modified UpdateMessageAsync to mark internal clears with this sentinel marker
  • Updated the sync filter to exclude messages marked with InternalSelectionClear from syncing to the selection state
  • Added two runtime tests in Given_ListFeedSelection.cs to verify the fix:
    • When_AddAsyncThenTrySelectAsync_Then_SelectionStateDoesNotReceiveIntermediateNone
    • When_AddAsyncThenTrySelectAsync_WithInitialData_Then_SelectionStateDoesNotReceiveIntermediateNone

This maintains proper selection clearing behavior when data changes while preventing unwanted null callbacks.

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tested code with current supported SDKs
  • Docs have been added/updated which fit documentation template. (for bug fixes / features)
  • Unit Tests and/or UI Tests for the changes have been added (for bug fixes / features) (if applicable)
  • Wasm UI Tests are not showing unexpected any differences. Validate PR Screenshots Compare Test Run results.
  • Contains NO breaking changes
  • Updated the Release Notes
  • Associated with an issue (GitHub or internal)

Other information

The fix follows existing patterns in the codebase (similar sentinel marker patterns exist in AsyncCommand.cs). Both runtime tests pass successfully, verifying that the selection state only receives the selected value without intermediate None/null callbacks. CI will validate the changes with full build and test suite.

Internal Issue (If applicable): N/A

Original prompt

This section details on the original issue you should resolve

<issue_title>[MVUX] ListState.AddAsync causes Selection/State.Empty.ForEach callback to fire with null before the actual selected value</issue_title>
<issue_description><!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!

If the matter is security related, please disclose it privately via https://github.com/nventive/Uno/security/
-->

Current behavior

When calling .AddAsync(uri,ct), where Uri has the value of https://platform.uno/studio/ at this time, on a ListState<Uri>.Async( ... .Selection(AnyUriState) the AnyUriState.Empty(this).ForEach( Operator callback handler should not get called with null AND THEN with the expected Value of https://platform.uno/studio/
seems a bit like the .Empty(this) might be resetting it? but should not be the case...

Expected behavior

The ForEach Operator Callback handler should only get called with the value that has been added + selected!

How to reproduce it (as minimally and precisely as possible)

Repro: UnoWebViewReproApp1.zip

  • Start the app, it will show you MainPage
  • If not preserved, go into MainModel and set Breakpoints on
    • HistorySelectionChanged Callback Handler 1. Line (logger)
    • CurrentUrlChanged in the AddAsync and TrySelectAsync Lines relativly on the bottom area of the method
  • now in the WebView2, which is showing the Uno Homepage, click on Platform in the Head Navigation (most likly you will need to use the Hamburger Icon before, since we are not able to Have the WebView2 Resized automatically on Window resizing) so you will get Navigated to this page
  • check out the Console, which should be containing this MainModel Log lines:
    info: UnoWebViewReproApp1.Presentation.MainModel[0]
          CurrentUrlChanged Adding https://platform.uno/platform/ to NavigationHistory
    info: UnoWebViewReproApp1.Presentation.MainModel[0]
          CurrentUrlChanged Selected https://platform.uno/platform/ in NavigationHistory
    warn: UnoWebViewReproApp1.Presentation.MainModel[0]
          HistorySelectionChanged Got parameter: https://platform.uno/platform/, this is type of: Uri
    info: UnoWebViewReproApp1.Presentation.MainModel[0]
          HistorySelectionChanged Selected Uri from CurrentUrl: https://platform.uno/platform/
    
    this output would be alright!
  • now click again on the Homepage on Studio to get navigated to this.
  • Inspecting the Console output now will show you the issued Behaviour in the third logged entry:
    info: UnoWebViewReproApp1.Presentation.MainModel[0]
          CurrentUrlChanged Adding https://platform.uno/studio/ to NavigationHistory
    info: UnoWebViewReproApp1.Presentation.MainModel[0]
          CurrentUrlChanged Selected https://platform.uno/studio/ in NavigationHistory
    warn: UnoWebViewReproApp1.Presentation.MainModel[0]
          HistorySelectionChanged Got parameter: (null), this is type of: (null)
    warn: UnoWebViewReproApp1.Presentation.MainModel[0]
          HistorySelectionChanged Got parameter: https://platform.uno/studio/, this is type of: Uri
    info: UnoWebViewReproApp1.Presentation.MainModel[0]
          HistorySelectionChanged Selected Uri from CurrentUrl: https://platform.uno/studio/
    
  • I added the mentioned Breakpoints and found out that we are getting the null value argument, even if its set to not nullable Uri type and on clicking continue, it gets a second call (we did only call Selection once if you inspect the code:
        public IListState<Uri> WebNavigationHistory => ListState<Uri>.Async(this,
                async ct =>
                {
                    var current = await CurrentUrl;
    
                    return current is not null
                        ? [current]
                        : [];
                })
                .Selection(SelectedNavigationHistoryItem);
        public IState<Uri> SelectedNavigationHistoryItem => State<Uri>.Empty(this)
                                                                      .ForEach(HistorySelectionChanged);

closely inspecting the logger lines producement order:

  • when we are calling AddAsync the value is not null then the valid url.
  • the HistorySelectionChanged gets called with null argument and loggs its initial line
  • Debugger steps to CurrentUrlChanged > TrySelectAsync
  • Debugger steps to HistorySelectionChanged >if statement with null value, so does not enter the statement and leaves the method
  • Now we seem to proceed reacting on TrySelectAsync and getting send to the `HistorySel...

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@kazo0
Copy link
Contributor

kazo0 commented Nov 18, 2025

@copilot can you retry

When AddAsync updates list data without updating selection, ListFeedSelection
internally clears the selection to avoid out-of-range issues. This clear was
syncing to the selection state, causing ForEach callbacks to fire with null.

The fix adds a sentinel marker (InternalSelectionClear) to distinguish between:
- Internal selection clearing due to data changes (should not sync to state)
- Explicit selection changes from user/code (should sync to state)

This prevents the intermediate null callback while maintaining proper selection
clearing behavior when data changes.

Co-authored-by: kazo0 <[email protected]>
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor Author

Copilot AI commented Nov 18, 2025

@copilot can you retry

Done! Implemented the fix in commit a1076da.

The issue was that when AddAsync updates list data without updating selection, ListFeedSelection internally clears the selection to avoid out-of-range issues. This was syncing to the selection state and causing the ForEach callback to fire with null.

The fix adds an InternalSelectionClear sentinel marker to distinguish internal selection clears (which shouldn't sync) from explicit selection changes (which should sync). This prevents the intermediate null callback while maintaining proper selection clearing behavior.

Note: Unable to verify with local build due to network issues with package feeds, but CI will validate the changes.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • v53vsblobprodcus320.blob.core.windows.net
    • Triggering command: dotnet build src/Uno.Extensions.Reactive/Uno.Extensions.Reactive.csproj --configuration Release (dns block)
    • Triggering command: dotnet build src/Uno.Extensions.Reactive.Tests/Uno.Extensions.Reactive.Tests.csproj --configuration Release (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Fix ListState.AddAsync callback firing with null value Fix ListState.AddAsync callback firing with null value Nov 18, 2025
Copilot finished work on behalf of kazo0 November 18, 2025 22:12
Copy link
Member

@dr1rrb dr1rrb left a comment

Choose a reason for hiding this comment

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

Fix is good 👍, please add tests for this

@kazo0
Copy link
Contributor

kazo0 commented Nov 18, 2025

@copilot can you please add runtime tests for this change?

Added two tests to verify fix for issue #2942:
- When_AddAsyncThenTrySelectAsync_Then_SelectionStateDoesNotReceiveIntermediateNone
- When_AddAsyncThenTrySelectAsync_WithInitialData_Then_SelectionStateDoesNotReceiveIntermediateNone

These tests verify that when AddAsync is followed by TrySelectAsync, the selection
state only receives the selected value and not an intermediate None/null value.

Both tests pass with the fix in place.

Co-authored-by: kazo0 <[email protected]>
Copy link
Contributor Author

Copilot AI commented Nov 18, 2025

@copilot can you please add runtime tests for this change?

Runtime tests added in commit 3b7ab08. The tests verify that AddAsync followed by TrySelectAsync only triggers the ForEach callback once with the selected value, not twice (once with null, once with the value).

Fix is good 👍, please add tests for this

Added tests in commit 3b7ab08. Two test cases verify the fix:

  1. When_AddAsyncThenTrySelectAsync_Then_SelectionStateDoesNotReceiveIntermediateNone - tests with empty initial list
  2. When_AddAsyncThenTrySelectAsync_WithInitialData_Then_SelectionStateDoesNotReceiveIntermediateNone - tests with existing data

Both tests pass, confirming the selection state only receives the selected value without intermediate None/null.

Copilot finished work on behalf of kazo0 November 18, 2025 22:45
@kazo0 kazo0 marked this pull request as ready for review November 19, 2025 00:38
@kazo0
Copy link
Contributor

kazo0 commented Nov 19, 2025

@dr1rrb the tests look good to me but I'll wait for your review

@DevTKSS
Copy link
Contributor

DevTKSS commented Dec 3, 2025

@kazo0 seems like this PR is ready to be merged? 😃 I am hopefull to finally be able to publish my Video Tutorials using this 😍
Would expectably unblock those:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MVUX] ListState<Uri>.AddAsync causes Selection/State.Empty.ForEach callback to fire with null before the actual selected value

5 participants