PrismLibrary / Prism

Prism is a framework for building loosely coupled, maintainable, and testable XAML applications in WPF, Xamarin Forms, and Uno / Win UI Applications..

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[BUG] When using iOS ModalPresentationStyles that can be automatically dismissed, navigation service gets out of sync & stops working with NavigationPage

Axemasta opened this issue · comments

Description

This is the issue I have been discussing in the discord.

When working with a modal navigation page, where the page in question has the following iOS specific presentation style:

On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.PageSheet);

Closing the popover by using the iOS swipe gesture (using Prism to close the modal works as intended), will cause the navigation service to lock up.

I have investigated with a local sample connected to the prism source code and the following happens:

  • Prism does not know the current page has been dismissed & holds a reference to it
  • The first time you call the navigation service, there will be an error output in the console:
[Presentation] Attempt to present <Microsoft_Maui_Controls_Platform_ControlsModalWrapper: 0x107931000> on <Microsoft_Maui_Controls_Platform_ControlsModalWrapper: 0x1087425e0> (from <Microsoft_Maui_Controls_Handlers_Compatibility_NavigationRenderer: 0x10c842400>) whose view is not in the window hierarchy.
  • Subsequent calls to the navigation service get deadlocked because the original semaphore for the previous attempt doesn't get cleared, this looks like the thread actually crashes since the finally & exception blocks on NavigateAsync don't get called.

Normal content pages presented in this way are unaffected, I believe this is because prism is listening for their disappeared events which do fire.

Steps to Reproduce

  1. Create a prism app
  2. Add a modal page to navigate to:
private async void OnNavPopover()
{
    var nav = await navigationService.CreateBuilder()
        .AddNavigationPage(true)
        .AddSegment<PopoverViewModel>()
        .NavigateAsync();

    if (!nav.Success)
    {
        Debugger.Break();
    }
}
3. Swipe the popover closed
4. Try to navigate again, nothing happens & the following warning is present in the console:

[Presentation] Attempt to present <Microsoft_Maui_Controls_Platform_ControlsModalWrapper: 0x107931000> on <Microsoft_Maui_Controls_Platform_ControlsModalWrapper: 0x1087425e0> (from <Microsoft_Maui_Controls_Handlers_Compatibility_NavigationRenderer: 0x10c842400>) whose view is not in the window hierarchy.

5. The navigation service is now soft locked and can't be used for any navigation

### Platform with bug

.NET MAUI

### Affected platforms

iOS

### Did you find any workaround?

I will be raising a PR to address this issue, if the following code is added to the `PrismNavigationPage`, `GoBackAsync` can be called after the page removal to ensure that the navigation service remains in sync with the app's page stack:
```csharp
protected override async void OnDisappearing()
{
    var presentationStyle = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Page.GetModalPresentationStyle(this);

    if (presentationStyle == UIModalPresentationStyle.FullScreen)
    {
        return;
    }

    if (PageNavigationService.NavigationSource == PageNavigationSource.Device)
    {
        await MvvmHelpers.HandleNavigationPageSwipedAway(this);
    }

    base.OnDisappearing();
}

Relevant log output

No response

Prism iOS Modal Gif

Here is a little clip from my repro sample, you can see that as soon as I close the navigation page popover, the other commands stop working, the view model code is below & is incredibly simple:

public class MainViewModel : ViewModelBase
{
    public DelegateCommand NavigateCommand { get; }
    
    public DelegateCommand ModalCommand { get; }
    
    public DelegateCommand PopoverCommand { get; }
    
    public DelegateCommand NavPopoverCommand { get; }
    
    public MainViewModel(INavigationService navigationService) 
        : base(navigationService)
    {
        Title = "Main Page";

        NavigateCommand = new DelegateCommand(OnNavigate);
        ModalCommand = new DelegateCommand(OnModal);
        PopoverCommand = new DelegateCommand(OnPopover);
        NavPopoverCommand = new DelegateCommand(OnNavPopover);
    }

    private async void OnNavigate()
    {
        var nav = await navigationService.CreateBuilder()
            .AddSegment<AnotherViewModel>()
            .NavigateAsync();

        if (!nav.Success)
        {
            Debugger.Break();
        }
    }
    
    private async void OnModal()
    {
        var navParams = new NavigationParameters()
        {
            { KnownNavigationParameters.UseModalNavigation, false },
        };

        var nav = await navigationService.CreateBuilder()
            .AddNavigationPage(true)
            .AddSegment<AnotherViewModel>()
            .WithParameters(navParams)
            .NavigateAsync();

        if (!nav.Success)
        {
            Debugger.Break();
        }
    }
    
    private async void OnPopover()
    {
        var nav = await navigationService.CreateBuilder()
            .AddSegment<PopoverViewModel>(true)
            .NavigateAsync();

        if (!nav.Success)
        {
            Debugger.Break();
        }
    }

    private async void OnNavPopover()
    {
        var nav = await navigationService.CreateBuilder()
            .AddNavigationPage(true)
            .AddSegment<PopoverViewModel>()
            .NavigateAsync();

        if (!nav.Success)
        {
            Debugger.Break();
        }
    }
}

closed by #3072