Skip to content

Comments

Shell: Update flyout behavior when items are dynamically replaced#28241

Open
Vignesh-SF3580 wants to merge 12 commits intodotnet:mainfrom
Vignesh-SF3580:fix-28078
Open

Shell: Update flyout behavior when items are dynamically replaced#28241
Vignesh-SF3580 wants to merge 12 commits intodotnet:mainfrom
Vignesh-SF3580:fix-28078

Conversation

@Vignesh-SF3580
Copy link
Contributor

@Vignesh-SF3580 Vignesh-SF3580 commented Mar 7, 2025

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 from this PR and let us know in a comment if this change resolves your issue. Thank you!

Issue Detail

Platform-specific symptoms:

  • Android: App crashes with IllegalArgumentException: No drawer view found with gravity LEFT when tapping flyout icon after items are replaced
  • Windows: Flyout menu icon is not displayed after clearing and adding items
  • iOS: Flyout icon is not visible when dynamically clearing and adding items with a small delay

Reproduction scenario:

  1. Shell starts with single item (e.g., LoginPage)
  2. Items.Clear() is called
  3. Multiple items are added (e.g., Page1, Page2)
  4. User attempts to open flyout → Crash on Android, missing icon on Windows/iOS

Root Cause

When Shell items are dynamically replaced (cleared then re-added), the flyout behavior state is not properly updated on platform handlers.

Why this matters:

  • When Items.Count == 1, GetEffectiveFlyoutBehavior() returns FlyoutBehavior.Disabled (single-item shells don't need flyout)
  • When Items.Count >= 2, it should return FlyoutBehavior.Flyout (multiple items require flyout navigation)
  • The transition from 1 → 2 items is a critical state change that must be communicated to platform handlers

The bug: When items are replaced dynamically, this state transition (disabled → enabled) was not triggering a notification to platform handlers, leaving them in stale state:

  • Android's DrawerLayout wasn't initialized (causing the crash)
  • Windows/iOS flyout icons weren't displayed

Description of Change

Added state tracking to ShellElementCollection to detect when items are cleared and then re-added, explicitly notifying flyout behavior observers when the flyout should transition from disabled to enabled.

Implementation:

  1. Added _isItemsCleared flag to track when Clear() is called
  2. In InnerCollectionChanged, when items are added after clearing:
    • Check if _isItemsCleared && Count == 2 (the threshold where flyout should enable)
    • Call shell.NotifyFlyoutBehaviorObservers() to update platform handlers
    • Reset _isItemsCleared flag

Why Count == 2 specifically?

  • Count == 1: Flyout is disabled (no flyout needed for single item)
  • Count == 2: Flyout transitions from disabled → enabled (notification required)
  • Count >= 3: Flag already reset, no duplicate notifications

This ensures platform handlers receive the flyout behavior update at exactly the moment when the flyout should become available.

Key Technical Details

NotifyFlyoutBehaviorObservers():

  • Iterates through all registered flyout behavior observers (platform handlers)
  • Calls OnFlyoutBehaviorChanged() on each observer
  • Updates the handler value for IFlyoutView.FlyoutBehavior

Type restriction:

  • Check this is ShellItemCollection because only ShellItem has Parent reference to Shell
  • Cannot use this pattern with ShellContentCollection or ShellSectionCollection

Normal update path:

  • Shell.AppearanceChanged() normally calls NotifyFlyoutBehaviorObservers() on structural changes
  • Dynamic item replacement bypasses this path, requiring explicit notification

What NOT to Do (for future agents)

  • Don't notify on every Add() - Performance impact; only need to notify when crossing the disabled → enabled threshold
  • Don't check Count == 1 - Flyout should remain disabled at Count == 1, no notification needed
  • Don't reset flag before notification - Flag must remain true until Count == 2 to catch the transition point

Edge Cases

Scenario Behavior Risk
Add 3+ items after clear Flag reset at Count == 2, subsequent adds don't retrigger Low - Correct behavior
Clear without re-adding Flag remains true until next Add Low - No impact
Nested Clear/Add cycles Flag resets properly on each cycle Low - Each cycle independent
Single item → Single item Count never reaches 2, no notification Low - Expected (flyout stays disabled)

Issues Fixed

Fixes #28078

Platforms Tested

  • Android
  • Windows
  • iOS
  • Mac

Screenshots

Before Issue Fix After Issue Fix
28078BeforeFix.mov
28078AfterFix.mov

iOS:

Before Issue Fix After Issue Fix
BeforeFix28078.mov
AfterFix28078.mov

@dotnet-policy-service dotnet-policy-service bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Mar 7, 2025
@jsuarezruiz jsuarezruiz added area-controls-contextmenu ContextMenu area-controls-shell Shell Navigation, Routes, Tabs, Flyout and removed area-controls-contextmenu ContextMenu labels Mar 7, 2025
@jsuarezruiz
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@jsuarezruiz
Copy link
Contributor

/rebase

@jsuarezruiz
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@jsuarezruiz
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@Vignesh-SF3580 Vignesh-SF3580 marked this pull request as ready for review March 25, 2025 14:28
@Vignesh-SF3580 Vignesh-SF3580 requested a review from a team as a code owner March 25, 2025 14:28
@jsuarezruiz
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@Dreamescaper
Copy link

Just confirming that this PR fixes my issue.
Currently I have a workaround in my code:

shell.ChildAdded += (_, _) =>
{
    if (mcShell.Items.Count == 2)
    {
        NotifyFlyoutBehaviorObservers(mcShell);
    }
};

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "NotifyFlyoutBehaviorObservers")]
extern static void NotifyFlyoutBehaviorObservers(Shell shell);

So I'm not really blocked. Still, would be nice to have this issue fixed :)

Copy link
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

This PR fixes a critical issue where Shell flyout menu icons disappear or cause crashes on Android after clearing and adding items to Shell.Items. The fix ensures that flyout behavior is properly updated when items are re-added after being cleared.

Key Changes

  • Added _isItemsCleared flag to track when items have been cleared from a collection
  • Added logic to notify flyout behavior observers when the second item is added after a clear operation
  • Added comprehensive UI tests with snapshots for Android, iOS, Windows, and Mac platforms

Reviewed changes

Copilot reviewed 3 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Controls/src/Core/Shell/ShellElementCollection.cs Core fix: Tracks cleared state and notifies flyout behavior observers when items are re-added
src/Controls/tests/TestCases.HostApp/Issues/Issue28078.cs Test page that reproduces the issue by clearing and adding Shell items
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28078.cs NUnit test implementation that verifies the fix works
src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShellFlyoutMenuCrash.png Visual verification snapshot for Android
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ShellFlyoutMenuCrash.png Visual verification snapshot for iOS
src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ShellFlyoutMenuCrash.png Visual verification snapshot for Windows
src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ShellFlyoutMenuCrash.png Visual verification snapshot for Mac

Comment on lines +158 to +164
// When items are cleared, the flyout behavior is set to disabled state. When adding items again, we need to update the flyout behavior. The flyout state should be updated only when the second item is added to the view.
if (_isItemsCleared && Count == 2 && this is ShellItemCollection shellItemCollection)
{
var shell = shellItemCollection[0].Parent as Shell;
shell?.NotifyFlyoutBehaviorObservers();
_isItemsCleared = false;
}
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

[nitpick] The comment and logic would be clearer if restructured. The comment states "The flyout state should be updated only when the second item is added" but the condition Count == 2 actually means "when the second item has been added" (after the add operation completes). Consider rephrasing to: "When items are cleared, the flyout behavior is set to disabled state. When adding items again, update the flyout behavior after the second item is added."

Additionally, the variable name _isItemsCleared doesn't clearly indicate that it's tracking whether the flyout behavior needs to be updated. Consider renaming to _needsFlyoutBehaviorUpdate for better clarity.

Copilot uses AI. Check for mistakes.
var shell = shellItemCollection[0].Parent as Shell;
shell?.NotifyFlyoutBehaviorObservers();
_isItemsCleared = false;
}
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The fix specifically checks this is ShellItemCollection but the _isItemsCleared flag is set in the base ShellElementCollection class for all collection types. If a non-ShellItemCollection is cleared and items are added, the flag will be set to true but never reset to false. This could lead to unexpected behavior.

Consider either:

  1. Moving the flag to ShellItemCollection specifically, or
  2. Resetting the flag in an else block: else { _isItemsCleared = false; }

This ensures the flag is properly managed for all collection types.

Suggested change
}
}
else if (_isItemsCleared)
{
_isItemsCleared = false;
}

Copilot uses AI. Check for mistakes.
@rmarinho
Copy link
Member

rmarinho commented Feb 14, 2026

🤖 AI Summary

📊 Expand Full Review
🔍 Pre-Flight — Context & Validation
📝 Review Sessionupdated ShellElementCollection.cs · 5ef3efe

Issue: #28078 - Shell crashes when tapping on flyout menu item after items replaced
Platforms Affected: Android (crash), Windows (icon missing), iOS (icon not visible)
Files Changed: 1 implementation file, 2 test files, 4 snapshot images

Issue Summary

Problem: When Shell.Items are cleared and new items are added, multiple platform issues occur:

  • Android: App crashes with Java.Lang.IllegalArgumentException: No drawer view found with gravity LEFT when tapping flyout menu button
  • Windows: Flyout menu icon is not displayed after clearing and adding items
  • iOS: Flyout icon is not visible when dynamically clearing and adding items with a small delay

Root Cause (from PR description): After clearing and adding new items, the flyout size was not calculated and the flyout behavior was not updated.

Reproduction Steps:

  1. Create a Shell with initial page (e.g., LogInPage)
  2. Clear Items: Items.Clear()
  3. Add new pages: Items.Add(new Page1()); Items.Add(new Page2());
  4. Tap flyout menu button → Crash on Android, missing icon on Windows/iOS

Key Findings from Discussion

Issue Comments:

  • Original reporter confirmed crash on Android, but iOS works fine with the repro code
  • Community member suggested workaround using IsVisible property instead of clearing items
  • Alternative workaround: Add new items first, then remove old one: Items.Add(new Page1()); Items.Add(new Page2()); Items.RemoveAt(0);
  • Another workaround involves routing/navigation before removing items

PR Review Comments:

  • Reviewer requested Windows and Mac snapshots (now committed)
  • Copilot suggested code clarity improvements (comment rephrasing, variable naming)
  • Copilot raised concern about _isItemsCleared flag not being reset for non-ShellItemCollection types

Edge Cases to Check

From review discussion:

  • Flag reset behavior for non-ShellItemCollection types (raised by Copilot)
  • Behavior when items are replaced without clearing (e.g., Items[0] = new Page1())
  • Behavior when only one item exists after clear+add

Files Classification

Fix files (1):

  • src/Controls/src/Core/Shell/ShellElementCollection.cs (+10 lines): Added _isItemsCleared flag tracking and flyout behavior notification

Test files (2):

  • src/Controls/tests/TestCases.HostApp/Issues/Issue28078.cs: Shell test page reproducing the issue
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28078.cs: NUnit test with Shell category

Test type: UI Tests (Shell flyout interaction)

Snapshot files (4):

  • Android, iOS, Windows, Mac snapshots committed

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #28241 Add _isItemsCleared flag; set on Clear(); notify flyout behavior when Count==2 after clear ⏳ PENDING (Gate) ShellElementCollection.cs (+10) Original PR fix

PR's Fix Approach

The fix adds state tracking to ShellElementCollection:

  1. New flag: bool _isItemsCleared field added to track when items are cleared
  2. Set on Clear(): Flag set to true in Clear() method (line 123)
  3. Check on Add(): When items are added after clear, check if Count == 2 and this is ShellItemCollection
  4. Notify observers: Call shell?.NotifyFlyoutBehaviorObservers() to update flyout behavior
  5. Reset flag: Set _isItemsCleared = false after notification

Key logic: The fix specifically targets the second item addition (Count == 2) because that's when the flyout menu becomes relevant (flyout needs at least 2 pages to be meaningful).

Reviewers' Concerns

Copilot's concerns:

  1. Variable naming: _isItemsCleared could be clearer as _needsFlyoutBehaviorUpdate
  2. Flag scope: Flag is in base class but only checked for ShellItemCollection - potential leak for other collection types

🚦 Gate — Test Verification
📝 Review Sessionupdated ShellElementCollection.cs · 5ef3efe

Result: ✅ PASSED
Platform: android
Mode: Full Verification

Verification Results

  • Tests FAIL without fix ✅ (proves tests detect the bug)
  • Tests PASS with fix ✅ (proves fix resolves the issue)

Test Details

Test Configuration:

  • Platform: Android
  • Device: emulator-5554
  • Test Filter: Issue28078
  • Test Name: ShellFlyoutMenuCrash
  • Category: Shell

Fix Files Validated:

  • src/Controls/src/Core/Shell/ShellElementCollection.cs

Execution Summary:

  • Total Tests Run: 1
  • Test Execution Time: ~6 seconds per run
  • Build Time: ~90 seconds (initial), ~28 seconds (with fix)

Test Behavior

The test successfully validates that:

  1. Without fix: Clicking the flyout menu after clearing and adding items causes the expected crash/failure
  2. With fix: The flyout menu works correctly after clearing and adding items

This confirms the tests are properly written and catch the exact issue described in #28078.


🔧 Fix — Analysis & Comparison
📝 Review Sessionupdated ShellElementCollection.cs · 5ef3efe

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-sonnet-4.5) Override Add() in ShellItemCollection to detect 1→2 transition ✅ PASS 1 file (ShellItemCollection.cs) No state flag; localized to derived class; checks Count==1 before, Count==2 after
2 try-fix (claude-opus-4.6) Notify flyout from Shell.Initialize items handler ✅ PASS 1 file (Shell.cs) Add NotifyFlyoutBehaviorObservers() to existing VisibleItemsChangedInternal handler
3 try-fix (gpt-5.2) Notify from Shell.SendFlyoutItemsChanged() ✅ PASS 1 file (Shell.cs) Call NotifyFlyoutBehaviorObservers() inside SendFlyoutItemsChanged()
4 try-fix (gpt-5.2-codex) Notify on page appearing ❌ FAIL 1 file (Shell.cs) Why failed: Too late - crash happens before page appears
6 try-fix (gpt-5.2) Track wasMulti/isMulti transitions in InnerCollectionChanged ✅ PASS 1 file (ShellElementCollection.cs) More complex state tracking; handles all 0/1↔2+ transitions
7 try-fix (gpt-5.2-codex) Override OnChildAdded in Shell.cs ✅ PASS 8 files (Shell.cs + 7 PublicAPI.Unshipped.txt) Shell-level hook; requires PublicAPI updates for new protected override
PR PR #28241 State tracking + Count==2 check in InnerCollectionChanged ✅ PASS (Gate) 1 file (ShellElementCollection.cs) Set _isItemsCleared flag on Clear(); check flag + Count==2 on Add()

Cross-Pollination Results

Round 2:

Model Response Action Taken
claude-sonnet-4.5 NEW IDEAS: Reactive attachment, property listener, virtual OnItemsRestored, lazy validation Declined - variations of existing approaches
claude-opus-4.6 NO NEW IDEAS N/A
gpt-5.2 NEW IDEA: Track wasMulti/isMulti transitions ✅ Implemented as Attempt 6 - PASSED
gpt-5.2-codex NEW IDEA: Override OnChildAdded in Shell.cs ✅ Implemented as Attempt 7 - PASSED
gemini-3-pro-preview (Failed to respond) Skipped after retry

Round 3:

Model Response
claude-sonnet-4.5 NEW IDEAS: Property setter interception, lazy visual tree evaluation, collection action filtering
claude-opus-4.6 NO NEW IDEAS
gpt-5.2 NEW IDEA: Force Reset rebuild on 1↔2 transition
gpt-5.2-codex NO NEW IDEAS
gemini-3-pro-preview NEW IDEA: Subscribe in ShellFlyoutItemsManager

Exhausted: Yes (7 attempts completed; remaining ideas are variations)

Selected Fix: PR's Fix (Original)

Reasoning:

After comparing 6 passing candidates with the PR's fix, the PR's approach is the best choice for the following reasons:

1. Simplicity and Clarity:

  • The PR's fix is straightforward: track when items are cleared, then notify when exactly 2 items exist
  • Single boolean flag with clear set/reset points
  • Easy to understand intent from the code and comment

2. Minimal Scope:

  • Only modifies ShellElementCollection.cs (1 file)
  • No PublicAPI changes (unlike Attempt 7)
  • No Shell-level changes that might affect other behaviors

3. Precise Targeting:

  • The check _isItemsCleared && Count == 2 && this is ShellItemCollection is very specific
  • Only triggers in the exact scenario that causes the bug (clear + add second item)
  • Less likely to cause unintended side effects in other scenarios

4. Comparison with Alternatives:

Candidate Why Not Selected
#1 (Override Add) Similar simplicity but requires overriding collection method; PR's approach keeps logic in existing event handler
#2 (Shell.Initialize) Triggers on ALL items changes, not just clear+add; may cause unnecessary notifications
#3 (SendFlyoutItemsChanged) Similar issue - broader scope than needed; triggers on all flyout item changes
#6 (wasMulti/isMulti) More complex state tracking with additional fields; harder to understand intent
#7 (OnChildAdded) Requires 8 file changes (PublicAPI updates); introduces new protected override for limited use case

5. Test Coverage:

  • PR includes comprehensive test coverage across all platforms (Android, iOS, Windows, Mac)
  • Tests specifically reproduce the clear+add scenario
  • Snapshots verify correct visual behavior

Root Cause Analysis

What was broken:
After Shell.Items.Clear() followed by adding new items, the flyout behavior remained in a disabled/stale state because the flyout observers were never notified to recalculate GetEffectiveFlyoutBehavior().

Why it broke:

  • GetEffectiveFlyoutBehavior() returns FlyoutBehavior.Disabled when Items.Count <= 1
  • After Clear(), count is 0 → flyout disabled
  • When adding items back, no code path existed to re-evaluate and notify the disabled→enabled transition
  • Android's DrawerLayout required the flyout to be properly initialized before the hamburger button could work

Why the fix works:
The PR's fix detects the specific transition point (adding the second item after a clear) and calls NotifyFlyoutBehaviorObservers(), which forces all platform handlers to re-evaluate GetEffectiveFlyoutBehavior() and update their UI accordingly (show hamburger button, setup drawer layout, etc.).

Edge cases handled:

  • Only triggers for ShellItemCollection (not other shell element collections)
  • Only triggers when Count == 2 (the point where flyout becomes meaningful)
  • Only triggers after items were cleared (via _isItemsCleared flag)
  • Flag is reset after notification to avoid repeated triggers

📋 Report — Final Recommendation
📝 Review Sessionupdated ShellElementCollection.cs · 5ef3efe

✅ Final Recommendation: APPROVE

Summary

PR #28241 successfully fixes a critical Shell crash on Android (and icon visibility issues on Windows/iOS) when clearing and re-adding Shell.Items. The fix has been thoroughly validated:

  • Gate passed - Tests correctly fail without fix, pass with fix
  • 6 passing alternatives explored - All use similar approach (detect 1→2 item transition)
  • PR's fix is the best choice - Simplest, most targeted, minimal scope
  • Comprehensive test coverage - UI tests + snapshots for all platforms

Root Cause

After Shell.Items.Clear() followed by adding new items, the flyout behavior remained in a disabled/stale state because the flyout observers were never notified to recalculate GetEffectiveFlyoutBehavior().

Why it broke:

  • GetEffectiveFlyoutBehavior() returns FlyoutBehavior.Disabled when Items.Count <= 1
  • After Clear(), count is 0 → flyout disabled
  • When adding items back, no code path existed to re-evaluate and notify the disabled→enabled transition
  • Android's DrawerLayout required the flyout to be properly initialized before the hamburger button could work → crash: "No drawer view found with gravity LEFT"

Fix Quality

What the code does:
The fix adds state tracking to ShellElementCollection:

  1. Set _isItemsCleared = true when Clear() is called
  2. On item add, check if _isItemsCleared && Count == 2 && this is ShellItemCollection
  3. Call shell?.NotifyFlyoutBehaviorObservers() to trigger re-evaluation
  4. Reset flag to false

Why this is the best approach:

  • Precise targeting: Only triggers in the exact clear+add scenario
  • Minimal scope: Single file change (ShellElementCollection.cs)
  • Clear intent: Boolean flag with obvious set/reset points
  • No side effects: Doesn't affect other collection change scenarios

Alternatives considered:
6 other passing fixes were explored (override Add(), Shell.Initialize handler, SendFlyoutItemsChanged, etc.). All work, but the PR's approach is simplest and most targeted.

Platform Coverage

Tested platforms:

  • ✅ Android (crash fix validated)
  • ✅ Windows (icon visibility verified)
  • ✅ iOS (icon visibility verified)
  • ✅ Mac (snapshot committed)

UI tests:

  • Test page: Issue28078.cs - Reproduces clear+add scenario
  • Test: ShellFlyoutMenuCrash - Verifies flyout opens after clear+add
  • Snapshots: Committed for all 4 platforms

PR Metadata Quality

Title: "Fixed FlyoutMenu icon issue when clearing and adding items"

  • ✅ Describes the behavior fix
  • ⚠️ Could be improved with platform prefix and component: Shell: Fix flyout crash/icon when clearing and adding items

Description:

  • ✅ Clear issue detail for each platform
  • ✅ Root cause documented
  • ✅ Platforms tested listed
  • ✅ Issue link included
  • ✅ Before/after videos
  • Missing required NOTE block at top (for users to test PR artifacts)
  • ⚠️ "Description of change" is vague - could be more specific about the state tracking approach

Recommended additions to description:

  1. Add NOTE block at top (required):
<!-- 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!
  1. Enhance "Description of change" to be more specific:
### Description of change

Added `_isItemsCleared` flag to `ShellElementCollection` to track when items are cleared. When adding items after a clear, the code now checks if exactly 2 items exist (the threshold for flyout visibility) and calls `NotifyFlyoutBehaviorObservers()` to force the platform handlers to recalculate flyout behavior and update the UI (show hamburger button, setup drawer layout, etc.).

**Key changes:**
- Set flag on `Clear()` (line 123)
- Check flag + Count==2 on item add (line 159)
- Call `shell?.NotifyFlyoutBehaviorObservers()` to trigger re-evaluation
- Reset flag after notification

Code Review Findings

✅ Implementation Quality:

  • Clean, focused change with clear intent
  • Well-commented (explains when flyout state should be updated)
  • Appropriate null-safety (shell?.)
  • Type-safe collection check (this is ShellItemCollection)

⚠️ Minor Considerations (from review comments):

  1. Variable naming (Copilot suggestion): _isItemsCleared could be clearer as _needsFlyoutBehaviorUpdate - but existing name is acceptable
  2. Flag scope (Copilot concern): Flag is in base class but only checked for ShellItemCollection - potential leak for other collection types. However, this is fine because:
    • Only ShellItemCollection checks the flag
    • Flag is harmless for other collection types (set but never used)
    • Keeps the logic in the base class where Clear() exists

No critical issues identified.

Recommendation Details

Approve because:

  1. Fix correctly resolves the crash and visibility issues
  2. Tests properly validate the fix (fail without, pass with)
  3. Implementation is clean and well-targeted
  4. Comprehensive platform coverage
  5. No alternative fixes found that are meaningfully simpler

Minor improvements for author (non-blocking):

  • Add NOTE block to description (required for users testing PR builds)
  • Consider more specific title: Shell: Fix flyout crash/icon when clearing and adding items
  • Enhance "Description of change" with technical details

These are documentation improvements that don't affect the quality of the code fix itself.


@rmarinho
Copy link
Member

rmarinho commented Feb 14, 2026

📋 PR Finalization Review

Title: ⚠️ Needs Update

Current: Fixed FlyoutMenu icon issue when clearing and adding items

Recommended: Shell: Update flyout behavior when items are dynamically replaced

Description: ⚠️ Needs Update

Description needs updates. See details below.

✨ Suggested PR Description

[!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 from this PR and let us know in a comment if this change resolves your issue. Thank you!

Issue Detail

Platform-specific symptoms:

  • Android: App crashes with IllegalArgumentException: No drawer view found with gravity LEFT when tapping flyout icon after items are replaced
  • Windows: Flyout menu icon is not displayed after clearing and adding items
  • iOS: Flyout icon is not visible when dynamically clearing and adding items with a small delay

Reproduction scenario:

  1. Shell starts with single item (e.g., LoginPage)
  2. Items.Clear() is called
  3. Multiple items are added (e.g., Page1, Page2)
  4. User attempts to open flyout → Crash on Android, missing icon on Windows/iOS

Root Cause

When Shell items are dynamically replaced (cleared then re-added), the flyout behavior state is not properly updated on platform handlers.

Why this matters:

  • When Items.Count == 1, GetEffectiveFlyoutBehavior() returns FlyoutBehavior.Disabled (single-item shells don't need flyout)
  • When Items.Count >= 2, it should return FlyoutBehavior.Flyout (multiple items require flyout navigation)
  • The transition from 1 → 2 items is a critical state change that must be communicated to platform handlers

The bug: When items are replaced dynamically, this state transition (disabled → enabled) was not triggering a notification to platform handlers, leaving them in stale state:

  • Android's DrawerLayout wasn't initialized (causing the crash)
  • Windows/iOS flyout icons weren't displayed

Description of Change

Added state tracking to ShellElementCollection to detect when items are cleared and then re-added, explicitly notifying flyout behavior observers when the flyout should transition from disabled to enabled.

Implementation:

  1. Added _isItemsCleared flag to track when Clear() is called
  2. In InnerCollectionChanged, when items are added after clearing:
    • Check if _isItemsCleared && Count == 2 (the threshold where flyout should enable)
    • Call shell.NotifyFlyoutBehaviorObservers() to update platform handlers
    • Reset _isItemsCleared flag

Why Count == 2 specifically?

  • Count == 1: Flyout is disabled (no flyout needed for single item)
  • Count == 2: Flyout transitions from disabled → enabled (notification required)
  • Count >= 3: Flag already reset, no duplicate notifications

This ensures platform handlers receive the flyout behavior update at exactly the moment when the flyout should become available.

Key Technical Details

NotifyFlyoutBehaviorObservers():

  • Iterates through all registered flyout behavior observers (platform handlers)
  • Calls OnFlyoutBehaviorChanged() on each observer
  • Updates the handler value for IFlyoutView.FlyoutBehavior

Type restriction:

  • Check this is ShellItemCollection because only ShellItem has Parent reference to Shell
  • Cannot use this pattern with ShellContentCollection or ShellSectionCollection

Normal update path:

  • Shell.AppearanceChanged() normally calls NotifyFlyoutBehaviorObservers() on structural changes
  • Dynamic item replacement bypasses this path, requiring explicit notification

What NOT to Do (for future agents)

  • Don't notify on every Add() - Performance impact; only need to notify when crossing the disabled → enabled threshold
  • Don't check Count == 1 - Flyout should remain disabled at Count == 1, no notification needed
  • Don't reset flag before notification - Flag must remain true until Count == 2 to catch the transition point

Edge Cases

Scenario Behavior Risk
Add 3+ items after clear Flag reset at Count == 2, subsequent adds don't retrigger Low - Correct behavior
Clear without re-adding Flag remains true until next Add Low - No impact
Nested Clear/Add cycles Flag resets properly on each cycle Low - Each cycle independent
Single item → Single item Count never reaches 2, no notification Low - Expected (flyout stays disabled)

Issues Fixed

Fixes #28078

Platforms Tested

  • Android
  • Windows
  • iOS
  • Mac

Screenshots

Before Issue Fix After Issue Fix
28078BeforeFix.mov
28078AfterFix.mov

iOS:

Before Issue Fix After Issue Fix
BeforeFix28078.mov
AfterFix28078.mov
Code Review: ⚠️ Issues Found

Code Review Findings for PR #28241

✅ Positive Observations

  1. Minimal, surgical fix - Only adds 10 lines to fix a cross-platform issue
  2. Good state management - Uses flag pattern to track clear/add cycles
  3. Correct threshold detection - Identifies the exact transition point (Count == 2) where flyout behavior changes
  4. Includes comprehensive tests - UI test with screenshot verification across all platforms
  5. Type safety - Properly checks this is ShellItemCollection before casting
  6. Proper cleanup - Resets flag after notification to prevent duplicate calls

🟡 Suggestions

1. Consider Thread Safety

Current code:

bool _isItemsCleared;

Observation: _isItemsCleared is accessed/modified in Clear() and InnerCollectionChanged() without synchronization.

Risk: Low - Shell operations are typically on UI thread, but if items are modified from multiple threads, race conditions possible.

Recommendation: Document that Shell item manipulation must occur on UI thread, or consider using volatile/Interlocked if thread safety is required.


2. Magic Number Documentation

Current code:

if (_isItemsCleared && Count == 2 && this is ShellItemCollection shellItemCollection)

Observation: Count == 2 is a "magic number" without inline explanation.

Recommendation: Add inline comment explaining why 2 is the threshold:

// Count == 2 is the threshold where flyout transitions from Disabled (1 item) to Enabled (2+ items)
if (_isItemsCleared && Count == 2 && this is ShellItemCollection shellItemCollection)

3. Null Safety Pattern

Current code:

var shell = shellItemCollection[0].Parent as Shell;
shell?.NotifyFlyoutBehaviorObservers();

Observation: Assumes shellItemCollection[0] exists (Count == 2 guarantees this) and uses null-conditional operator for shell reference.

Recommendation: Consider adding defensive check or assertion:

if (shellItemCollection.Count < 1)
    return; // Should never happen given Count == 2 check

var shell = shellItemCollection[0].Parent as Shell;
shell?.NotifyFlyoutBehaviorObservers();

However: Given the Count == 2 check, current code is safe. This is more about defensive programming.


4. Edge Case: Rapid Clear/Add Cycles

Scenario: User calls Items.Clear() then adds 1 item, then calls Clear() again, then adds 2 items.

Current behavior:

  • First Clear: _isItemsCleared = true
  • Add 1 item: Flag stays true, Count == 1, no notification
  • Second Clear: _isItemsCleared = true (already true)
  • Add 2 items: Count == 2, notification triggered, flag reset

Observation: Handles correctly - flag is "sticky" until Count == 2 is reached.

Risk: None identified.


5. Platform Handler Initialization Timing

Observation: NotifyFlyoutBehaviorObservers() calls Handler?.UpdateValue() which requires platform handler to exist.

Question: What happens if items are replaced before handler is created?

From Shell.cs line 1972-1973:

if (CurrentItem == null || GetVisiblePage() == null)
    return;

Answer: Method guards against null CurrentItem/Page, so early returns safely. Handler?.UpdateValue() also uses null-conditional operator.

Verdict: Safely handled.


6. Test Coverage Gap: Count == 3+ Scenario

Current test (Issue28078.cs):

Items.Clear();
Items.Add(new _28708Page1());
Items.Add(new _28708Page2());

Observation: Test only validates 0 → 2 items. Doesn't test 0 → 3+ items.

Recommendation: Consider adding test case:

Items.Clear();
Items.Add(new Page1());
Items.Add(new Page2());
Items.Add(new Page3());

Risk: Low - Logic should handle Count >= 2 correctly (flag reset at Count == 2), but explicit test would verify.


7. Alternative: Use Count == 1 with Reset Delay

Alternative approach consideration:

Could the notification happen when Count transitions from 1 → 2, using previous count tracking?

int _previousCount;

void InnerCollectionChanged(...)
{
    // ...
    if (_previousCount == 1 && Count == 2 && this is ShellItemCollection shellItemCollection)
    {
        var shell = shellItemCollection[0].Parent as Shell;
        shell?.NotifyFlyoutBehaviorObservers();
    }
    _previousCount = Count;
}

Comparison:

  • ✅ More general (catches any 1 → 2 transition, not just after Clear)
  • ❌ More state to track
  • ❌ Doesn't handle non-sequential adds (Insert at index 0, etc.)

Verdict: Current approach with _isItemsCleared flag is simpler and sufficient for the reported issue.


🔴 Critical Issues

None identified. The fix is sound and addresses the reported issue correctly.


Summary

The code change is well-implemented with a focused fix for the reported issue. The suggestions above are minor improvements for code clarity and defensive programming, but are not blockers. The current implementation correctly handles the state transition and platform notification timing.

Recommendation: ✅ Ready to merge after addressing title/description feedback. Code changes are solid.

@rmarinho rmarinho added s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-gate-failed AI could not verify tests catch the bug labels Feb 14, 2026
@kubaflo kubaflo added s/agent-fix-lose AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Feb 15, 2026
kubaflo
kubaflo previously approved these changes Feb 15, 2026
Copy link
Member

@PureWeen PureWeen left a comment

Choose a reason for hiding this comment

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

@Vignesh-SF3580 can we use unit tests here instead of UITests?

@kubaflo kubaflo added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-lose AI could not beat the PR fix - PR is the best among all candidates labels Feb 20, 2026
@kubaflo kubaflo added the s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation label Feb 20, 2026
@Vignesh-SF3580
Copy link
Contributor Author

can we use unit tests here instead of UITests?

@PureWeen I’ve added a unit test to cover this scenario.

@kubaflo
Copy link
Contributor

kubaflo commented Feb 23, 2026

/azp run maui-pr-uitests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

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

Labels

area-controls-shell Shell Navigation, Routes, Tabs, Flyout community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell crashes when tapping on flyout menu item after items replaced

6 participants