iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔗

Type-Safe Navigation Pattern for Prism for Xamarin.Forms

に公開

A while ago, I introduced ViewModel First navigation in Prism for Xamarin.Forms.

http://www.nuits.jp/embed/2016/11/07/185707

However, there were two points of concern (not necessarily bad) in that example:

  1. The ViewModel is dependent on the class reference of another ViewModel.
  2. Deep Linking is ultimately dependent on strings.

So, I thought about ways to solve these issues.

I gave it some thought, but to be honest, I think it is overkill.

Therefore, I titled this the "Type Safe Navigation Perfectionist Pattern."

Moreover, this method still leaves the issue of "where to define the parameter keys for navigation between screens" unresolved, and trying to solve this would raise the overkill level even further.

I have my own preferred method that I consider the best, but first, I would like to share this as a slightly excessive anti-pattern.

As for the code in this post, I have made it available on GitHub under the solution name "TypeSafeNavigation," so please take a look.

//hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fnuitsjp%2FPrimerOfPrismForms%2Ftree%2Fmaster%2F03.NavigationService

Design Policy

  • Maintain type safety during navigation by defining navigation destinations using an Enum.

Pros

  • Complete type safety for navigation is maintained.
    The ViewModel First navigation I explained previously allows passing objects other than ViewModels to navigation, so it cannot be called fully type-safe.
  • Supports Deep Linking.
    However, technically, even with ViewModel First, you can achieve Deep Linking using the same method.

Cons

  • An extension framework for navigation must be created every time and cannot be reused.
    (This is because it uses an Enum for navigation, but there might be a way, assuming my C# skills were not sufficient.)
  • The definition location for navigation parameter keys is not specified, leaving issues with parameter exchange.

Implementing the Navigation Framework

Defining an Enum to represent navigation destinations

In this example, I will explain using NavigationPage with two screens: MainPage and SecondPage.

Define an Enum to represent the destinations as follows:

    public enum NavigateDestination
    {
        NavigationPage,
        MainPage,
        SecondPage
    }

Creating an extension method to register Views to the DI container

Create an extension method to register Views to the container using the Enum as a key as follows.

This time, it is defined in a class named NavigationExtensions.cs.

public static IUnityContainer RegisterTypeForNavigation<TView>(
    this IUnityContainer unityContainer, 
    NavigateDestination navigateDestination) where TView : Page
{
    return unityContainer.RegisterTypeForNavigation<TView>(
        navigateDestination.ToString());
}

It simply converts the Enum to a string and calls the existing IUnityContainer's RegisterTypeForNavigation method.

I could create an Attribute to add NavigationDestination to the View, but I have not done so for the following reasons:

  • If the method name is not different from "RegisterTypeForNavigation," it will conflict with Prism, and since it cannot be auto-generated from the Prism Template Pack anyway, there is little point.
  • In addition to being pointless, it becomes slightly slower due to the Attribute processing.

Creating a class to represent navigation

This process is necessary to support Deep Linking.

If it is not required, it could be simplified further.

public class Navigation
{
    /// <summary>
    /// Navigation destination
    /// </summary>
    private readonly NavigateDestination _destination;
    /// <summary>
    /// Parameters for navigation
    /// </summary>
    private readonly NavigationParameters _parameters;

    /// <summary>
    /// Initialize instance specifying only the destination
    /// </summary>
    public Navigation(NavigateDestination destination)
        : this(destination, new NavigationParameters())
    {
    }

    /// <summary>
    /// Initialize instance specifying destination and query-string-like parameters
    /// </summary>
    public Navigation(NavigateDestination destination, string query)
        : this(destination, new NavigationParameters(query))
    {
    }

    /// <summary>
    /// Initialize instance specifying destination and navigation parameters
    /// </summary>
    public Navigation(NavigateDestination destination, NavigationParameters parameters)
    {
        _destination = destination;
        _parameters = parameters;
    }

    /// <summary>
    /// Get or set navigation parameters
    /// </summary>
    public object this[string param]
    {
        get { return _parameters[param]; }
        set { _parameters[param] = value; }
    }

    /// <summary>
    /// Convert to a string applicable to Prism navigation specifications
    /// </summary>
    public override string ToString()
    {
        return _destination.ToString() + _parameters.ToString();
    }
}

Preparing extension methods for navigation

I have prepared two types of extension methods for navigation:

  • A method that only specifies NavigateDestination for cases where navigation parameters are not required.
  • A method that takes Navigation as an argument to navigate with parameters.

The former simply delegates to the latter internally.

The implementation itself is in NavigationExtensions.cs.

public static Task NavigateAsync(
    this INavigationService navigationService, 
    params NavigateDestination[] navigatesDestination)
{
    return navigationService.NavigateAsync(
        navigatesDestination.Select(x => new Navigation(x)).ToArray());
}

public static Task NavigateAsync(
    this INavigationService navigationService, 
    params Navigation[] navigations)
{
    var builder = new StringBuilder();
    var delim = string.Empty;
    foreach (var navigation in navigations)
    {
        builder.Append(delim);
        builder.Append(navigation.ToString());
        delim = "/";
    }
    return navigationService.NavigateAsync(builder.ToString());
}

Now, the implementation of the navigation framework side is complete.

Incorporating the navigation process

Registering Views to the DI container

As with regular Prism, perform this in the RegisterTypes method of App.xaml.cs.

protected override void RegisterTypes()
{
    Container.RegisterTypeForNavigation<NavigationPage>(NavigateDestination.NavigationPage);
    Container.RegisterTypeForNavigation<MainPage>(NavigateDestination.MainPage);
    Container.RegisterTypeForNavigation<SecondPage>(NavigateDestination.SecondPage);
}

Implementing navigation to the initial screen

Similarly, implement this in OnInitialized.

At this time, I am setting MainPage within NavigationPage and passing a parameter to MainPage with the key "title".

protected override void OnInitialized()
{
    InitializeComponent();

    NavigationService.NavigateAsync(
        new Navigation(NavigateDestination.NavigationPage), 
        new Navigation(NavigateDestination.MainPage, "title=Hello%20from%20Xamarin.Forms"));
}

Deep Linking is possible as much as you want, but as mentioned above, where to define the parameter keys is left unresolved, resulting in magic strings.

Implementing navigation to SecondPage

I will write out the pattern for navigating without parameters just in case.

public DelegateCommand NavigateToSecondPageCommand => 
    new DelegateCommand(
        () => _navigationService.NavigateAsync(NavigateDestination.SecondPage));

It is written inside the navigation command.

It is very simple because you only specify the Navigation Enum.

Summary

In conclusion, achieving type-safe navigation itself is possible.

However, this approach alone cannot solve the problem of where to define parameter keys between screen transitions.

It is not that there is no way to solve it, but since the entire architecture would become too complex, I personally think the ViewModel First-based method is sufficient.

I would like to write about concrete solutions in an article to coincide with Advent Calendar.

So that is all for today.

See you next time!

Discussion