エンタープライズ アプリ ナビゲーション
Note
この電子ブックは 2017 年春に発行されたもので、その後は改訂されていません。 このブックには今なお価値のある内容が多く含まれていますが、一部の記載内容は古くなっています。
Xamarin.Forms では、ページ ナビゲーションがサポートされています。これは通常、ユーザーの UI 操作によって、または内部ロジックによって状態が変わる結果としてアプリ自体から生じます。 ところが、Model-View-ViewModel (MVVM) パターンを使用するアプリで実装するナビゲーションは、次の課題を満たす必要があるため、複雑になる場合があります。
- ナビゲート先のビューを、ビュー間の緊密な結合と依存関係を導入しないアプローチを使って特定する方法。
- ナビゲート先のビューがインスタンス化および初期化されるプロセスを調整する方法。 MVVM を使う場合、ビューとビュー モデルをインスタンス化し、ビューのバインド コンテキストを介して相互に関連付ける必要があります。 アプリで依存関係挿入コンテナーを使っている場合、ビューとビュー モデルのインスタンス化で特定の構築メカニズムが必要になる可能性があります。
- ビュー優先ナビゲーションを実行するのか、ビュー モデル優先ナビゲーションを実行するのか。 ビュー優先ナビゲーションでは、ナビゲート先のページから、ビューの種類の名前を参照します。 ナビゲーション中に、指定したビューは、対応するビュー モデルやその他の依存サービスと共にインスタンス化されます。 別の方法として、ビュー モデル優先ナビゲーションを使用します。このとき、ナビゲート先のページから、ビュー モデルの種類の名前を参照します。
- ビューとビュー モデル全体でアプリのナビゲーション動作を明確に分離する方法。 MVVM パターンにより、アプリの UI とそのプレゼンテーションおよびビジネス ロジックが分離されます。 ところが、アプリのナビゲーション動作は、多くの場合、アプリの UI 部分とプレゼンテーション部分に及びます。 ユーザーがビューからナビゲーションを開始することが多く、ナビゲーションの結果としてビューが置き換えられます。 一方、ビュー モデル内からナビゲーションを開始または調整する必要があることも多くあります。
- 初期化のためにナビゲーション中にパラメーターを渡す方法。 たとえば、ユーザーが注文の詳細を更新するためにあるビューに移動する場合、正しいデータを表示できるように、そのビューに注文データを渡す必要があります。
- 特定のビジネス ルールに従うようにナビゲーションを調整する方法。 たとえば、ユーザーがビューから移動する前にメッセージを表示して、無効なデータを修正する、または、そのビュー内で行われたデータの変更を送信または破棄するように求めることができるようにする場合があります。
この章では、このような課題に対処するために、ビュー モデル優先のページ ナビゲーションを実行するために使われる NavigationService
というナビゲーション サービス クラスについて説明します。
Note
アプリで使われる NavigationService
は、ContentPage インスタンス間の階層ナビゲーションを実行するためだけに設計されています。 このサービスを使って他のページの型間を移動すると、予期しない動作が発生する可能性があります。
ページ間のナビゲーション
ナビゲーション ロジックは、ビューのコードビハインドまたはデータ バインドされたビュー モデル内に存在する場合があります。 ビューにナビゲーション ロジックを配置するのが最も単純な方法ですが、単体テストでテストするのは簡単ではありません。 ビュー モデル クラスにナビゲーション ロジックを配置すると、単体テストを通じてロジックを実行できます。 さらに、ビュー モデルでは、特定のビジネス ルールが確実に適用されるようにナビゲーションを制御するロジックを実装できます。 たとえば、アプリによっては、入力したデータが有効であることが保証されない限り、ユーザーはページから移動できません。
通常、NavigationService
クラスは、テスト容易性を高めるためにビュー モデルから呼び出されます。 ただし、ビュー モデルからビューに移動するには、ビュー モデルがビュー、特にアクティブなビュー モデルが関連付けられていないビューを参照する必要があるため、これはお勧めできません。 そのため、ここで示されている NavigationService
は、移動先のターゲットとしてビュー モデルの型を指定します。
eShopOnContainers モバイル アプリは、NavigationService
クラスを使ってビュー モデル優先のナビゲーションを提供します。 このクラスを使用して、次のコード例に示される INavigationService
インターフェイスを実装します。
public interface INavigationService
{
ViewModelBase PreviousPageViewModel { get; }
Task InitializeAsync();
Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase;
Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;
Task RemoveLastFromBackStackAsync();
Task RemoveBackStackAsync();
}
このインターフェイスで、実装クラスで次のメソッドを提供する必要があることを指定します。
メソッド | 目的 |
---|---|
InitializeAsync |
アプリの起動時に、2 つのページのいずれかへのナビゲーションを実行します。 |
NavigateToAsync |
指定したページへの階層ナビゲーションを実行します。 |
NavigateToAsync(parameter) |
パラメーターを渡して、指定したページへの階層ナビゲーションを実行します。 |
RemoveLastFromBackStackAsync |
ナビゲーション スタックから前のページを削除します。 |
RemoveBackStackAsync |
ナビゲーション スタックから前のページをすべて削除します。 |
さらに、INavigationService
インターフェイスでは、実装クラスが PreviousPageViewModel
プロパティを提供する必要があることを指定します。 このプロパティは、ナビゲーション スタックの前のページに関連付けられたビュー モデルの型を返します。
Note
INavigationService
インターフェイスでは、通常、GoBackAsync
メソッドも指定します。これは、ナビゲーション スタック内の前のページに戻るためにプログラムで使用します。 ただし、このメソッドは必須ではないため、eShopOnContainers モバイル アプリにはありません。
NavigationService インスタンスの作成
次のコード例に示すように、NavigationService
クラスは、INavigationService
インターフェイスを実装し、Autofac 依存関係挿入コンテナーを持つシングルトンとして登録されます。
builder.RegisterType<NavigationService>().As<INavigationService>().SingleInstance();
次のコード例に示すように、INavigationService
インターフェイスは ViewModelBase
クラス コンストラクターで解決されます。
NavigationService = ViewModelLocator.Resolve<INavigationService>();
これにより、Autofac 依存関係挿入コンテナーに保存されている NavigationService
オブジェクトへの参照が返されます。これを作成するには、App
クラスの InitNavigation
メソッドを使います。 詳細については、「アプリの起動時のナビゲーション」を参照してください。
ViewModelBase
クラスによって、種類が INavigationService
の NavigationService
プロパティに NavigationService
インスタンスが格納されます。 そのため、すべてのビュー モデル クラスが、ViewModelBase
クラスから派生し、NavigationService
プロパティを使って、INavigationService
インターフェイスで指定されたメソッドにアクセスできます。 これにより、Autofac 依存関係挿入コンテナーから各ビュー モデル クラスに NavigationService
オブジェクトを挿入するオーバーヘッドを回避できます。
ナビゲーション要求の処理
Xamarin.Forms には、ユーザーが必要に応じてページ間を前後に移動できる階層ナビゲーション エクスペリエンスを実装する NavigationPage
クラスが用意されています。 階層ナビゲーションの詳細については、 の階層ナビゲーションに関するページを参照してください。
次のコード例に示すように、eShopOnContainers アプリで NavigationPage
クラスを直接使うのではなく、NavigationPage
クラスを CustomNavigationView
クラスでラップします。
public partial class CustomNavigationView : NavigationPage
{
public CustomNavigationView() : base()
{
InitializeComponent();
}
public CustomNavigationView(Page root) : base(root)
{
InitializeComponent();
}
}
このラップの目的は、クラスの XAML ファイル内の NavigationPage
インスタンスのスタイル設定を簡単にすることです。
次のコード例に示すように、いずれかの NavigateToAsync
メソッドを呼び出して、移動先のページのビュー モデルの型を指定することにより、ビュー モデル クラス内でナビゲーションが実行されます。
await NavigationService.NavigateToAsync<MainViewModel>();
次のコード例は、NavigationService
クラスが提供する NavigateToAsync
メソッドを示しています。
public Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase
{
return InternalNavigateToAsync(typeof(TViewModel), null);
}
public Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase
{
return InternalNavigateToAsync(typeof(TViewModel), parameter);
}
各メソッドでは、ViewModelBase
クラスから派生したビュー モデル クラスで、InternalNavigateToAsync
メソッドを呼び出して階層ナビゲーションを実行できます。 さらに 2 つ目の NavigateToAsync
メソッドで、ナビゲート先のビュー モデルに渡す引数としてナビゲーション データを指定できます。通常、これは初期化の実行に使われます。 詳細については、「ナビゲーション中にパラメーターを渡す」を参照してください。
InternalNavigateToAsync
メソッドはナビゲーション要求を実行します。次にコード例を示します。
private async Task InternalNavigateToAsync(Type viewModelType, object parameter)
{
Page page = CreatePage(viewModelType, parameter);
if (page is LoginView)
{
Application.Current.MainPage = new CustomNavigationView(page);
}
else
{
var navigationPage = Application.Current.MainPage as CustomNavigationView;
if (navigationPage != null)
{
await navigationPage.PushAsync(page);
}
else
{
Application.Current.MainPage = new CustomNavigationView(page);
}
}
await (page.BindingContext as ViewModelBase).InitializeAsync(parameter);
}
private Type GetPageTypeForViewModel(Type viewModelType)
{
var viewName = viewModelType.FullName.Replace("Model", string.Empty);
var viewModelAssemblyName = viewModelType.GetTypeInfo().Assembly.FullName;
var viewAssemblyName = string.Format(
CultureInfo.InvariantCulture, "{0}, {1}", viewName, viewModelAssemblyName);
var viewType = Type.GetType(viewAssemblyName);
return viewType;
}
private Page CreatePage(Type viewModelType, object parameter)
{
Type pageType = GetPageTypeForViewModel(viewModelType);
if (pageType == null)
{
throw new Exception($"Cannot locate page type for {viewModelType}");
}
Page page = Activator.CreateInstance(pageType) as Page;
return page;
}
InternalNavigateToAsync
メソッドは、まず CreatePage
メソッドを呼び出すことで、ビュー モデルへのナビゲーションを実行します。 このメソッドは、指定されたビュー モデルの型に対応するビューを検索し、このビューの型のインスタンスを作成して返します。 ビュー モデルの型に対応するビューを見つけるには、次のことを前提とした規約ベースのアプローチを使います。
- ビューは、ビュー モデルの型と同じアセンブリ内にある。
- ビューは、子の名前空間 .Views 内にある。
- ビュー モデルは、子の名前空間 .ViewModels 内にある。
- ビュー名は、ビュー モデル名から "Model" を除いたものに対応します。
ビューのインスタンスが作成されると、対応するビュー モデルに関連付けられます。 このようになるしくみの詳細については、「ビュー モデル ロケーターを使用してビュー モデルを自動的に作成する」を参照してください。
作成しているビューが LoginView
の場合は CustomNavigationView
クラスの新しいインスタンス内にラップされ、Application.Current.MainPage
プロパティに割り当てられます。 それ以外の場合は、CustomNavigationView
インスタンスが取得され、それが null でない場合は、PushAsync
メソッドが呼び出され、作成中のビューがナビゲーション スタックにプッシュされます。 ただし、取得された CustomNavigationView
インスタンスが null
の場合、作成中のビューは CustomNavigationView
クラスの新しいインスタンス内にラップされ、Application.Current.MainPage
プロパティに割り当てられます。 このメカニズムにより、ナビゲーション中に、空である場合とデータが含まれている場合の両方で、ナビゲーション スタックに確実にページが正しく追加されます。
ヒント
ページをキャッシュすることを検討してください。 ページをキャッシュすると、現在表示されていないビューのメモリが使われます。 ただし、ページ キャッシュがないと、新しいページに移動するたびに XAML 解析とページとそのビュー モデルの構築が実行されることになり、複雑なページの場合はパフォーマンスに影響する可能性があります。 過剰な数のコントロールを使わない、適切に設計されたページの場合、パフォーマンスは十分なはずです。 ただし、ページの読み込み時間が遅い場合は、ページ キャッシュが役立つことがあります。
ビューが作成されてナビゲートされると、ビューの関連ビュー モデルの InitializeAsync
メソッドが実行されます。 詳細については、「ナビゲーション中にパラメーターを渡す」を参照してください。
アプリの起動時のナビゲーション
アプリを起動すると、App
クラスの InitNavigation
メソッドが呼び出されます。 以下のコード例はこのメソッドを示しています。
private Task InitNavigation()
{
var navigationService = ViewModelLocator.Resolve<INavigationService>();
return navigationService.InitializeAsync();
}
このメソッドにより、Autofac 依存関係挿入コンテナーに新しい NavigationService
オブジェクトが作成され、その InitializeAsync
メソッドを呼び出す前に、そのオブジェクトへの参照を返します。
Note
INavigationService
インターフェイスが ViewModelBase
クラスによって解決されると、コンテナーは、InitNavigation メソッドの呼び出し時に作成された NavigationService
オブジェクトへの参照を返します。
次のコード例は、 NavigationService
InitializeAsync
メソッドを示しています。
public Task InitializeAsync()
{
if (string.IsNullOrEmpty(Settings.AuthAccessToken))
return NavigateToAsync<LoginViewModel>();
else
return NavigateToAsync<MainViewModel>();
}
アプリにキャッシュされたアクセス トークンがあり、それが認証に使われている場合は、MainView
に移動します。 それ以外の場合は、LoginView
はナビゲート先です。
Autofac 依存関係挿入コンテナーの詳細については、「依存関係の挿入の概要」を参照してください。
ナビゲーション中にパラメーターを渡す
INavigationService
インターフェイスで指定される NavigateToAsync
メソッドの 1 つを使うと、ナビゲーション データを、ナビゲーション先のビュー モデルに渡される引数として指定できます。通常、これは初期化の実行に使われます。
たとえば、ProfileViewModel
クラスには、ユーザーが ProfileView
ページで注文を選択したときに実行される OrderDetailCommand
が含まれています。 そして、次のコード例に示すように、OrderDetailAsync
メソッドが実行されます。
private async Task OrderDetailAsync(Order order)
{
await NavigationService.NavigateToAsync<OrderDetailViewModel>(order);
}
このメソッドは、OrderDetailViewModel
へのナビゲーションを呼び出し、ユーザーが ProfileView
ページで選んだ順序を表す Order
インスタンスを渡します。 NavigationService
クラスによって OrderDetailView
が作成されると、OrderDetailViewModel
クラスのインスタンスが作成され、ビューの BindingContext
に割り当てられます。 OrderDetailView
に移動した後、InternalNavigateToAsync
メソッドを使い、ビューの関連ビュー モデルの InitializeAsync
メソッドを実行します。
InitializeAsync
メソッドは、オーバーライド可能なメソッドとして ViewModelBase
クラスに定義されています。 このメソッドで、ナビゲーション操作中にビュー モデルに渡されるデータを表す object
引数を指定します。 そのため、ナビゲーション操作からデータを受信するビュー モデル クラスは、必要な初期化を実行するための InitializeAsync
メソッドの独自の実装を提供しています。 次のコード例は、OrderDetailViewModel
クラスの InitializeAsync
メソッドを示します。
public override async Task InitializeAsync(object navigationData)
{
if (navigationData is Order)
{
...
Order = await _ordersService.GetOrderAsync(
Convert.ToInt32(order.OrderNumber), authToken);
...
}
}
このメソッドは、ナビゲーション操作中にビュー モデルに渡された Order
インスタンスを取得し、それを使って OrderService
インスタンスから完全な注文の詳細を取得します。
動作を使ったナビゲーションの呼び出し
ナビゲーションは通常、ユーザー操作によってビューからトリガーされます。 たとえば、LoginView
では、認証成功後にナビゲーションが実行されます。 次のコード例は、動作によってナビゲーションがどのように呼び出されるかを示しています。
<WebView ...>
<WebView.Behaviors>
<behaviors:EventToCommandBehavior
EventName="Navigating"
EventArgsConverter="{StaticResource WebNavigatingEventArgsConverter}"
Command="{Binding NavigateCommand}" />
</WebView.Behaviors>
</WebView>
実行時に、EventToCommandBehavior
によって、WebView
とのやりとりとの応答が行われます。 WebView
で、Web ページにナビゲートすると、Navigating
イベントが発生し、LoginViewModel
の NavigateCommand
が実行されます。 既定では、このイベントのイベント引数がコマンドに渡されます。 このデータは、ソースとターゲットの間で渡される際に、EventArgsConverter
プロパティで指定されたコンバーターによって変換され、WebNavigatingEventArgs
から Url
が返されます。 そのため、NavigationCommand
が実行されると、Web ページの URL がパラメーターとして登録された Action
に渡されます。
そして、次のコード例に示すように、NavigationCommand
で NavigateAsync
メソッドを実行します。
private async Task NavigateAsync(string url)
{
...
await NavigationService.NavigateToAsync<MainViewModel>();
await NavigationService.RemoveLastFromBackStackAsync();
...
}
このメソッドは、MainViewModel
へのナビゲーションを呼び出し、ナビゲーションに続いて、ナビゲーション スタックから LoginView
ページを削除します。
ナビゲーションの確認または取り消し
場合によって、アプリで、ナビゲーション操作中にユーザーと対話し、ユーザーがナビゲーションを確認または取り消すことができるようにする必要があります。 これが必要になるのは、たとえば、ユーザーがデータ入力ページを完了する前に移動しようとしたときです。 この場合、ユーザーが、そのページから移動する、またはナビゲーション操作が発生する前に取り消すことができるようにアプリから通知する必要があります。 これは、ビュー モデル クラスで、通知からの応答を使ってナビゲーションを呼び出すかどうかを制御することで実現できます。
まとめ
Xamarin.Forms では、ページ ナビゲーションがサポートされています。これは通常、ユーザーの UI 操作によって、または内部ロジックによって状態が変わる結果としてアプリ自体から生じます。 ところが、MVVM パターンを使用するアプリで実装するナビゲーションは複雑になる場合があります。
この章では、ビューモデルからビューモデル優先ナビゲーションを実行するために使用される NavigationService
クラスについて説明しました。 ビュー モデル クラスにナビゲーション ロジックを配置すると、自動テストを通じてロジックを実行できます。 さらに、ビュー モデルでは、特定のビジネス ルールが確実に適用されるようにナビゲーションを制御するロジックを実装できます。