Return value from Xamarin.Forms Shell modal
Xamarin.Forms Shell makes navigation in Xamarin.Forms apps much simpler by using URL based routes instead of the NavigationPage
-based hierarchical navigation.
It works well even for modal pages. You only need to set the Shell.PresentationMode
accordingly in the page XAML template:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ShellModalReturnValues.Views.ModalPage"
Shell.PresentationMode="ModalAnimated">
To make it easy to use the modal page from elsewhere in code, I wanted to implement a method with the following signature:
public async Task<bool> Show(string msg);
The method would open a modal page with the given message and return a Task<bool>
that would complete after the user closed the modal page by tapping one of the two buttons.
Passing the msg
parameter to the modal page could easily be implemented by following the official documentation:
Adding the
QueryPropertyAttribute
to the page instructs the Shell to parse the query parameter from the URL:[QueryProperty(nameof(Message), nameof(Message))] [XamlCompilation(XamlCompilationOptions.Compile)] public partial class ModalPage : ContentPage
It will assign the parsed value to the specified property. In the property setter I simply pass the value to the view model:
public string Message { get => ViewModel.Message; set => ViewModel.Message = Uri.UnescapeDataString(value ?? string.Empty); }
Since the value is passed as a query parameter in the URL, I call the
Uri.UnescapeDataString
method to decode it. I also need to encode it using theUri.EscapeDataString
method when constructing the URL to navigate to the modal page in myShow
method:await Shell.Current.GoToAsync( $"{nameof(ModalPage)}?{nameof(ModalPage.Message)}={Uri.EscapeDataString(msg)}");
Of course, this assumes that I have previously registered the route for
ModalPage
in theAppShell
class:
Routing.RegisterRoute(nameof(ModalPage), typeof(ModalPage));
The modal can be closed by navigating to the parent URL:
private async Task Close(bool result)
{
await Shell.Current.GoToAsync("..");
}
While I could include a query parameter in the above URL, it didn't get parsed in the page I was navigating back to even if I correctly configured the QueryPropertyAttribute
for it. But that would be inconvenient anyway because my Show
method wouldn't work as planned without adding that attribute to any page that wanted to call the modal.
It would be much more elegant if the modal page (or its view model in my case) would raise an event with the appropriate value when being closed:
public class ModalClosedEventArgs : EventArgs
{
public ModalClosedEventArgs(bool result)
{
Result = result;
}
public bool Result { get; }
}
public event EventHandler<ModalClosedEventArgs> Closed;
private async Task Close(bool result)
{
await Shell.Current.GoToAsync("..");
Closed?.Invoke(this, new ModalClosedEventArgs(result));
}
There was just the matter of attaching a handler to that event in my Show
method. How could I get a reference to the modal page I am opening and its view modal? I managed to achieve that using the Shell's Navigated
event. I add aa handler to it before navigating to the modal page:
public static async Task<bool> Show(string msg)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ModalViewModel viewModel = null;
void ClosedHandler(object sender, ModalClosedEventArgs e)
{
viewModel.Closed -= ClosedHandler;
taskCompletionSource.SetResult(e.Result);
}
void NavigatedHandler(object sender, ShellNavigatedEventArgs e)
{
if (Shell.Current.CurrentPage is ModalPage page)
{
viewModel = page.ViewModel;
page.ViewModel.Closed += ClosedHandler;
}
Shell.Current.Navigated -= NavigatedHandler;
}
Shell.Current.Navigated += NavigatedHandler;
await Shell.Current.GoToAsync(
$"{nameof(ModalPage)}?{nameof(ModalPage.Message)}={Uri.EscapeDataString(msg)}");
return await taskCompletionSource.Task;
}
Inside the handler I check that the CurrentPage
matches my expectations, add a handler to the Closed
event on the modal view model and immediately remove the handler from the Navigated
event to avoid memory leaks.
Similarly, I immediately remove the handler from the Closed
event when it is first raised. I use a TaskCompletionSource
to create and complete the task returned from my Show
method.
So far this approach works very reliably. Hopefully, it won't break in a future version of Xamarin.Forms. At least not before proper support for returning values from modals is added to Shell.
To see all of the above code combined in a working example, you can take a look in my GitHub repository.
Although there's no support for returning values from modal pages when using Xamarin.Forms Shell navigation, it can be implemented using the tools that are available. The Shell's Navigated
event is raised whenever a new page is navigated to, and the CurrentPage
property provides access to that page. This makes it possible to directly interact with the page or its view model, for example, to add a handler to an event that's raised with the return value of the modal page.