Getting Xamarin.Forms Prism navigation service to throw exceptions

Hello Folks!

Today a quickie: since I don't know which version, the awesome Xamarin.Forms navigation framework Prism, stopped raising exceptions on navigation. It now returns a INavigationResult which has a Exception property...

The issue

Being a supporter of the component-oriented architecture, I love my Tasks.
When I navigate to a page, this action is embedded in a Task, and this Task is wrapped into a bindable object: the TaskLoaderCommand.
Doing so, I can bind my navigation command to a snackbar or whatever visual error I want:

public class CommandsPageViewModel : ANavigableViewModel
{
    private readonly IRetroGamingService _retroGamingService;

    public CommandsPageViewModel(INavigationService navigationService, IRetroGamingService retroGamingService)
        : base(navigationService)
    {
        _retroGamingService = retroGamingService;

        Loader = new TaskLoaderNotifier<Game>();

        BuyGameCommand = new TaskLoaderCommand(BuyGame);
        PlayTheGameCommand = new TaskLoaderCommand(PlayTheGame);

        CompositeNotifier = new CompositeTaskLoaderNotifier(
            BuyGameCommand.Notifier,
            PlayTheGameCommand.Notifier);
    }

    public CompositeTaskLoaderNotifier CompositeNotifier { get; }

    public TaskLoaderCommand BuyGameCommand { get; }

    public TaskLoaderCommand PlayTheGameCommand { get; }

    public TaskLoaderNotifier<Game> Loader { get; }

    public string LoadingText { get; set; }

    public override void OnNavigated(object parameter)
    {
        Loader.Load(() => GetRandomGame());
    }

    private async Task<Game> GetRandomGame()
    {
        await Task.Delay(TimeSpan.FromSeconds(2));

        return await _retroGamingService.GetRandomGame(true);
    }

    private async Task PlayTheGame()
    {
        LoadingText = "Loading the game...";
        RaisePropertyChanged(nameof(LoadingText));

        NavigationService.NavigateAsync("GamePlayer");
    }

    private async Task BuyGame()
    {
        LoadingText = "Proceeding to payment";
        RaisePropertyChanged(nameof(LoadingText));

        await Task.Delay(2000);
        throw new LocalizedException($"Sorry, we only accept DogeCoin...");
    }
}
<!-- Loading dialog -->
<AbsoluteLayout Grid.Row="1"
                BackgroundColor="#77002200"
                IsVisible="{Binding CompositeNotifier.ShowLoader}">
    <Grid x:Name="ErrorNotificationView"
            AbsoluteLayout.LayoutFlags="PositionProportional"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 300, 150"
            RowDefinitions="*,*">
        <Grid.Behaviors>
            <forms:TimedVisibilityBehavior VisibilityInMilliseconds="4000" />
        </Grid.Behaviors>
        <Image Grid.RowSpan="2"
                Aspect="Fill"
                Source="{inf:ImageResource Sample.Images.window_border.png}" />
        <Image x:Name="BusyImage"
                Margin="15,30,15,0"
                Aspect="AspectFit"
                Source="{inf:ImageResource Sample.Images.busy_bee_white_bg.png}" />
        <Label Grid.Row="1"
                Style="{StaticResource TextBody}"
                Margin="{StaticResource ThicknessLarge}"
                VerticalOptions="Center"
                HorizontalTextAlignment="Center"
                Text="{Binding LoadingText}" />
    </Grid>
</AbsoluteLayout>

<!-- SNACKBAR -->
<forms:Snackbar Grid.Row="1"
                Margin="15"
                VerticalOptions="End"
                BackgroundColor="White"
                FontFamily="{StaticResource FontAtariSt}"
                IsVisible="{Binding CompositeNotifier.ShowError, Mode=TwoWay}"
                Text="{Binding CompositeNotifier.LastError, Converter={StaticResource ExceptionToErrorMessageConverter}}"
                TextColor="{StaticResource TextPrimaryColor}"
                TextHorizontalOptions="Start" />

More about component-oriented architecture here: https://github.com/roubachof/Sharpnado.TaskLoaderView

Now you can see the issue, if Prism navigation service just swallows the exception, all the Task won't raise the exception and it will fail silently...

It's also very dull to debug, when creating new pages first navigations are unlikely to work on the first try. You will maybe have some xaml issues, or even code behind initialization issues. With the new Prism implementation, it will just silently fails...

Also, I really don't like to process all navigation results to see if an exception is thrown, it breaks the beauty of the Exception/Tasks duo.

The solution

Fortunately, the Prism framework is really easily extensible, so fixing this behavior is pretty straightforward.
We just have to extend the PageNavigationService and make it raise the exception:

using System;
using System.Threading.Tasks;

using Prism.Behaviors;
using Prism.Common;
using Prism.Ioc;
using Prism.Navigation;

namespace MyCompany.Navigation;

public class PageNavigationRaisingExceptionService : PageNavigationService
{
    public PageNavigationRaisingExceptionService(
        IContainerProvider container,
        IApplicationProvider applicationProvider,
        IPageBehaviorFactory pageBehaviorFactory)
        : base(container, applicationProvider, pageBehaviorFactory)
    {
    }

    protected override async Task<INavigationResult> GoBackInternal(
        INavigationParameters parameters,
        bool? useModalNavigation,
        bool animated)
    {
        var result = await base.GoBackInternal(parameters, useModalNavigation, animated);
        if (result.Exception != null)
        {
            throw result.Exception;
        }

        return result;
    }

    protected override async Task<INavigationResult> GoBackToRootInternal(INavigationParameters parameters)
    {
        var result = await base.GoBackToRootInternal(parameters);
        if (result.Exception != null)
        {
            throw result.Exception;
        }

        return result;
    }

    protected override async Task<INavigationResult> NavigateInternal(
        Uri uri,
        INavigationParameters parameters,
        bool? useModalNavigation,
        bool animated)
    {
        var result = await base.NavigateInternal(uri, parameters, useModalNavigation, animated);
        if (result.Exception != null)
        {
            throw result.Exception;
        }

        return result;
    }
}

Then we register the new implementation after the old one, in the App.xaml.cs:

namespace MyCompany
{
    public partial class App
    {
        public App(IPlatformInitializer initializer)
            : base(initializer)
        {
        }
        
        protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)
        {
            base.RegisterRequiredTypes(containerRegistry);
            containerRegistry.RegisterScoped<INavigationService, PageNavigationRaisingExceptionService>();
            containerRegistry.Register<INavigationService, PageNavigationRaisingExceptionService>(NavigationServiceName);
        }
    }
}

And hooray! Navigation errors are now taken care of automatically thanks to our task-oriented architecture \o/

You can find the gist here: https://gist.github.com/roubachof/b127000a91e5054dfd73179344da2ecc