Return value from Xamarin.Forms Shell modal

March 12th 2021 Xamarin

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 the Uri.EscapeDataString method when constructing the URL to navigate to the modal page in my Show 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 the AppShell 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.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License