nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

【Prism for Xamarin.Forms】Type Safe Navigation潔癖症パターン

ちょっと前に、Prism for Xamarin.FormsにおけるViewModel Firstによる画面遷移をご紹介しました。

www.nuits.jp

ただこの例には二つの気になる点(悪い点というわけでもない)がありました。

  1. ViewModelが別のViewModelにクラスの参照のみであるが発生している
  2. DeepLinkは結局文字列依存

という訳で、これらを解決する方法を考えてみました。

考えてみたんですけど、ぶっちゃけやり過ぎだと思います。
という訳でタイトルも「Type Safe Navigation潔癖症パターン」にしてみました。
しかもこの方法、結局 「画面遷移間のパラメーターキーをどこに定義すべきか」 という課題が積み残されていて、これを解決しようとするとさらにやり過ぎレベルがアップします。

私自身がベストと考える方法は別にありますが、まずは、やややり過ぎのアンチパターンとして公開しておこうと思います。

さて、今回のコードについては以下のGithubにTypeSafeNavigationというソリューション名で公開していますので、併せてご覧ください。

github.com

設計方針

  • 画面遷移先をEnumで定義することにより、画面遷移による型安全性を保つ

メリット

  • 画面遷移の型安全性が完全に保てる
    以前解説したViewModel Firstな画面遷移は実際のところ画面遷移にViewModel以外のオブジェクトも渡せ型安全とは言えない
  • DeepLinkにも対応している
    けど、実際はViewModel Firstであっても、同じ方法を利用すればDeepLink可能でもある

デメリット

  • 画面遷移の拡張フレームワークを都度作成する必要があり再利用できない
    (画面遷移にEnumを使うためですが、私のC#力が足りないだけで、方法はあるかも知れません)
  • 画面遷移時のパラメーターキーの定義箇所が規定されておらず、パラメーターの授受に課題が残っている

画面遷移フレームワークの実装

画面遷移を表すEnumを定義する

今回はNavigationPageを利用し、MainPageとSecondPageの2種類の画面が存在するケースで説明します。
以下のように遷移先を表すEnumを定義します。

    public enum NavigateDestination
    {
        NavigationPage,
        MainPage,
        SecondPage
    }

DIコンテナへViewを登録する拡張メソッドを作成する

EnumをキーにViewをコンテナへ登録する拡張メソッドを以下のように作成します。
今回は、NavigationExtensions.csというクラスに定義しています。

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

Enumを文字列化して、既存のIUnityContainerのRegisterTypeForNavigationメソッドを呼び出しているだけです。
ViewにNavigationDestinationを付与するAttributeを作っても良いのですが次の理由からやっていません。

  • メソッド名をRegisterTypeForNavigationとは別の名称にしないとPrismと競合し、どうせPrism Template Packから自動生成できないのであまり意味がない
  • 意味がない上に、Attribute処理が入るためやや遅くなる

画面遷移を表すクラスを作成する

これはDeepLinkに対応するために必要な処理です。
不要であればもう少し簡略化することができるでしょう。

public class Navigation
{
    /// <summary>
    /// 画面遷移先
    /// </summary>
    private readonly NavigateDestination _destination;
    /// <summary>
    /// 画面遷移時のパラメーター
    /// </summary>
    private readonly NavigationParameters _parameters;

    /// <summary>
    /// 画面遷移先のみを指定してインスタンスを初期化する
    /// </summary>
    /// <param name="destination"></param>
    public Navigation(NavigateDestination destination)
        : this(destination, new NavigationParameters())
    {
    }

    /// <summary>
    /// 画面遷移先とクエリー文字列状のパラメーターを指定しインスタンスを初期化する
    /// </summary>
    /// <param name="destination"></param>
    /// <param name="query"></param>
    public Navigation(NavigateDestination destination, string query)
        : this(destination, new NavigationParameters(query))
    {
    }

    /// <summary>
    /// 画面遷移先と遷移パラメーターを指定してインスタンスを初期化する
    /// </summary>
    /// <param name="destination"></param>
    /// <param name="parameters"></param>
    public Navigation(NavigateDestination destination, NavigationParameters parameters)
    {
        _destination = destination;
        _parameters = parameters;
    }

    /// <summary>
    /// 遷移パラメーターを取得・設定する
    /// </summary>
    /// <param name="param"></param>
    /// <returns></returns>
    public object this[string param]
    {
        get { return _parameters[param]; }
        set { _parameters[param] = value; }
    }

    /// <summary>
    /// Prismの画面遷移指定に適用可能な文字列へ変換する
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return _destination.ToString() + _parameters.ToString();
    }
}

画面遷移用の拡張メソッドを用意する

画面遷移用の拡張メソッドを2種類用意しています。

  • 画面遷移にパラメーターが不要なケースでNavigateDestinationのみを指定するメソッド
  • 画面遷移をパラメーター付きで行うため、引数にNavigationを指定するメソッド

前者は内部的に後者に移譲しているだけです。
実装自体は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());
}

さて、これで画面遷移フレームワーク側の実装は完了です。

画面遷移処理を組み込む

DIコンテナへViewを登録する

通常のPrismと同じように、App.xaml.csのRegisterTypesにて行います。

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

先頭画面への画面遷移を実装する

同様にOnInitializedに実装します。
この時、NavigationPage内にMainPageを設定し、MainPageに「title」をいうキーでパラメーターを渡しています。

protected override void OnInitialized()
{
    InitializeComponent();

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

いくらでもDeepLink可能ですが、上述のとおりパラメーターキーをどこに定義すべきか未解決でマジックストリングになってしまっています。

SecondPageへの画面遷移を実装する

パラメーターなしで遷移するパターンを一応書いておきます。

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

画面遷移コマンド内に書いてしまっています。
画面遷移Enumを指定するだけなので非常に簡単です。

総括

という訳で、TypeSafeに画面遷移すること自体は実現できています。
しかし、画面遷移間のパラメーターキーをどこに定義すべきか、結局このアプローチだけでは解決できません。
解決する方法が無いわけではないのですが、アーキテクチャ全体が複雑化しすぎるため、個人的にはViewModel Firstベースの方法で良いと思っています。
具体的な解放については、Advent Calenderに合わせて記事化したいと思います。

ということで、今日はここまで。
それではまた!