Skip to content

[Android] CV RemainingItemsThresholdReachedCommand fires on initial data load - fix#28321

Closed
kubaflo wants to merge 205 commits intodotnet:inflight/currentfrom
kubaflo:fix-28279
Closed

[Android] CV RemainingItemsThresholdReachedCommand fires on initial data load - fix#28321
kubaflo wants to merge 205 commits intodotnet:inflight/currentfrom
kubaflo:fix-28279

Conversation

@kubaflo
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo commented Mar 11, 2025

Issues Fixed

Fixes #28279

Before After iOS
Screen.Recording.2025-03-11.at.18.24.14.mov
Screen.Recording.2025-03-11.at.18.23.26.mov
Screen.Recording.2025-03-11.at.18.25.29.mov
<Grid RowDefinitions="Auto,*">
        <Label>
            <Label.FormattedText>
                <FormattedString>
                    <Span Text="Loaded items: "/>
                    <Span Text="{Binding Items.Count}"/>
                </FormattedString>
            </Label.FormattedText>
        </Label>
        <CollectionView
            Grid.Row="1"
            RemainingItemsThresholdReachedCommand="{Binding LoadMoreItemsCommand}"
            RemainingItemsThreshold="0"
            ItemsSource="{Binding Items}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Label Text="{Binding .}"
                           HeightRequest="100"
                           BackgroundColor="Green"/>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
public class MainPageViewModel : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;

	protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
	{
		PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
	}

	public ObservableCollection<string> Items { get; set; }
	public Command LoadMoreItemsCommand { get; }
	public MainPageViewModel()
	{
		LoadMoreItemsCommand = new Command(() =>
		{
			int size = Items.Count;
			for (int i = size; i < size + 10; i++)
				Items.Add($"Item{i}");
		});
		_ = LoadItems();
	}

	async Task LoadItems()
	{
		await Task.Delay(1000);
		Items = new ObservableCollection<string>(Enumerable.Range(0, 4).Select(x => $"Item{x}").ToList());
		OnPropertyChanged(nameof(Items));
	}
}

@kubaflo kubaflo requested a review from a team as a code owner March 11, 2025 17:31
@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Mar 11, 2025
@jsuarezruiz
Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@jsuarezruiz jsuarezruiz added platform/android area-controls-collectionview CollectionView, CarouselView, IndicatorView labels Mar 12, 2025
Copy link
Copy Markdown
Contributor

@jsuarezruiz jsuarezruiz left a comment

Choose a reason for hiding this comment

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

@kubaflo kubaflo self-assigned this Mar 31, 2025
@kubaflo
Copy link
Copy Markdown
Contributor Author

kubaflo commented Mar 31, 2025

Could you include a test based on https://github.com/DharunAR/MAUICollectionView?

Done

@kubaflo
Copy link
Copy Markdown
Contributor Author

kubaflo commented Jan 28, 2026

🤖 AI Summary

🚦 Test Verification

Result: ✅ PASSED
Mode: Failure Only
Platform: ANDROID

Expand Details

Test Verification Report

Date: 2026-01-28 15:19:07 | Platform: ANDROID | Status: ✅ PASSED

Summary

Check Expected Actual Result
Tests WITHOUT fix FAIL FAIL
Tests WITH fix PASS PASS

✅ Final Verdict

VERIFICATION PASSED

The tests correctly detect the issue:

  • ✅ Tests FAIL without the fix (as expected - bug is present)
  • ✅ Tests PASS with the fix (as expected - bug is fixed)

Conclusion: The tests properly validate the fix and catch the bug when it's present.


Configuration

Platform: android
Test Filter: Issue28321
Mode: FullVerification

Fix Files

  • src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs

Test Results Details

Test Run 1: WITHOUT Fix

Expected: Tests should FAIL (bug is present)
Actual: Tests FAILED ✅

Test Run 2: WITH Fix

Expected: Tests should PASS (bug is fixed)
Actual: Tests PASSED ✅

Test: RemainingItemsThresholdReachedCommandShouldNotFireOnInitialDataLoad - Passed in 2s

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an Android CollectionView regression where RemainingItemsThresholdReachedCommand can fire during initial layout/data load instead of only after user-initiated scrolling.

Changes:

  • Gate RemainingItemsThresholdReached dispatch on Android behind an actual scroll-state transition (non-idle).
  • Add a new HostApp issue page reproducing the scenario with delayed initial item load.
  • Add an Appium UI test asserting the command does not fire on initial data load.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

File Description
src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs Tracks scroll state and prevents threshold events firing before scrolling begins.
src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml Adds a repro UI for the delayed-load + threshold command scenario.
src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml.cs Adds the repro page code-behind and view model to load items and append on threshold command.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28321.cs Adds a UI test validating the initial item count remains unchanged (command not invoked).

[Issue(IssueTracker.Github, 28321, "CV RemainingItemsThresholdReachedCommand fires on initial data load", PlatformAffected.Android)]
public partial class Issue28321 : ContentPage
{
public readonly record struct Data(string Text, string AutomationId);
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Data record struct is declared but never used. Please remove it to avoid dead code/noise in this new test page.

Suggested change
public readonly record struct Data(string Text, string AutomationId);

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +5
x:Class="Maui.Controls.Sample.Issues.Issue28321"
Title="Issue28321">
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The XAML page is named/title’d Issue28321, but this PR is intended to fix issue #28279 per the PR title/description. Please rename the page/class to match the actual issue number so the UI test infrastructure navigates to the correct page.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +10
public class Issue28321 : _IssuesUITest
{
public Issue28321(TestDevice device) : base(device) { }

public override string Issue => "CV RemainingItemsThresholdReachedCommand fires on initial data load";
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

This new UI test class/file is named Issue28321, but the PR/linked report is #28279. Please align the test name/issue number with the actual issue being fixed so the test clearly covers the reported regression (rename test class/file and corresponding HostApp page).

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +6
[Issue(IssueTracker.Github, 28321, "CV RemainingItemsThresholdReachedCommand fires on initial data load", PlatformAffected.Android)]
public partial class Issue28321 : ContentPage
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

PR metadata says this change fixes GitHub issue #28279, but this new HostApp issue page is numbered 28321 (file/class name and [Issue] attribute). Please align the issue number with the actual fixed issue so the test page is discoverable and traceable (rename file/class and update the [Issue] attribute accordingly).

Copilot uses AI. Check for mistakes.
@marcojak
Copy link
Copy Markdown

marcojak commented Feb 3, 2026

When should we expect to see this fixed? This is a very serious bug!

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 28321

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 28321"

Vignesh-SF3580 and others added 5 commits March 19, 2026 08:46
…ertPageBefore (dotnet#29924)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Detail
When using the InsertPageBefore method to change the root page in a
NavigationPage, the back button is not visible, but the flyout icon
remains visible.
 

### Root Cause
The flyout icon was not updated when changing the root page using
InsertPageBefore.
 

### Description of Change
The updated logic checks whether the inserted page becomes the new root,
updates the flyout icon. Previously, the flyout icon was updated based
on NavPageController.Pages. However, this collection was not updated
with the actual native navigation stack. Since pages are inserted
directly into ViewControllers in InsertPageBefore, the fix updates the
flyout icon based on the ViewControllers stack instead.

### Tested the behavior in the following platforms
 
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### Issues Fixed
 
Fixes dotnet#29921

### Screenshots

| Before Issue Fix | After Issue Fix |
|----------|----------|
| <video width="300" height="600"
src="https://github.com/user-attachments/assets/20acfd44-79af-45c7-ad9d-c55f2e825b49">
| <video width="300" height="600"
src="https://github.com/user-attachments/assets/fc99cc8b-8ed2-424f-876c-bd703e10872f">)
|
…is set to null (dotnet#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR dotnet#25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR dotnet#25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes dotnet#30363
Fixes dotnet#26187

### Regression PR

This fix addresses a regression introduced in PR dotnet#25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue dotnet#30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue dotnet#26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
…#33481)

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

### Description of Change
This pull request introduces support for a Material Design 3 styled
`ActivityIndicator` on Android, allowing the app to use the new Material
component when the Material3 feature is enabled. The main changes
involve conditional registration of a new handler, the implementation of
a `MaterialActivityIndicatorHandler`, and the addition of a custom
`MaterialActivityIndicator` control for Android.

**Material3 ActivityIndicator support for Android:**

* Added a new `MaterialActivityIndicatorHandler` class that extends
`ActivityIndicatorHandler` and creates a `MaterialActivityIndicator`
(the new Material3 control) as its platform view. It also customizes
layout behavior to ensure proper sizing and centering.
* Implemented the `MaterialActivityIndicator` control in
`MaterialActivityIndicator.cs`, inheriting from
`CircularProgressIndicator` and overriding measurement logic to ensure
the indicator remains square and properly sized according to Material
guidelines.

**Handler registration logic:**

* Updated the `AddControlsHandlers` extension method to conditionally
register either `MaterialActivityIndicatorHandler` or the classic
`ActivityIndicatorHandler` for `ActivityIndicator`, based on whether
Material3 is enabled on Android.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes dotnet#33479 

**Material3 Spec**
[ActivityIndicator](https://m3.material.io/components/progress-indicators/specs)
### Output Screenshot
| Material 2  | Material 3 |
|---------|--------|
| <video height=600 width=300
src="https://github.com/user-attachments/assets/23d67157-b19f-4b5d-b21d-4e50c45f288f">
|  <video height=600 width=300
src="https://github.com/user-attachments/assets/6a5de984-6407-4a5c-b60b-09e9fc16342a"> 
|
…dotnet#33246)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Background and Context

This PR addresses issue dotnet#33037 where iOS large titles in Shell don't
transition properly when scrolling. This is the third attempt to fix the
issue:

1. **PR dotnet#33039** (merged Dec 8, 2025): Changed the condition from
`PrefersLargeTitles = largeTitleDisplayMode == Always` to
`PrefersLargeTitles = largeTitleDisplayMode != Never`
2. **PR dotnet#33230** (merged Dec 19, 2025): Reverted dotnet#33039 because it
caused "a lot of failing tests" - large titles started appearing
unintentionally in many scenarios, breaking UI test snapshots
3. **PR dotnet#33246** (this PR): Re-attempts the fix with an important
addition: opt-in check

## Root Cause

Shell doesn't have a `PrefersLargeTitles` property at the Shell level.
The previous implementation would update large title preferences
whenever any page was displayed, regardless of whether the developer
explicitly requested large title behavior.

**Problem with PR dotnet#33039:** Changing the condition to
`largeTitleDisplayMode != Never` meant that:
- `LargeTitleDisplayMode.Always` → PrefersLargeTitles = true ✅
- `LargeTitleDisplayMode.Automatic` → PrefersLargeTitles = true ⚠️
(unintended for pages that didn't set the property)
- `LargeTitleDisplayMode.Never` → PrefersLargeTitles = false ✅

Since the default value is `Automatic`, pages that never set
`LargeTitleDisplay` would suddenly get large titles, breaking many UI
tests.

## Description of Change

This PR implements an **opt-in model** for large title display in Shell:

### 1. Added Opt-In Check
Before updating large title preferences, the code now checks if the
`LargeTitleDisplay` property is explicitly set on the Page:

```csharp
if (!page.IsSet(PlatformConfiguration.iOSSpecific.Page.LargeTitleDisplayProperty))
{
    return; // Don't update if property not explicitly set
}
…rItems (dotnet#34085)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details

- On iOS 26, setting Shell.ForegroundColor (either at the Shell level or
Page level) no longer applies the specified color to ToolbarItems.
- The toolbar items remain in the default system tint color instead of
respecting the configured foreground color.

### Root Cause

- On iOS 26, Apple's LiquidGlass redesign changed how
UINavigationBar.TintColor propagates to bar button items — it is no
longer automatically inherited.

### Description of Change

- Added UpdateRightBarButtonItemTintColors() method in
ShellPageRendererTracker.cs to explicitly set the TintColor of right bar
button items to the Shell's foreground color for iOS 26+ and Mac
Catalyst 26+. This ensures toolbar items correctly reflect the intended
color.
- Updated property change handlers (HandleShellPropertyChanged,
OnPagePropertyChanged) to call UpdateRightBarButtonItemTintColors() when
relevant properties change, guaranteeing color updates are applied when
needed.


### Issues Fixed
Fixes dotnet#34083

### Validated the behaviour in the following platforms

- [ ] Windows
- [ ] Android
- [x] iOS
- [x] Mac

### Output

| Before | After |
|----------|----------|
| <img
src="https://github.com/user-attachments/assets/dc70555a-7e03-4922-94d5-1f6723c059f1">
| <img
src="https://github.com/user-attachments/assets/30a18bca-103d-4d76-a4ef-956fbbef3fe9">
|
devanathan-vaithiyanathan and others added 15 commits March 21, 2026 19:12
…ling Permissions.CheckStatusAsync<Permissions.Microphone>() (dotnet#33179)

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
On .NET 10 Windows, calling
Permissions.CheckStatusAsync<Permissions.Microphone>() throws an
exception when the app is running as an unpackaged app.

This is a regression introduced after Windows apps were changed to run
as unpackaged by default. In unpackaged apps, AppxManifest.xml is not
used, so microphone capabilities declared in the manifest are ignored.
However, the current implementation always validates microphone
capability declarations against AppxManifest.xml, which causes an
exception for unpackaged apps.

### Description of Change

<!-- Enter description of the fix in this section -->
Updated the Windows microphone permission logic to skip manifest
capability checks for unpackaged apps. The microphone declaration is now
required only for packaged apps, preventing exceptions and aligning the
behavior with other Essentials APIs.

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes dotnet#32989 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [ ] Android
- [x] Windows
- [ ] iOS
- [ ] Mac


| Before  | After  |
|---------|--------|
| **Windows**<br> <img
src="https://github.com/user-attachments/assets/7870d1a7-eeab-4e85-be4d-b951f0bd3d19"
width="600" height="300"> | **Windows**<br> <img
src="https://github.com/user-attachments/assets/bd43f5b8-85b7-4e3c-8404-d01c165fefa4"
width="600" height="300"> |

---------
…ackground (dotnet#29225)

### Issue details
Setting SearchBar background to transparent was showing black color on
iOS.

### Description of changes
Handled transparent background in SearchBar by setting an empty
background image and UIColor.Clear. If the background is not
transparent, reset the background image to null to allow applying
backgrounds properly.


<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

<!-- Enter description of the fix in this section -->

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes dotnet#11677 

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### Output Screenshot

| Before  | After  |
|---------|--------|
| **iOS**<br> <video
src="https://github.com/user-attachments/assets/80fdaaa1-8ddf-4315-bffa-cf3463a88a98"
width="300" height="600"> | **iOS**<br> <video
src="https://github.com/user-attachments/assets/51ba2b0e-2a05-4e8a-8e75-0c448ad253c8"
width="300" height="600"> |
…t Applied in Horizontal Layouts (dotnet#29474)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Issue Details

- [Android] CollectionView MeasureFirstItem ItemSizingStrategy Not
Applied in Horizontal Layouts.

### Root Cause

- The first item's size was not applied correctly to subsequent items
because the _pixelSize field was being set to null when using a
horizontal orientation.

### Description of Changes

- Removed the assignment of null to the _pixelSize field, ensuring the
first item's size is preserved and used for measuring subsequent items.
- Introduced the UpdateAdapter method to re-measure and apply the first
item’s size to all items when switching the layout orientation from
vertical to horizontal.

Validated the behaviour in the following platforms
- [x] Android
- [x] Windows , iOS, MacOS - CollectionView MeasureFirstItem sizing not
applied on Windows, iOS and macOS [dotnet#29130]

### Issues Fixed

Fixes dotnet#29192 


### Output images
Android

| Before  | After  |
|---------|--------|
|<video
src="https://github.com/user-attachments/assets/bbafe55b-a665-4e4f-8f19-a722f3db29cf"
> | <img
src="https://github.com/user-attachments/assets/42eade41-ebce-4490-8666-6a51364988d6"
width="300" /> |
…, works on Windows (dotnet#27847)

### Issue Details:

ItemsView not visible when Grouping is enabled and ItemSizingStrategy
property set to MeasureFirstItem for CollectionView in android platform.
        
### Root Cause:

The ItemSizingStrategy property is set to MeasureFirstItem for the
CollectionView in the sample. As a result, the CollectionView calculates
the size of the first item (which is a group header template) and
applies this size to all subsequent templated items during binding. When
the second item (defined by the ItemTemplate) is measured, it uses the
size of the first item. However, during the layout phase, the second
item respects the height request specified in the DataTemplate and is
laid out according to that height. This mismatch between the measured
size and the layout size for the ItemTemplate results in incorrect
dimensions, causing the item to not display properly in the view.

### Description of Change:

I calculated the size from the ItemTemplate instead of the group header
template or any other template by adding a condition to check if
templatedItemViewHolder.ItemViewType == ItemViewType.TemplatedItem. This
allows the calculated size from the first ItemTemplate to be reused for
all subsequent ItemTemplate items. As a result, the correct dimensions
are measured and laid out according to the calculated size.

**Tested the behavior in the following platforms.**

- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### Reference:

N/A


### Issues Fixed:

Fixes dotnet#20855 
Fixes dotnet#29191
Fixes dotnet#32578 

### Screenshots
| Before  | After  |
|---------|--------|
| <img width="259" alt="Screenshot 2025-02-17 at 7 58 19 PM"
src="https://github.com/user-attachments/assets/f3ff0dba-39cd-4d19-aac4-2b9db1e82995"
/> | <img width="259" alt="Screenshot 2025-02-17 at 7 52 33 PM"
src="https://github.com/user-attachments/assets/1a1e446d-be9f-44ec-8742-774d969ab7c8"
/> |
…is null (dotnet#32314)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details
- On Windows, TimePicker renders a default time when its value is null,
both on first load and after runtime updates, instead of showing an
empty state.
- On macOS, DatePicker and TimePicker render the default date/time when
their values are null, again both initially and dynamically, rather than
remaining empty.

### Root Cause

Windows :
- The TimePicker handler uses the null-coalescing operator with
TimeSpan.Zero when the Time property is null. As a result, it defaults
to 12:00 AM in 12-hour format and 00:00 in 24-hour format.


### Description of Change
- Updated TimePickerExtensions.UpdateTime to clear the native control's
time value when the logical time is null, instead of defaulting to
midnight.
- Documentation : [Time
Picker](https://learn.microsoft.com/en-us/windows/apps/design/controls/time-picker#:~:text=When%20SelectedTime%20is%20null%2C%20the%20picker%20is%20%27unset%27%20and%20shows%20the%20field%20names%20instead%20of%20a%20time.)

**Regarding Mac Platform :**
- The same behavior is observed on the native platform as well. The
values are not cleared when the date or time picker is set to null. In
such cases, the default date or time is rendered.

### Issues Fixed
Fixes dotnet#32266 

### Validated the behaviour in the following platforms

- [x] Windows
- [x] Android
- [x] iOS
- [x] Mac

### Output

| Before | After |
|----------|----------|
| <img
src="https://github.com/user-attachments/assets/70d2fda1-66e4-4083-8df5-75c064ada067">
| <img
src="https://github.com/user-attachments/assets/33f4fc5f-456f-4de8-b72c-99c07dc07eaa">
|
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Root Cause of the issue

- The native WinUI ToggleSwitch control defines a default
[MinWidth](https://github.com/microsoft/microsoft-ui-xaml/blob/a3021d80977fecc7d6ca33dc94dc500c5535faab/src/controls/dev/CommonStyles/ToggleSwitch_themeresources.xaml#L188C20-L188C45)
of 154 in its style, likely to accommodate the OnContent and OffContent
values supported in WinUI. However, since .NET MAUI does not support
displaying these contents, the default minimum width is unnecessary. As
a result, the control always reserves that space, leading to unnecessary
space usage in .NET MAUI.

### Description of Change

- The native ToggleSwitch's minimum width is now set based on the
MinimumWidthRequest value provided by the MAUI Switch view. Since MAUI
does not support OnContent and OffContent, the default width is
unnecessary. This change ensures the control's width reflects the actual
layout requirement, resolving the unwanted spacing issue.

### Reference
-
[ToggleSwitchThemeMinWidth](https://github.com/microsoft/microsoft-ui-xaml/blob/a3021d80977fecc7d6ca33dc94dc500c5535faab/src/controls/dev/CommonStyles/ToggleSwitch_themeresources.xaml#L188C20-L188C45)
-
[DefaultSpacing](https://github.com/microsoft/microsoft-ui-xaml/blob/a3021d80977fecc7d6ca33dc94dc500c5535faab/src/controls/dev/CommonStyles/ToggleSwitch_themeresources.xaml#L501)


### Issues Fixed

Fixes dotnet#28901
Fixes dotnet#30273
Fixes dotnet#10107


### Tested the behaviour in the following platforms

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshot

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/d8f0c4da-1199-457f-8186-4bf80495c9c8">
| <video
src="https://github.com/user-attachments/assets/8128cfbf-1aaf-46c9-a059-45e512114f73">
|
…umTrackColor (dotnet#30089)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause of the issue

- On Windows, in the default slider style, the
[SliderTrackFill](https://github.com/microsoft/microsoft-ui-xaml/blob/main/src/controls/dev/CommonStyles/Slider_themeresources.xaml)
resource is mapped to the slider’s background and is used to render the
maximum (unfilled) portion of the track. In the normal visual state, the
slider background remains unchanged and uses the default style. When a
background color is applied via the Slider.Background property, it
occupies the same visual area used to render the maximum track.
 
- As a result, when both MaximumTrackColor and Background are set, the
background color takes precedence in the normal state, effectively
hiding the MaximumTrackColor. Since no visual state override exists for
the background in the default template, the assigned background remains
active and visually overrides the maximum track.
 
- However, during interaction states like PointerOver or Pressed, WinUI
applies the MaximumTrackColor to the HorizontalTrackRect, making it
appear only on hover or press.
 
- The core issue is that the background color was applied using the
Slider.Background property instead of updating the appropriate
SliderContainerBackground resource. On Windows, this resource controls
the background of the slider’s container and is the correct way to apply
background without interfering with maximum track rendering.

### Description of Change

- Implemented a mapper for the Slider background color on Windows that
updates the SliderContainerBackground and related theme resources. This
ensures the background is applied to the slider's root layout,
consistent with other platforms. As a result, both Background and
MaximumTrackColor now render correctly without overlapping, resolving
the issue. This approach ensures visual consistency with how the slider
appears on other platforms.

### Issues Fixed

Fixes dotnet#25921

### Tested the behaviour in the following platforms

- [x] Windows
- [x] iOS
- [x] Mac
- [x] Android

### Screenshot

**Note:** In this video, the `BackgroundColor` is set to `yellow`, and
the `MaximumTrackColor` is set to `red`.

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/815d6e24-94f4-42ec-8e68-2abbcd0f0ba5">
| <video
src="https://github.com/user-attachments/assets/9f4d5847-5db1-4b8c-9dc9-1efffa95fc27">
|
### Issue Detail
When navigating to a page using Shell with query parameters (e.g., via
[QueryProperty]), the expected query value was not available during
navigation.

### Root Cause
In Shell navigation with query parameters, the query attributes were
being set on the page after it was assigned to the Content property. As
a result, the query value was not available when the navigation
occurred.

### Description of Change
The query parameters (QueryAttributesProperty) are now assigned to the
page before it is set to ContentCache in ShellContent. This ensures the
query value is available to the page before the Navigated event is
triggered.

### Tested the behavior in the following platforms
 
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### Issues Fixed
 
Fixes dotnet#10509
Fixes dotnet#11113
Fixes dotnet#29645
Fixes dotnet#24241

### Screenshots

| Before Issue Fix | After Issue Fix |
|----------|----------|
| <video width="300" height="600"
src="https://github.com/user-attachments/assets/23e15d65-0f11-431f-9c2e-6c7fb3a30364">
| <video width="300" height="600"
src="https://github.com/user-attachments/assets/e566f8e5-a029-45ea-9172-5369130ee6ab">)
|
### RootCause: 
When updating the spacing value, the VerticalItem ContainerStyle is
applied, which results in the spacing value being updated to vertical,
even when using HorizontalList
### Description of Change:
I modified it by checking the orientation and apply the appropriate
ItemContainerStyle. Now, it adjusts the spacing accordingly.
### Tested the behaviour in the following platforms
- [x] Windows
- [x] Android
- [x] iOS
- [x] Mac
### Issue Fixed:
Fixes: dotnet#23377 

### Output
| Before Issue Fix | After Issue Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/ec537d52-e07c-4c64-8ee4-682cb9678ede">
| <video
src="https://github.com/user-attachments/assets/0205fa8b-f3b6-466e-8fb4-84c8765ca6d7">
|
…n the shell search handler (dotnet#28474)

### Issue Details
* When the search controller has an empty item source and is focused on
the Shell search handler, the flyout icon and content page title
disappear with no option to make them visible again.

### Root Cause
* The ShowsCancelButton property of the Shell's UISearchController
search bar is set to false. As a result, when the search controller is
focused, the cancel button does not appear, preventing the flyout icon
and content page title from being visible when the search controller has
an empty item source.

### Description of Changes
* Setting ShowsCancelButton to false is no longer necessary, as this
behavior is now handled by the automaticallyShowsCancelButton property
in UISearchBar.
* The cancel button ensures that the flyout icon and content page title
return to their normal visibility after focusing and unfocusing. When
clicking the cancel button, the flyout icon and content page title are
now correctly displayed, when the search controller has an empty item
source.

### Issues Fixed
Fixes dotnet#22060 

Validated the behaviour in the following platforms
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### Output
|Before|After|
|--|--|
| <video
src="https://github.com/user-attachments/assets/6df06656-bc5e-4627-b940-f6b6ca91e994"
>| <video
src="https://github.com/user-attachments/assets/c3120cd8-b258-4858-af09-13ea65574f62">|
<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->

### Description of Change
When calculating the width and height of the cell, add space to prevent
using the wrong view size to calculate the platform size.

**iOS**:
| Before fix | After fix |
|--------|--------|
| <img
src="https://github.com/user-attachments/assets/93dc4912-0fce-49c1-bc06-05ef03d3f711"
width="300" /> | <img
src="https://github.com/user-attachments/assets/ae1f5a40-f440-437b-8353-58b3d02cbdea"
width="300" /> |

**Android**:
| Before fix | After fix |
|--------|--------|
| <img
src="https://github.com/user-attachments/assets/5b673c29-6880-4e27-9bdb-d3119df869b7"
width="300" /> | <img
src="https://github.com/user-attachments/assets/920e3c0b-0861-4e66-b98f-140c9f850a09"
width="300" /> |

**maccatalyst**:
| Before fix | After fix |
|--------|--------|
| <img
src="https://github.com/user-attachments/assets/312a5408-645d-46ba-abc3-fa6cbbccf14c"
width="500" /> | <img
src="https://github.com/user-attachments/assets/ceba112a-20fb-40c7-a37a-b4fa0e208083"
width="500" /> |

<!-- Enter description of the fix in this section -->

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes dotnet#26633

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

---------
…idth is set (dotnet#25748)

### Root cause

- When setting the width request explicitly on a vertical layout, the
layout content of the label does not update its width and height
constraints correctly. When setting the width request in label inside
the vertical layout, the layout content of the label does not update its
width and height constraints correctly. It causes the label text
cropped.
- This is due to a missing explicit calculation for the label and layout
in the GetDesiredSizeFromHandler(this IViewHandler viewHandler, double
widthConstraint, double heightConstraint) method in the
ViewHandlerExtension.iOS class.

## Description of changes

- Included the IsExplicitSet calculation for layout and label to update
the width and height constraints.

### Issues Fixed

Fixes dotnet#15559 

### Tested the behavior in the following platforms

- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac

### ScreenShot

iOS

| Before  | After  |
|---------|--------|
|<img
src="https://github.com/user-attachments/assets/55914247-95b7-4a3e-a827-1e4bab040b11"
width="300" height="600"> | <img
src="https://github.com/user-attachments/assets/bdcff9a9-fb53-449f-9853-20c93c075def"
width="300" height="600"> |

macOS

| Before  | After  |
|---------|--------|
|<img
src="https://github.com/user-attachments/assets/e7d13898-6eae-42f0-98a7-21b6a51a71d3">
| <img
src="https://github.com/user-attachments/assets/8502c8ec-a765-4998-8886-57930065c9f2"
> |
…#34535)

> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING
MAIN. !!!!!!!
-->
### Issue Details
Flex-grow calculation was distributing the entire container space
instead of just the free space, causing items to be sized incorrectly.

### Description of Change

<!-- Enter description of the fix in this section -->
* Calculate actual free space: freeSpace = flex_dim - extra_flex_dim
* Distribute only free space proportionally based on grow ratios
* Preserve item's intrinsic size instead of resetting to 0

### Issues Fixed

<!-- Please make sure that there is a bug logged for the issue being
fixed. The bug should describe the problem and how to reproduce it. -->

Fixes dotnet#34464 

<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->

**Tested the behavior in the following platforms.**
- [x] Android
- [x] Windows
- [x] iOS
- [x] Mac


| Before  | After  |
|---------|--------|
| **Mac**<br> <img
src="https://github.com/user-attachments/assets/2f155ddd-a04a-4217-9bd8-f294357ba171"
width="600" height="300"> | **Mac**<br> <img
src="https://github.com/user-attachments/assets/cb840cc4-7c7b-4a30-aeb6-8db3a8458c79"
width="600" height="300"> |

---------

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…control edge (dotnet#34362)

Fixes dotnet#34119

When dragging from the edge of a control on Windows, `PointerExited` can
fire before the manipulation begins.

In this scenario the pointer ID is removed from the `_fingers`
collection before `HandlePan` runs, resulting in `_fingers.Count == 0`.
Since `PanGestureRecognizer` typically expects `TouchPoints == 1`, the
recognizer never matches and `PanUpdated` is not raised.

This change prevents removing the pointer prematurely when a gesture
interaction has not fully completed, ensuring `_fingers` remains
consistent until the gesture lifecycle finishes.

After this change, dragging from the edge of a control correctly
triggers `PanUpdated` as expected.

### Tested

Reproduced using a simple `AbsoluteLayout` with a draggable view and
`PanGestureRecognizer`.

Before fix:
- Starting the drag from the edge of the control sometimes prevented
`PanUpdated` from firing.

After fix:
- `PanUpdated` fires consistently when dragging from any part of the
control, including edges.
…et#34287)

### Description of Change

Removed permission request for adding photos to the devices library on
iOS, inside MediaPicker.ios, which doesn't need this permission at this
spot inside the code as it does not add the newly captured photo to the
device's library here.

Given the [official
documentation](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/device-media/picker?view=net-maui-10.0&tabs=windows#take-a-photo),
adding the picture to the device's library is an action given in the
example _after_ the CapturePhotoAsync is called, if the task's result
contains a value.
This action is completely optional, the developer could implement
different logic for handling the picture data after calling
CapturePhotoAsync, meaning the permission request becomes unnecessary
and redundant.

It is up to the developer to save the image to the device, which then
requires the permission, meaning the developer should implement the
permission request within their own logic.

### Issues Fixed

Fixes dotnet#34246
@kubaflo kubaflo changed the base branch from main to inflight/current March 22, 2026 18:04
prakashKannanSf3972 and others added 3 commits March 22, 2026 22:38
…iew Items When PointerOver Is Applied (dotnet#29815)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause :
 
The `ChangeVisualState` method does not account for the `Selected` state
in its state resolution logic. It prioritizes PointerOver over Selected,
and defaults to Normal when no pointer interaction exists—even if the
item is selected. This results in inconsistent visual feedback where the
Selected state styling is ignored or overridden.

### Description of Change

Enhanced `IsElementInSelectedState`() extension to VisualElement that
properly detects if an element is currently in the Selected visual state
by inspecting the CurrentState property of all visual state groups.
 
Updated `ChangeVisualState`() to prioritize Selected over PointerOver
and Normal:

When an element is enabled, it first checks if the element is in the
Selected state

- If the elements with the pointer over them, it applies either the
`Selected` or `PointerOver` state, depending on selection status
- If the elements are without the pointer over them, it applies either
the `Selected` or `Normal` state, depending on the selection status

This change ensures a consistent visual representation of `selected`
items while preserving pointer and focus behavior.

### Issues Fixed

Fixes dotnet#29484 

**Tested the behaviour in the following platforms**

- [x] Android
- [x]  Windows
- [x]  iOS
- [x] Mac

### Output

| Before| After|
|--|--|
| <video
src="https://github.com/user-attachments/assets/f2f82f55-7501-4560-9b80-162b9f923f0a">
| <video
src="https://github.com/user-attachments/assets/88b33563-f165-4271-bf04-97a57305d6ab">
| <video
src="https://github.com/user-attachments/assets/2649e2bc-69bc-48a4-b825-0d7169a62c0d">
| <video
src="https://github.com/user-attachments/assets/d531d7ac-efee-4e8a-b1f9-7235a303c487">
|
### Description of Change

Using the delta from ManipulationDeltaRoutedEventArgs for cumulative
displacement generates invalid event data. Consider the following
sequence for X coordinates moving the mouse from left to right:

x = 0;  delta = 0;  => totalX = 0;
x = 10; delta = 10; => totalX = 20;
x = 11;  delta = 1;  => totalX = 12;

On low end hardware or complex scenes, this causes the pan to jump
around when dragging.

### Issues Fixed

Fixes: dotnet#33539

Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 23, 2026

🤖 AI Summary

📊 Expand Full Review67ac195 · Merge branch 'inflight/current' into fix-28279
🔍 Pre-Flight — Context & Validation

Issue: #28279 - CollectionView RemainingItemsThresholdReachedCommand fires on initial data load
PR: #28321 - [Android] CV RemainingItemsThresholdReachedCommand fires on initial data load - fix
Platforms Affected: Android
Files Changed: 2 implementation/support, 3 test

Key Findings

  • The linked issue describes an Android regression where RemainingItemsThresholdReachedCommand fires during initial async data load before any user scroll.
  • Changed files split into one Android implementation file (RecyclerViewScrollListener.cs), one Android public API metadata file, and three UI-test files (HostApp XAML/code-behind plus Shared NUnit/Appium test).
  • Reviewer-requested test coverage was added based on the public repro, and a later AI verification comment claims FAIL-without-fix / PASS-with-fix on Android.
  • Unresolved inline review comments flag test hygiene issues: the new test page/class/file are numbered 28321 instead of the linked issue 28279, and the HostApp page includes an unused Data record.
  • Issue discussion raises an edge-case expectation question for non-zero thresholds when the initial item count already satisfies the threshold; the reported bug is specifically threshold 0 firing on initial load without scrolling.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #28321 Track Android RecyclerView scroll state and suppress threshold dispatch until scrolling has actually started; add Android UI repro/test coverage. ⏳ PENDING (Gate) src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs, src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt, src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml, src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml.cs, src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28321.cs Original PR; review comments still question test naming/cleanup.

🚦 Gate — Test Verification

Result: ⚠️ SKIPPED

Gate Result: ⚠️ SKIPPED

Platform: android
Mode: Full Verification

  • Tests FAIL without fix: not verified locally
  • Tests PASS with fix: not verified locally

Notes:

  • Required gate execution was attempted twice via task agent.
  • Attempt 1 failed because the verification skill auto-detected thousands of unrelated changed files from the review branch diff and never reached test execution.
  • Attempt 2 retried with explicit fix file context, but the skill invocation environment did not pass the explicit FixFiles parameter through to the script, so it fell back to the same invalid auto-detection path.
  • Prior PR discussion includes an AI summary claiming Android full verification passed, but this review run could not independently reproduce that result due to the environment blocker above.

🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-opus-4.6) Query RecyclerView.ScrollState directly in OnScrolled and only evaluate threshold while non-idle, keeping existing deferred idle dispatch. ✅ PASS src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs, src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt Simpler than PR boolean tracking; uses native scroll state as source of truth.
2 try-fix (claude-sonnet-4.6) Skip threshold evaluation on zero-displacement OnScrolled callbacks (dx == 0 && dy == 0) while leaving idle dispatch behavior intact. ✅ PASS src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs Smallest passing change so far; fully stateless.
3 try-fix (gpt-5.3-codex) Skip threshold evaluation on the first OnScrolled callback after item count transitions from zero to non-zero. ❌ FAIL src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs Not empirically validated; test command failed before execution due to missing/up-to-date MSBuild task prerequisite.
4 try-fix (gemini-3-pro-preview) Track adapter item count and suppress threshold evaluation on the initial 0→N load transition, while retaining deferred idle dispatch. ✅ PASS src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs, src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt Passing alternative; also surfaced that current PR diff duplicates OnScrollStateChanged and duplicates its PublicAPI entry.
5 try-fix round 2 (claude-opus-4.6) Arm threshold logic only after the visible range moves beyond its initial layout snapshot. ✅ PASS src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs, src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt Distinct passing alternative based on visible-range movement rather than scroll state or displacement.
6 try-fix round 2 (claude-sonnet-4.6) Skip threshold evaluation while recyclerView.IsInLayout is true. ✅ PASS src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs, src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt Directly targets layout-driven callbacks from initial population.
7 try-fix round 2 (gpt-5.3-codex) Require explicit touch intent from MauiRecyclerView before threshold evaluation can run. ❌ FAIL src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs, src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs Could not be empirically validated because Android build failed in unrelated EntryHandler2.Android.cs before test execution.
PR PR #28321 Track Android RecyclerView scroll state with a _scrollStarted field and suppress threshold dispatch until scrolling has started; add Android UI repro/test coverage. ⚠️ GATE BLOCKED LOCALLY src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs, src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt, src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml, src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml.cs, src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28321.cs Local gate blocked by verification script environment; PR also has unresolved review comments on test numbering/cleanup, and the current diff shape duplicates OnScrollStateChanged plus its PublicAPI entry.

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 1 N/A Independent attempt completed.
claude-sonnet-4.6 1 N/A Independent attempt completed.
gpt-5.3-codex 1 N/A Independent attempt hit build prerequisite blocker before test execution.
gemini-3-pro-preview 1 N/A Independent attempt completed.
claude-opus-4.6 2 Yes Arm threshold only after visible range changes from the initial snapshot.
claude-sonnet-4.6 2 Yes Gate threshold evaluation on !recyclerView.IsInLayout.
gpt-5.3-codex 2 Yes Gate threshold evaluation on explicit touch input.
gemini-3-pro-preview 2 No NO NEW IDEAS
claude-opus-4.6 3 Yes Arm threshold only after cumulative post-load scroll distance exceeds touch slop.
claude-sonnet-4.6 3 Yes Suppress threshold evaluation while ItemAnimator is running.
gpt-5.3-codex 3 Yes Delay attaching the scroll listener until after initial global layout.
gemini-3-pro-preview 3 Yes Skip threshold evaluation when scroll range changed due to content-size change.

Exhausted: Yes (max 3 rounds reached)
Selected Fix: Candidate #2 — it is the simplest passing fix, requires only a tiny localized change in RecyclerViewScrollListener, introduces no additional state or listener wiring, and still directly blocks layout/data-load callbacks from triggering threshold logic.


📋 Report — Final Recommendation

Result: ⚠️ REQUEST CHANGES

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue/PR discussion reviewed; implementation/test files classified.
Gate ⚠️ SKIPPED Android verification skill was attempted twice but blocked by diff/parameter-passing issues in this environment.
Try-Fix ✅ COMPLETE 7 attempts total, 5 passing alternatives, 3 cross-pollination rounds completed (max reached).
Report ✅ COMPLETE

Summary

This PR addresses a real Android regression, and the added repro coverage is directionally correct, but I do not recommend approval in its current state.

The strongest issue is correctness of the current diff itself: src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs contains two OnScrollStateChanged(RecyclerView, int) overrides, and src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt contains the same OnScrollStateChanged API entry twice. That is a concrete defect in the submitted change, independent of the local gate blocker.

Beyond that, try-fix exploration found multiple simpler passing alternatives. The best validated option was candidate #2: ignore threshold evaluation for zero-displacement OnScrolled callbacks (dx == 0 && dy == 0), which solves the repro with a much smaller, stateless change than the PR’s _scrollStarted tracking.

The PR also still carries unresolved review feedback on the test assets: the added issue page/test are numbered after PR 28321 rather than linked issue 28279, and the HostApp code-behind includes an unused Data record.

Root Cause

Android RecyclerView emits OnScrolled callbacks during initial layout/data population even when no user scroll has occurred. The threshold logic currently treats those callbacks as eligible for RemainingItemsThresholdReached, so the command can fire on first load.

Fix Quality

The PR fix is plausible in intent, but the current implementation is not ready:

  • it introduces duplicate override/API declarations in the current diff,
  • it uses extra state where several smaller passing alternatives exist,
  • and the test additions still need cleanup/alignment with the linked issue.

Given the duplicate method/API entries plus the existence of simpler validated alternatives, this should be revised before approval.


@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 23, 2026
@kubaflo
Copy link
Copy Markdown
Contributor Author

kubaflo commented Apr 1, 2026

Code Review — PR #28321

Independent Assessment

What this changes: Adds a _scrollStarted boolean to RecyclerViewScrollListener (Android) that gates RemainingItemsThresholdReached dispatch. The flag is set in a new OnScrollStateChanged override and checked in OnScrolled before evaluating the threshold. Also adds a UI test page and NUnit/Appium test to verify the command does not fire on initial data load.

Inferred motivation: Android's RecyclerView emits OnScrolled(0, 0) callbacks during initial layout/data population. The existing threshold logic treated these as eligible scroll events, causing RemainingItemsThresholdReachedCommand to fire before the user ever scrolled.

Reconciliation with PR Narrative

Author claims: Fixes #28279RemainingItemsThresholdReachedCommand fires on initial data load on Android.
Agreement/disagreement: The root cause analysis is correct and the approach of gating on scroll state is reasonable. However, the current diff has a critical merge conflict that must be resolved before this can merge.

Findings

❌ Critical — Duplicate OnScrollStateChanged override (compile error)

The file src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs on the PR head contains two OnScrollStateChanged(RecyclerView, int) overrides:

  1. Lines ~41-45 (added by this PR): Sets _scrollStarted based on scroll state
  2. Lines ~107-117 (from inflight/current base): Handles deferred _pendingRemainingItemsThresholdReached dispatch when idle

C# does not allow two overrides of the same method — this will not compile, and CI confirms all builds are failing. These two methods must be merged into one:

public override void OnScrollStateChanged(RecyclerView recyclerView, int newState)
{
    base.OnScrollStateChanged(recyclerView, newState);
    _scrollStarted = newState != RecyclerView.ScrollStateIdle;
    
    if (_pendingRemainingItemsThresholdReached && newState == RecyclerView.ScrollStateIdle)
    {
        _pendingRemainingItemsThresholdReached = false;
        if (!_disposed && _itemsView is not null)
        {
            _itemsView.SendRemainingItemsThresholdReached();
        }
    }
}

❌ Duplicate PublicAPI entry

src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt contains the OnScrollStateChanged entry twice (lines 9 and 11). One must be removed.

⚠️ Missing trailing newlines

All three new test files lack a newline at end-of-file:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml
  • src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28321.cs

⚠️ Unused Data record struct

Issue28321.xaml.cs line 8 declares public readonly record struct Data(string Text, string AutomationId) which is never referenced. Should be removed.

Devil's Advocate

  • The _scrollStarted approach tracks extra state whereas simpler alternatives exist (e.g., checking dx == 0 && dy == 0 in OnScrolled, or querying recyclerView.ScrollState directly). However, the explicit state tracking is arguably more intentional/readable and correctly handles edge cases where a zero-displacement scroll might occur mid-fling. The approach is defensible once the merge conflict is resolved.
  • One could argue the threshold should fire on initial load if items already satisfy the threshold — but the linked issue and Android documentation both indicate it should only fire in response to user scrolling, so the current gating is correct.

Verdict: NEEDS_CHANGES

Confidence: high
Summary: The fix correctly identifies and addresses the root cause, and the test is well-structured. However, the PR branch has diverged badly from inflight/current (205 commits, 2292 files), resulting in a duplicate OnScrollStateChanged override that won't compile and a duplicate PublicAPI entry. A rebase and merge of the two OnScrollStateChanged bodies is required before this can proceed.

@kubaflo
Copy link
Copy Markdown
Contributor Author

kubaflo commented Apr 1, 2026

🟡 .NET MAUI Review - Changes Suggested

Expand Full Review - 67ac195 - [Android] CV RemainingItemsThresholdReachedCommand fires on initial data load - fix

Independent Assessment

What this changes: Adds a _scrollStarted boolean to RecyclerViewScrollListener (Android) that gates RemainingItemsThresholdReached dispatch. The flag is set in a new OnScrollStateChanged override and checked in OnScrolled before evaluating the threshold. Also adds a UI test page and NUnit/Appium test to verify the command does not fire on initial data load.

Inferred motivation: Android's RecyclerView emits OnScrolled(0, 0) callbacks during initial layout/data population. The existing threshold logic treated these as eligible scroll events, causing RemainingItemsThresholdReachedCommand to fire before the user ever scrolled.

Reconciliation with PR Narrative

Author claims: Fixes #28279RemainingItemsThresholdReachedCommand fires on initial data load on Android.
Agreement/disagreement: The root cause analysis is correct and the approach of gating on scroll state is reasonable. However, the current diff has a critical merge conflict that must be resolved before this can merge.

Findings

❌ Critical — Duplicate OnScrollStateChanged override (compile error)

The file src/Controls/src/Core/Handlers/Items/Android/RecyclerViewScrollListener.cs on the PR head contains two OnScrollStateChanged(RecyclerView, int) overrides:

  1. Lines ~41-45 (added by this PR): Sets _scrollStarted based on scroll state
  2. Lines ~107-117 (from inflight/current base): Handles deferred _pendingRemainingItemsThresholdReached dispatch when idle

C# does not allow two overrides of the same method — this will not compile, and CI confirms all builds are failing. These two methods must be merged into one:

public override void OnScrollStateChanged(RecyclerView recyclerView, int newState)
{
    base.OnScrollStateChanged(recyclerView, newState);
    _scrollStarted = newState != RecyclerView.ScrollStateIdle;
    
    if (_pendingRemainingItemsThresholdReached && newState == RecyclerView.ScrollStateIdle)
    {
        _pendingRemainingItemsThresholdReached = false;
        if (!_disposed && _itemsView is not null)
        {
            _itemsView.SendRemainingItemsThresholdReached();
        }
    }
}

❌ Duplicate PublicAPI entry

src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt contains the OnScrollStateChanged entry twice (lines 9 and 11). One must be removed.

⚠️ Missing trailing newlines

All three new test files lack a newline at end-of-file:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml
  • src/Controls/tests/TestCases.HostApp/Issues/Issue28321.xaml.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28321.cs

⚠️ Unused Data record struct

Issue28321.xaml.cs line 8 declares public readonly record struct Data(string Text, string AutomationId) which is never referenced. Should be removed.

Devil's Advocate

  • The _scrollStarted approach tracks extra state whereas simpler alternatives exist (e.g., checking dx == 0 && dy == 0 in OnScrolled, or querying recyclerView.ScrollState directly). However, the explicit state tracking is arguably more intentional/readable and correctly handles edge cases where a zero-displacement scroll might occur mid-fling. The approach is defensible once the merge conflict is resolved.
  • One could argue the threshold should fire on initial load if items already satisfy the threshold — but the linked issue and Android documentation both indicate it should only fire in response to user scrolling, so the current gating is correct.

Verdict: NEEDS_CHANGES

Confidence: high
Summary: The fix correctly identifies and addresses the root cause, and the test is well-structured. However, the PR branch has diverged badly from inflight/current (205 commits, 2292 files), resulting in a duplicate OnScrollStateChanged override that won't compile and a duplicate PublicAPI entry. A rebase and merge of the two OnScrollStateChanged bodies is required before this can proceed.

@kubaflo
Copy link
Copy Markdown
Contributor Author

kubaflo commented Apr 2, 2026

Not needed anymore

@kubaflo kubaflo closed this Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-controls-collectionview CollectionView, CarouselView, IndicatorView community ✨ Community Contribution platform/android s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CollectionView RemainingItemsThresholdReachedCommand fires on initial data load