Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ internal static IMauiHandlersCollection AddControlsHandlers(this IMauiHandlersCo

#if IOS || MACCATALYST
handlersCollection.AddHandler(typeof(NavigationPage), typeof(Handlers.Compatibility.NavigationRenderer));
handlersCollection.AddHandler(typeof(TabbedPage), typeof(Handlers.Compatibility.TabbedRenderer));
handlersCollection.AddHandler<TabbedPage, TabbedViewHandler>();
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Regression Prevention / Architectural Layer Placement — This unconditionally flips every iOS/MacCatalyst TabbedPage from Handlers.Compatibility.TabbedRenderer to the brand-new TabbedViewHandler for all consumers, with no feature switch/opt-out. The linked issue (#33082) called for a staged rollout behind an AppContext switch disabled by default, but that switch was removed here. Given the size of the new native tab-bar implementation (TabBarControllerManager, MauiTabBarController, ~1300 new lines) and that the only test-asset change in this PR is a single updated snapshot PNG, this is a very high blast-radius change to ship as an unconditional default. Recommend gating behind a RuntimeFeature-style switch (default off) until broader device/UI regression coverage exists, consistent with the linked issue's rollout plan.

handlersCollection.AddHandler(typeof(FlyoutPage), typeof(Handlers.Compatibility.PhoneFlyoutPageRenderer));
#endif

Expand Down
11 changes: 7 additions & 4 deletions src/Controls/src/Core/TabbedPage/TabbedPage.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ public partial class TabbedPage

#endif

#if WINDOWS || ANDROID || TIZEN
#if PLATFORM
TabbedViewHandler.PlatformViewFactory = OnCreatePlatformView;
#endif

#if IOS
TabbedViewHandler.Mapper.ReplaceMapping<TabbedPage, ITabbedViewHandler>(nameof(PlatformConfiguration.iOSSpecific.Page.PrefersHomeIndicatorAutoHiddenProperty), MapPrefersHomeIndicatorAutoHiddenProperty);
TabbedViewHandler.Mapper.ReplaceMapping<TabbedPage, ITabbedViewHandler>(nameof(PlatformConfiguration.iOSSpecific.Page.PrefersStatusBarHiddenProperty), MapPrefersPrefersStatusBarHiddenProperty);
#if IOS || MACCATALYST
TabbedViewHandler.Mapper.ReplaceMapping<TabbedPage, ITabbedViewHandler>(nameof(FlowDirection), MapFlowDirection);
TabbedViewHandler.Mapper.ReplaceMapping<TabbedPage, ITabbedViewHandler>(PlatformConfiguration.iOSSpecific.Page.PrefersHomeIndicatorAutoHiddenProperty.PropertyName, MapPrefersHomeIndicatorAutoHiddenProperty);
TabbedViewHandler.Mapper.ReplaceMapping<TabbedPage, ITabbedViewHandler>(PlatformConfiguration.iOSSpecific.Page.PrefersStatusBarHiddenProperty.PropertyName, MapPrefersPrefersStatusBarHiddenProperty);
TabbedViewHandler.Mapper.ReplaceMapping<TabbedPage, ITabbedViewHandler>(PlatformConfiguration.iOSSpecific.Page.PreferredStatusBarUpdateAnimationProperty.PropertyName, MapPreferredStatusBarUpdateAnimation);
TabbedViewHandler.Mapper.ReplaceMapping<TabbedPage, ITabbedViewHandler>(PlatformConfiguration.iOSSpecific.TabbedPage.TranslucencyModeProperty.PropertyName, MapTranslucencyMode);
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
#endif
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
}
}
Expand Down
57 changes: 56 additions & 1 deletion src/Controls/src/Core/TabbedPage/TabbedPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ public partial class TabbedPage : MultiPage<Page>, IBarElement, IElementConfigur

readonly Lazy<PlatformConfigurationRegistry<TabbedPage>> _platformConfigurationRegistry;

// Stores the collection change args from OnPagesChanged so MapItemsSource
// can handle Add/Remove incrementally instead of full rebuild.
internal NotifyCollectionChangedEventArgs _pendingPagesChangedArgs;

// Stores the page whose Title/Icon changed so the mapper can refresh
// only that page's tab bar item instead of all children.
internal Page _pendingPropertyChangedPage;

// Tracks pages with active PropertyChanged subscriptions so they can be
// unsubscribed on Reset (where Children is already empty and e.OldItems is null).
HashSet<Page> _subscribedPages;

/// <summary>Gets or sets the background color of the tab bar. This is a bindable property.</summary>
public Color BarBackgroundColor
{
Expand Down Expand Up @@ -110,7 +122,38 @@ private protected override void OnHandlerChangingCore(HandlerChangingEventArgs a
void OnPagesChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
WireUnwireChanges(false);

// Unsubscribe removed pages — they're no longer in Children after mutation.
// On Reset, e.OldItems is null and Children is already empty, so use _subscribedPages.
if (e.OldItems is not null)
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
{
foreach (var item in e.OldItems)
{
if (item is Page page)
{
page.PropertyChanged -= OnPagePropertyChanged;
_subscribedPages?.Remove(page);
}
}
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset
&& _subscribedPages is not null)
{
// Reset path: Children is already empty, e.OldItems is null.
// Unsubscribe all previously tracked pages.
foreach (var page in _subscribedPages)
{
page.PropertyChanged -= OnPagePropertyChanged;
}
_subscribedPages.Clear();
}

_pendingPagesChangedArgs = e;
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Handler?.UpdateValue(TabbedPage.ItemsSourceProperty.PropertyName);

// Clear after UpdateValue — iOS mapper consumes it synchronously during the call above.
// On other platforms the mapper doesn't use it, so clear to avoid retaining removed pages.
_pendingPagesChangedArgs = null;
WireUnwireChanges(true);
}

Expand All @@ -119,16 +162,28 @@ void WireUnwireChanges(bool wire)
foreach (var page in Children)
{
if (wire)
{
page.PropertyChanged += OnPagePropertyChanged;
_subscribedPages ??= new HashSet<Page>();
_subscribedPages.Add(page);
}
else
{
page.PropertyChanged -= OnPagePropertyChanged;
_subscribedPages?.Remove(page);
}
}
}

void OnPagePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == Page.TitleProperty.PropertyName)
if (e.PropertyName == Page.TitleProperty.PropertyName ||
e.PropertyName == Page.IconImageSourceProperty.PropertyName)
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
Comment thread
Tamilarasan-Paranthaman marked this conversation as resolved.
{
_pendingPropertyChangedPage = sender as Page;
Handler?.UpdateValue(TabbedPage.ItemsSourceProperty.PropertyName);
_pendingPropertyChangedPage = null;
}
}
}

Expand Down
Loading
Loading