-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Fix Changing Content property of ShellContent doesn't change visual content #34630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4494082
fa52f4b
41267ad
8b7624e
66f06f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,26 @@ void OnShellContentPropertyChanged(object sender, System.ComponentModel.Property | |
| { | ||
| UpdateTabTitle(shellContent); | ||
| } | ||
| else if (e.PropertyName == ShellContent.ContentProperty.PropertyName && sender is ShellContent changedContent) | ||
| { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Navigation & Shell — This handles |
||
| // The page inside this ShellContent changed — force ViewPager2 to recreate the | ||
| // fragment so it picks up the new content. | ||
| if (_viewPager?.Adapter is ShellFragmentStateAdapter adapter) | ||
| { | ||
| adapter.InvalidateShellContent(changedContent); | ||
| SafeNotifyDataSetChanged(); | ||
|
devanathan-vaithiyanathan marked this conversation as resolved.
|
||
|
|
||
| // Keep toolbar state in sync when the active tab's content page is replaced. | ||
| if (ShellSection?.CurrentItem == changedContent && _toolbarTracker is not null) | ||
| { | ||
| var page = ((IShellContentController)changedContent).GetOrCreateContent(); | ||
| if (page is not null) | ||
| { | ||
| _toolbarTracker.Page = page; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void UpdateTabTitle(ShellContent shellContent) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,6 +101,9 @@ public override void ViewDidLoad() | |
| ShellSection.PropertyChanged += OnShellSectionPropertyChanged; | ||
| ShellSectionController.ItemsCollectionChanged += OnShellSectionItemsChanged; | ||
|
|
||
| foreach (var item in ShellSectionController.GetItems()) | ||
| item.PropertyChanged += OnShellContentPropertyChanged; | ||
|
|
||
| _blurView = new UIView(); | ||
| UIVisualEffect blurEffect = UIBlurEffect.FromStyle(UIBlurEffectStyle.ExtraLight); | ||
| _blurView = new UIVisualEffectView(blurEffect); | ||
|
|
@@ -151,6 +154,11 @@ void IDisconnectable.Disconnect() | |
| if (ShellSectionController != null) | ||
| ShellSectionController.ItemsCollectionChanged -= OnShellSectionItemsChanged; | ||
|
|
||
| var shellContents = ShellSectionController?.GetItems(); | ||
| if (shellContents != null) | ||
| foreach (var item in shellContents) | ||
| item.PropertyChanged -= OnShellContentPropertyChanged; | ||
|
|
||
| if (_shellContext?.Shell != null) | ||
| _shellContext.Shell.PropertyChanged -= HandleShellPropertyChanged; | ||
|
|
||
|
|
@@ -500,6 +508,8 @@ void OnShellSectionItemsChanged(object sender, NotifyCollectionChangedEventArgs | |
| { | ||
| foreach (ShellContent oldItem in e.OldItems) | ||
| { | ||
| oldItem.PropertyChanged -= OnShellContentPropertyChanged; | ||
|
|
||
| // if current item is removed will be handled by the currentitem property changed event | ||
| // That way the render is swapped out cleanly once the new current item is set | ||
| if (_currentContent == oldItem) | ||
|
|
@@ -524,6 +534,8 @@ void OnShellSectionItemsChanged(object sender, NotifyCollectionChangedEventArgs | |
| { | ||
| foreach (ShellContent newItem in e.NewItems) | ||
| { | ||
| newItem.PropertyChanged += OnShellContentPropertyChanged; | ||
|
|
||
| if (_renderers.ContainsKey(newItem)) | ||
| continue; | ||
|
|
||
|
|
@@ -535,6 +547,75 @@ void OnShellSectionItemsChanged(object sender, NotifyCollectionChangedEventArgs | |
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 iOS: If Low probability since a user would have to change a page's content mid-swipe, but consider adding: if (_isAnimatingOut != null && _isAnimatingOut == oldRenderer)
return; // content change during animation — skip; animation completion will handle layout |
||
| } | ||
|
|
||
| void OnShellContentPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) | ||
| { | ||
| if (_isDisposed) | ||
| return; | ||
|
|
||
| if (e.PropertyName == ShellContent.ContentProperty.PropertyName && sender is ShellContent shellContent) | ||
| { | ||
| // INotifyPropertyChanged.PropertyChanged fires AFTER the BindableProperty's | ||
| // propertyChanged callback (OnContentChanged) in BindableObject.SetValueCore. | ||
| // For a Page, ContentCache is already updated by the time we get here. | ||
| if (shellContent.Content is Page newPage) | ||
| { | ||
| // Content is a Page — available immediately, no deferral needed. | ||
| UpdateRendererForShellContent(shellContent, newPage); | ||
| } | ||
| else if (shellContent.Content is DataTemplate) | ||
| { | ||
| // Content is a DataTemplate — ContentCache is populated by OnContentChanged, | ||
| // which has already run before this PropertyChanged notification. However, | ||
| // defer to the next run-loop iteration to ensure any async post-processing | ||
| // in GetOrCreateContent() sees a fully settled ContentCache. | ||
| BeginInvokeOnMainThread(() => | ||
| { | ||
| if (_isDisposed) | ||
| return; | ||
| var page = ((IShellContentController)shellContent).GetOrCreateContent(); | ||
| UpdateRendererForShellContent(shellContent, page); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void UpdateRendererForShellContent(ShellContent shellContent, Page newPage) | ||
| { | ||
| if (newPage == null) | ||
| return; | ||
|
|
||
| if (!_renderers.TryGetValue(shellContent, out var oldRenderer)) | ||
| return; | ||
|
|
||
| // If the existing renderer is already showing the new page, nothing to do. | ||
| if (oldRenderer.VirtualView == newPage) | ||
| return; | ||
|
|
||
| bool isCurrentContent = shellContent == _currentContent; | ||
|
|
||
| // Remove the old renderer | ||
| if (isCurrentContent) | ||
| oldRenderer.ViewController?.ViewIfLoaded?.RemoveFromSuperview(); | ||
|
|
||
| oldRenderer.ViewController?.RemoveFromParentViewController(); | ||
| oldRenderer.DisconnectHandler(); | ||
| _renderers.Remove(shellContent); | ||
|
|
||
| // Create a new renderer for the updated page | ||
| var renderer = SetPageRenderer(newPage, shellContent); | ||
| AddChildViewController(renderer.ViewController); | ||
|
|
||
| if (isCurrentContent) | ||
| { | ||
| _containerArea.AddSubview(renderer.ViewController.View); | ||
| renderer.ViewController.View.Frame = _containerArea.Bounds; | ||
| UpdateAdditionalSafeAreaInsets(renderer); | ||
|
|
||
| if (_tracker != null) | ||
| _tracker.Page = newPage; | ||
| } | ||
| } | ||
|
|
||
| IPlatformViewHandler SetPageRenderer(Page page, ShellContent shellContent) | ||
| { | ||
| page.Handler?.DisconnectHandler(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| namespace Maui.Controls.Sample.Issues; | ||
|
|
||
| [Issue(IssueTracker.Github, 12669, "Changing Content property of ShellContent doesn't change visual content", PlatformAffected.All)] | ||
| public class Issue12669 : TestShell | ||
| { | ||
| protected override void Init() | ||
| { | ||
| FlyoutBehavior = FlyoutBehavior.Disabled; | ||
|
|
||
| var shellContent = new ShellContent { Title = "Test" }; | ||
|
|
||
| var changeButton = new Button | ||
| { | ||
| Text = "Change Content", | ||
| AutomationId = "ChangeContentButton" | ||
| }; | ||
|
|
||
| shellContent.Content = new ContentPage | ||
| { | ||
| Content = new VerticalStackLayout | ||
| { | ||
| VerticalOptions = LayoutOptions.Center, | ||
| HorizontalOptions = LayoutOptions.Center, | ||
| Children = | ||
| { | ||
| new Label | ||
| { | ||
| Text = "Original Content", | ||
| AutomationId = "OriginalContent" | ||
| }, | ||
| changeButton | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| changeButton.Clicked += (s, e) => | ||
| { | ||
| shellContent.ContentTemplate = null; | ||
| shellContent.Content = new ContentPage | ||
| { | ||
| Content = new VerticalStackLayout | ||
| { | ||
| VerticalOptions = LayoutOptions.Center, | ||
| HorizontalOptions = LayoutOptions.Center, | ||
| Children = | ||
| { | ||
| new Label | ||
| { | ||
| Text = "Content Changed", | ||
| AutomationId = "NewContent" | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| }; | ||
|
|
||
| var tab = new Tab { Title = "Main" }; | ||
| tab.Items.Add(shellContent); | ||
| Items.Add(tab); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| using NUnit.Framework; | ||
| using UITest.Appium; | ||
| using UITest.Core; | ||
|
|
||
| namespace Microsoft.Maui.TestCases.Tests.Issues; | ||
| public class Issue12669 : _IssuesUITest | ||
| { | ||
| public Issue12669(TestDevice device) : base(device) { } | ||
|
|
||
| public override string Issue => "Changing Content property of ShellContent doesn't change visual content"; | ||
|
|
||
| [Test] | ||
| [Category(UITestCategories.Shell)] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Test coverage gaps — three uncovered scenarios:
|
||
| public void ShellContentShouldUpdateWhenContentPropertyChanges() | ||
| { | ||
| App.WaitForElement("OriginalContent"); | ||
| App.WaitForElement("ChangeContentButton"); | ||
|
|
||
| App.Tap("ChangeContentButton"); | ||
|
|
||
| // After clicking, the ShellContent.Content is changed to a new page. | ||
| // The visual should update to show the new content. | ||
| App.WaitForElement("NewContent"); | ||
| var labelText = App.FindElement("NewContent").GetText(); | ||
| Assert.That(labelText, Is.EqualTo("Content Changed")); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HookEvents()subscribesOnShellContentPropertyChangedonly for the items present when the renderer is created, and Android'sOnItemsCollectionChangedonly updates the adapter. Unlike the iOS change, it does not subscribee.NewItemsor unsubscribee.OldItems. AShellContentadded after renderer creation will therefore never reach this newContentPropertybranch when itsContentchanges, so its fragment will not be invalidated/recreated on Android. Removed items also remain subscribed until teardown.