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