Skip to content

Commit c9d0abf

Browse files
Fix: Null Reference Exception in ShellContentFragment.Destroy (#29713)
* Fix null reference exception when removing appearance observer in ShellContentFragment * Change to use `as` syntax * Add test to verify ShellContentFragment.Destroy handles null _shellContext gracefully * Add test to ensure ShellContentFragment.Destroy handles null _shellContext gracefully * Add safeguard in ShellContentFragment.Destroy to prevent multiple calls
1 parent cdfb373 commit c9d0abf

File tree

2 files changed

+62
-1
lines changed

2 files changed

+62
-1
lines changed

src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance)
7171
AToolbar _toolbar;
7272
IShellToolbarTracker _toolbarTracker;
7373
bool _disposed;
74+
bool _destroyed;
7475

7576
public ShellContentFragment(IShellContext shellContext, ShellContent shellContent)
7677
{
@@ -167,13 +168,18 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,
167168

168169
void Destroy()
169170
{
171+
if (_destroyed)
172+
return;
173+
174+
_destroyed = true;
175+
170176
// If the user taps very quickly on back button multiple times to pop a page,
171177
// the app enters background state in the middle of the animation causing the fragment to be destroyed without completing the animation.
172178
// That'll cause `IAnimationListener.onAnimationEnd` to not be called, so we need to call it manually if something is still subscribed to the event
173179
// to avoid the navigation `TaskCompletionSource` to be stuck forever.
174180
AnimationFinished?.Invoke(this, EventArgs.Empty);
175181

176-
((IShellController)_shellContext.Shell).RemoveAppearanceObserver(this);
182+
(_shellContext?.Shell as IShellController)?.RemoveAppearanceObserver(this);
177183

178184
if (_shellContent != null)
179185
{

src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,61 @@ await CreateHandlerAndAddToWindow<ShellRenderer>(shell, (handler) =>
516516
});
517517
}
518518

519+
[Fact(DisplayName = "ShellContentFragment.Destroy handles null _shellContext gracefully")]
520+
public async Task ShellContentFragmentDestroyHandlesNullShellContext()
521+
{
522+
SetupBuilder();
523+
524+
var shell = await CreateShellAsync(shell =>
525+
{
526+
shell.Items.Add(new TabBar()
527+
{
528+
Items =
529+
{
530+
new ShellContent()
531+
{
532+
Route = "Item1",
533+
Content = new ContentPage { Title = "Page 1" }
534+
},
535+
new ShellContent()
536+
{
537+
Route = "Item2",
538+
Content = new ContentPage { Title = "Page 2" }
539+
},
540+
}
541+
});
542+
});
543+
544+
await CreateHandlerAndAddToWindow<ShellHandler>(shell, async (handler) =>
545+
{
546+
await OnLoadedAsync(shell.CurrentPage);
547+
await OnNavigatedToAsync(shell.CurrentPage);
548+
549+
// Navigate to trigger fragment creation
550+
await shell.GoToAsync("//Item2");
551+
await OnNavigatedToAsync(shell.CurrentPage);
552+
553+
// Test normal destruction - should work without issues
554+
await shell.GoToAsync("//Item1");
555+
await OnNavigatedToAsync(shell.CurrentPage);
556+
557+
// Test null context scenario
558+
var exception = Record.Exception(() =>
559+
{
560+
// Create fragment with null context - this should not throw
561+
Page page = new ContentPage();
562+
var fragment = new ShellContentFragment((IShellContext)null, page);
563+
564+
// Dispose the fragment which calls Destroy internally
565+
// This validates the null-conditional operators in Destroy method
566+
fragment.Dispose();
567+
});
568+
569+
// Verify no exception was thrown - validates (_shellContext?.Shell as IShellController)?.RemoveAppearanceObserver(this);
570+
Assert.Null(exception);
571+
});
572+
}
573+
519574
protected AView GetFlyoutPlatformView(ShellRenderer shellRenderer)
520575
{
521576
var drawerLayout = GetDrawerLayout(shellRenderer);

0 commit comments

Comments
 (0)