Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Controls/src/Core/Shell/ShellContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,35 @@ shellSection.Parent is ShellItem shellItem &&
this.Window is null; // user has set the main page to a different shell instance
}

// Additional check: Don't disconnect if we're just switching between tabs in the same section
Copy link
Owner

Choose a reason for hiding this comment

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

Despite these changes, the Loaded event is still triggered multiple times , this issue occurs only on Android.

Copy link
Author

Choose a reason for hiding this comment

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

I've implemented an Android-specific fix to address the multiple Loaded events issue. The problem was that Android's Fragment lifecycle in Shell navigation causes ViewAttachedToWindow/ViewDetachedFromWindow events to fire multiple times during tab switching, even with the initial ShellContent fix.

The solution adds platform-specific tracking in VisualElement to prevent redundant Loaded events by:

  • Tracking the last platform view instance to detect genuine view changes vs fragment lifecycle events
  • Only firing Loaded events when switching to a genuinely new platform view or transitioning from unloaded state
  • Clearing the tracked view on handler changes and unload events

This should resolve the Android-specific multiple Loaded events while maintaining the cross-platform fix for Shell disconnect logic. Commit: 64ade55

// This prevents unnecessary page recreation and multiple Loaded events
// Fixes: https://github.com/dotnet/maui/issues/11
if (disconnect && Parent is ShellSection currentSection &&
currentSection.Parent is ShellItem currentItem &&
shell?.CurrentItem == currentItem)
{
// We're still within the same ShellItem, just switching tabs
// Only disconnect if this specific ShellContent is not the current one
// and it's not adjacent to the current one (to allow for smooth tab transitions)
if (currentSection.CurrentItem == this)
{
disconnect = false;
}
else
{
var items = currentSection.Items;
var currentIndex = items.IndexOf(currentSection.CurrentItem);
var thisIndex = items.IndexOf(this);

// Keep adjacent tabs loaded to allow for smooth transitions and swipe gestures
// This prevents the erratic Page.Loaded events described in the issue
if (currentIndex >= 0 && thisIndex >= 0 && Math.Abs(currentIndex - thisIndex) <= 1)
{
disconnect = false;
}
}
}

if (!disconnect)
{
shell?.NotifyFlyoutBehaviorObservers();
Expand Down
18 changes: 18 additions & 0 deletions src/Controls/src/Core/VisualElement/VisualElement.Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Microsoft.Maui.Controls
public partial class VisualElement
{
IDisposable? _loadedUnloadedToken;

partial void HandlePlatformUnloadedLoaded()
{
_loadedUnloadedToken?.Dispose();
Expand All @@ -27,7 +28,20 @@ partial void HandlePlatformUnloadedLoaded()
{
if (view.IsLoaded())
{
#if ANDROID
// Android-specific: Only send Loaded if this is a genuinely new platform view
// or if we haven't fired Loaded for this view yet. This prevents multiple
// Loaded events during Shell fragment lifecycle changes.
bool shouldSendLoaded = _lastPlatformView != view || !_isLoadedFired;
_lastPlatformView = view;

if (shouldSendLoaded)
{
SendLoaded(false);
}
#else
SendLoaded(false);
#endif

// If SendLoaded caused the unloaded tokens to wire up
_loadedUnloadedToken?.Dispose();
Expand Down Expand Up @@ -57,6 +71,10 @@ partial void HandlePlatformUnloadedLoaded()
}
else
{
#if ANDROID
// Android-specific: Clear the tracked platform view when unloading
_lastPlatformView = null;
#endif
SendUnloaded();
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/Controls/src/Core/VisualElement/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,11 @@ void UpdateBoundsComponents(Rect bounds)
EventHandler? _loaded;
EventHandler? _unloaded;
bool _watchingPlatformLoaded;
#if ANDROID
// Android-specific: Track the last platform view to prevent redundant Loaded events
// during Shell fragment lifecycle changes
object? _lastPlatformView;
#endif
Rect _frame = new Rect(0, 0, -1, -1);
event EventHandler? _windowChanged;
event EventHandler? _platformContainerViewChanged;
Expand Down Expand Up @@ -1819,6 +1824,11 @@ private protected override void OnHandlerChangedCore()
base.OnHandlerChangedCore();

IsPlatformEnabled = Handler != null;
#if ANDROID
// Android-specific: Reset tracked platform view when handler changes
// to ensure proper Loaded event handling with new platform views
_lastPlatformView = null;
#endif
UpdatePlatformUnloadedLoadedWiring(Window);
}

Expand Down
217 changes: 217 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/ShellPageLoadedIssue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using Microsoft.Maui.Controls;

namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.Github, 11, "For pages used in FlyoutItem/Tab/ShellContent the Page.Loaded event is called erratically", PlatformAffected.All)]
public class ShellPageLoadedIssue : Shell
{
public static Dictionary<string, int> PageLoadedCounts = new Dictionary<string, int>();
public static Dictionary<string, List<string>> LoadedPageSequence = new Dictionary<string, List<string>>();
public static string CurrentTestName = "";

public ShellPageLoadedIssue()
{
var tab = new Tab { Title = "The Pages", Route = "ViewCaseViewModel" };

tab.Items.Add(new ShellContent
{
Title = "Main Page",
ContentTemplate = new DataTemplate(() => new TestPage1()),
Route = "Route1"
});

tab.Items.Add(new ShellContent
{
Title = "Other Page",
ContentTemplate = new DataTemplate(() => new TestPage2()),
Route = "Route2"
});

tab.Items.Add(new ShellContent
{
Title = "Another Page",
ContentTemplate = new DataTemplate(() => new TestPage3()),
Route = "Route3"
});

var flyoutItem = new FlyoutItem
{
FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems
};
flyoutItem.Items.Add(tab);

this.Items.Add(flyoutItem);

// Add single shell contents
this.Items.Add(new ShellContent
{
Title = "Single Main",
ContentTemplate = new DataTemplate(() => new TestPage1()),
Route = "MainPage"
});

this.Items.Add(new ShellContent
{
Title = "Single Other",
ContentTemplate = new DataTemplate(() => new TestPage2()),
Route = "OtherPage"
});
}

public static void ResetCounters(string testName)
{
CurrentTestName = testName;
PageLoadedCounts.Clear();
if (!LoadedPageSequence.ContainsKey(testName))
LoadedPageSequence[testName] = new List<string>();
else
LoadedPageSequence[testName].Clear();
}

public static void RecordPageLoaded(string pageName)
{
if (!PageLoadedCounts.ContainsKey(pageName))
PageLoadedCounts[pageName] = 0;

PageLoadedCounts[pageName]++;

if (!string.IsNullOrEmpty(CurrentTestName))
{
if (!LoadedPageSequence.ContainsKey(CurrentTestName))
LoadedPageSequence[CurrentTestName] = new List<string>();
LoadedPageSequence[CurrentTestName].Add($"{pageName}:{PageLoadedCounts[pageName]}");
}
}
}

public class TestPage1 : ContentPage
{
public TestPage1()
{
Title = "Main Page";
AutomationId = "TestPage1";
Content = new StackLayout
{
Children =
{
new Label { Text = "This is Test Page 1", AutomationId = "Page1Label" },
new Button
{
Text = "Navigate to Other Page",
AutomationId = "NavigateToOtherButton",
Command = new Command(async () => await Shell.Current.GoToAsync("//Route2"))
},
new Button
{
Text = "Navigate to Another Page",
AutomationId = "NavigateToAnotherButton",
Command = new Command(async () => await Shell.Current.GoToAsync("//Route3"))
},
new Label { Text = GetLoadedCountText(), AutomationId = "LoadedCountLabel" }
}
};

this.Loaded += (s, e) =>
{
ShellPageLoadedIssue.RecordPageLoaded("TestPage1");
if (Content is StackLayout stack && stack.Children.Last() is Label label)
{
label.Text = GetLoadedCountText();
}
};
}

private string GetLoadedCountText()
{
return $"Page1 Loaded Count: {(ShellPageLoadedIssue.PageLoadedCounts.ContainsKey("TestPage1") ? ShellPageLoadedIssue.PageLoadedCounts["TestPage1"] : 0)}";
}
}

public class TestPage2 : ContentPage
{
public TestPage2()
{
Title = "Other Page";
AutomationId = "TestPage2";
Content = new StackLayout
{
Children =
{
new Label { Text = "This is Test Page 2", AutomationId = "Page2Label" },
new Button
{
Text = "Navigate to Main Page",
AutomationId = "NavigateToMainButton",
Command = new Command(async () => await Shell.Current.GoToAsync("//Route1"))
},
new Button
{
Text = "Navigate to Another Page",
AutomationId = "NavigateToAnotherButton",
Command = new Command(async () => await Shell.Current.GoToAsync("//Route3"))
},
new Label { Text = GetLoadedCountText(), AutomationId = "LoadedCountLabel" }
}
};

this.Loaded += (s, e) =>
{
ShellPageLoadedIssue.RecordPageLoaded("TestPage2");
if (Content is StackLayout stack && stack.Children.Last() is Label label)
{
label.Text = GetLoadedCountText();
}
};
}

private string GetLoadedCountText()
{
return $"Page2 Loaded Count: {(ShellPageLoadedIssue.PageLoadedCounts.ContainsKey("TestPage2") ? ShellPageLoadedIssue.PageLoadedCounts["TestPage2"] : 0)}";
}
}

public class TestPage3 : ContentPage
{
public TestPage3()
{
Title = "Another Page";
AutomationId = "TestPage3";
Content = new StackLayout
{
Children =
{
new Label { Text = "This is Test Page 3", AutomationId = "Page3Label" },
new Button
{
Text = "Navigate to Main Page",
AutomationId = "NavigateToMainButton",
Command = new Command(async () => await Shell.Current.GoToAsync("//Route1"))
},
new Button
{
Text = "Navigate to Other Page",
AutomationId = "NavigateToOtherButton",
Command = new Command(async () => await Shell.Current.GoToAsync("//Route2"))
},
new Label { Text = GetLoadedCountText(), AutomationId = "LoadedCountLabel" }
}
};

this.Loaded += (s, e) =>
{
ShellPageLoadedIssue.RecordPageLoaded("TestPage3");
if (Content is StackLayout stack && stack.Children.Last() is Label label)
{
label.Text = GetLoadedCountText();
}
};
}

private string GetLoadedCountText()
{
return $"Page3 Loaded Count: {(ShellPageLoadedIssue.PageLoadedCounts.ContainsKey("TestPage3") ? ShellPageLoadedIssue.PageLoadedCounts["TestPage3"] : 0)}";
}
}
}
Loading