Prism.Formsを使ってViewModelから画面遷移を行う場合、デフォルトでは以下の様にページの名前を指定する必要があります。
navigationService.NavigateAsync("MainPage");
ViewModelはViewについて知らないという、MVVMの原則からすると、これはちょっと嫌な感じです。ViewModelからは、ViewModelを指定して遷移したいものです。(これを、ViewModel Firstな画面遷移と言います)
なので、ViewModel Firstを実現するためのライブラリPrism.NavigationExを作成して、NuGetで公開しました。現行版である、Prism.Forms 7.0.0.396に対応しています。また、プレリリース版として、7.1.0.279-pre対応版も用意しました。
NuGet
https://www.nuget.org/packages/Prism.NavigationEx/
GitHub
https://github.com/f-miyu/Prism.NavigationEx
Prism.NavigationExの機能
Prism.NavigationExは、ViewModelを指定して画面遷移できるのはもちろん、他にも以下の機能を備えています。
- タイプセーフなパラメータ渡し
- async/awaitを用いた遷移先からの戻り値の受け取り
- 遷移処理ごとにその遷移が可能かどうかの判定処理を設定できる機能
- DeepLinkやTabbedPageのサポート
- Viewの一括登録
使い方
ViewModelのクラスは、NavigationViewModel
を継承する必要があります。NavigateAsync
の型パラメータに遷移するViewModelを指定します。
public class MainPageViewModel : NavigationViewModel
{
public DelegateCommand GoToNextCommand { get; }
public MainPageViewModel(INavigationService navigationService) : base(navigationService)
{
GoToNextCommand = new DelegateCommand(() => NavigateAsync<NextPageViewModel>());
}
}
public class NextPageViewModel : NavigationViewModel
{
public NextPageViewModel(INavigationService navigationService) : base(navigationService)
{
}
}
パラメータを受け取りたい場合は、NavigationViewModel
にパラメータの型を指定して、抽象メソッドのPrepare
を実装します。Prepare
メソッドでは、引数として遷移元から渡されたパラメータが渡されるので、それを使って初期化処理を行います。
NavigateAsync
は、遷移するViewModelと、パラメータの型を指定して、引数にパラメータを渡します。遷移先のNavigationViewModel
のパラメータの型とNavigateAsync
で指定したパラメータの型が一致していないとコンパイルエラーになります。
public class MainPageViewModel : NavigationViewModel
{
public DelegateCommand GoToNextCommand { get; }
public MainPageViewModel(INavigationService navigationService) : base(navigationService)
{
GoToNextCommand = new DelegateCommand(() => NavigateAsync<NextPageViewModel, int>(100));
}
}
public class NextPageViewModel : NavigationViewModel<int>
{
private int _parameter;
public NextPageViewModel(INavigationService navigationService) : base(navigationService)
{
}
public override void Prepare(int parameter)
{
_parameter = parameter;
}
}
遷移先から戻り値を受け取る場合は、NavigationViewModelResult
を継承します。NavigateAsync
は、遷移するViewModelと、戻り値の型を指定して、awaitで戻るのを待つことが出来ます。
戻り値は、INavigationResult
型で、Success
プロパティがtrueなら、Data
プロパティにGoBackAsync
の引数で与えた戻り値が入っています。ナビゲーションバーの戻るボタンが押されて戻って来た場合などは、Success
プロパティは、falseになります。
これも、遷移先のNavigationViewModelResult
の戻り値の型とNavigateAsync
で指定した戻り値の型が一致していないとコンパイルエラーになります。
public class MainPageViewModel : NavigationViewModel
{
public DelegateCommand GoToNextCommand { get; }
public MainPageViewModel(INavigationService navigationService) : base(navigationService)
{
GoToNextCommand = new DelegateCommand(async () =>
{
var result = await NavigateAsync<NextPageViewModel, string>();
if (result.Success)
{
var data = result.Data;
}
});
}
}
public class NextPageViewModel : NavigationViewModelResult<string>
{
public DelegateCommand GoBackCommand { get; }
public NextPageViewModel(INavigationService navigationService) : base(navigationService)
{
GoBackCommand = new DelegateCommand(() => GoBackAsync("result"));
}
}
パラメータも戻り値も必要な場合は、NavigationViewModel
に両方の型を指定します。NavigateAsync
も両方の型を指定することになります。
public class MainPageViewModel : NavigationViewModel
{
public DelegateCommand GoToNextCommand { get; }
public MainPageViewModel(INavigationService navigationService) : base(navigationService)
{
GoToNextCommand = new DelegateCommand(async () =>
{
var result = await NavigateAsync<NextPageViewModel, int, string>(100);
if (result.Success)
{
var data = result.Data;
}
});
}
}
public class NextPageViewModel : NavigationViewModel<int, string>
{
private int _parameter;
public DelegateCommand GoBackCommand { get; }
public NextPageViewModel(INavigationService navigationService) : base(navigationService)
{
GoBackCommand = new DelegateCommand(() => GoBackAsync("result"));
}
public override void Prepare(int parameter)
{
_parameter = parameter;
}
}
NavigateAsyncの引数
NavigateAsyncは、useModalNavigation
、animated
に加えて、以下の引数を取ることが出来ます。
wrapInNavigationPage
trueなら、NavigationPage
の入れ子になります。使用されるNavigationPage
は、後述するNavigationNameProvider
でカスタマイズすることが出来ます。
NavigateAsync<NextPageViewModel>(wrapInNavigationPage: true);
noHistory
trueなら、ナビゲーションスタックがクリアされます。(/で始まる絶対パスでの指定になります)
NavigateAsync<NextPageViewModel>(noHistory: true);
canNavigate
遷移できるかどうかを判定するTask<bool>
を返すデリゲートを渡すことが出来ます。trueを返せば遷移して、falseを返せば、遷移がキャンセルされます。
awaitで戻り値を待っている時に、falseを返した場合は、そのTaskがキャンセルされ、INavigationResult
のSuccess
プロパティはfalseで返ってきます。
NavigateAsync<NextPageViewModel>(canNavigate: () => pageDialogService.DisplayAlertAsync("title", "message", "OK", "Cancel");
replaced
trueなら、現在のページが指定したViewModelのページに置きかわります。
NavigateAsync<NextPageViewModel>(replaced: true);
GoBackAsync、GoBackToRootAsyncの引数
canNavigate
GoBackAsync
、GoBackToRootAsync
もNavigateAsync
と同じようにcanNavigate
を指定できます。
GoBackAsync(canNavigate: () => pageDialogService.DisplayAlertAsync("title", "message", "OK", "Cancel");
Deep Link
Deep Linkもサポートしています。
以下のように、NavigationFactory
のCreate
メソッドで最初の遷移先のViewModelを指定して、Add
メソッドで、後続の遷移のViewModelを指定します。
var navigation = NavigationFactory.Create<MainPageViewModel>()
.Add<NextPageViewModel>();
.Add<NextNextPageViewModel>();
NavigateAsync(navigation);
パラメータを与える場合は、パラメータの型も指定します。
var navigation = NavigationFactory.Create<MainPageViewModel>()
.Add<NextPageViewModel, int>(100);
.Add<NextNextPageViewModel, int>(200);
NavigateAsync(navigation);
遷移の途中のViewModelが戻り値を受け取る必要がある場合は、以下のようにデリゲートを渡すことができます。遷移先から戻ってきた時にこのデリゲートが呼び出されます。
デリゲートを指定する場合は、戻り値の型も指定します。後続でAdd
するViewModelの戻り値の型は、ここで指定した戻り値の型と一致している必要があります。異なると、コンパイルエラーになります。
デリゲートの引数であるviewModel
は、INavigationViewModel
なので、初期化処理を行うならキャストが必要になります。これは、TabbedPage
では、表示されているタブのViewModelが渡されるので、複数の型に対応するためにINavigationViewModel
を渡すようにしています。
var navigation = NavigationFactory.Create<MainPageViewModel, string>((viewModel, result) =>
{
if (result.Success && viewModel is MainPageViewModel mainPageViewModel)
{
var data = result.Data;
}
})
.Add<NextPageViewModel, int, string>(100, (viewModel, result) =>
{
if (result.Success && viewModel is NextPageViewModel nextPageViewModel)
{
var data = result.Data;
}
})
.Add<NextNextPageViewModel, int>(200);
NavigateAsync(navigation);
canNavigate
を指定することもできます。ただし、遷移前に呼び出されるのは、最後の遷移で指定したものだけです。途中のものは、遷移後に呼び出されます。
var navigation = NavigationFactory.Create<MainPageViewModel>()
.Add<NextPageViewModel>();
.Add<NextNextPageViewModel>(() => pageDialogService.DisplayAlertAsync("title", "message", "OK", "Cancel"));
NavigateAsync(navigation);
TabbedPage
TabbedPageのタブも以下のようにViewModelやパラメータの型を指定することができます。
Tab
クラスのコンストラクタの引数であるwrapInNavigationPage
をtrueにしたら、そのタブは、NavigationPage
の入れ子になります。
var navigation = NavigationFactory.Create<MyTabbedPageViewModel>(null, new Tab<FirstTabPageViewModel, string>("text", true), new Tab<SecondTabPageViewModel>());
NavigateAsync(navigation);
NavigationNameProvider
デフォルトでは、Viewのクラス名は、ViewModelのクラス名から後ろの「ViewModel」を取ったものになります。このルールは独自のものに変更することが可能です。例えば、Viewのクラス名を〇〇Viewという名前にしたい場合は、以下のように設定します。
NavigationNameProvider.SetDefaultViewModelTypeToNavigationNameResolver(viewModelType =>
{
var suffix = "Model";
var name = viewModelType.Name;
if (name.EndsWith(suffix))
{
name = name.Substring(0, name.Length - suffix.Length);
}
return name;
});
また、wrapInNavigationPage
をtrueにした時に使用されるNavigationPage
を独自のものにすることもできます。
NavigationNameProvider.DefaultNavigationPageName = nameof(MyNavigationPage);
Viewの一括登録
以下のようにして、プロジェクトで定義してあるXamarin.Forms.Page
クラスを継承している全てのクラス(抽象クラスは除く)とNavigationPage
の一括登録を行うことができます。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation(this);
}
最後に
Prism.Formsはいいフレームワークなのですが、画面遷移のやり方に関してはやや不満があり、軽く拡張して使っていたのを、今回きちんとライブラリとしてまとめてみました。
割といい感じにできたと思うので、もしよろしければ、使ってみてください。