エンタープライズ アプリ のナビゲーション
Note
この電子ブックは 2017 年の春に発行され、それ以来更新されていません。 貴重なままの本に多くがありますが、資料の一部は時代遅れです。
Xamarin.Forms には、ページ ナビゲーションのサポートが含まれています。これは通常、ユーザーが UI とやり取りしたか、内部ロジックドリブン状態の変更の結果としてアプリ自体から行われます。 ところが、Model-View-ViewModel (MVVM) パターンを使用するアプリで実装するナビゲーションは、次の課題を満たす必要があるため、複雑になる場合があります。
- ビュー間の緊密な結合と依存関係を導入しないアプローチを使用して、移動するビューを識別する方法。
- 移動先のビューがインスタンス化および初期化されるプロセスを調整する方法。 MVVM を使用する場合、ビューとビュー モデルをインスタンス化し、ビューのバインド コンテキストを介して相互に関連付ける必要があります。 アプリで依存関係挿入コンテナーを使用している場合、ビューとビュー モデルのインスタンス化には、特定の構築メカニズムが必要になる場合があります。
- ビュー優先ナビゲーションを実行するか、モデル優先ナビゲーションを表示するかを指定します。 ビュー優先ナビゲーションでは、ナビゲート先のページから、ビューの種類の名前を参照します。 ナビゲーション中に、指定されたビューが、対応するビュー モデルおよび他の依存サービスと共にインスタンス化されます。 もう 1 つの方法は、ビュー モデルの最初のナビゲーションを使用することです。ここで、移動先のページはビュー モデルの種類の名前を参照します。
- ビューとビュー モデル全体でアプリのナビゲーション動作をクリーンに分離する方法。 MVVM パターンは、アプリの UI とそのプレゼンテーションとビジネス ロジックの間の分離を提供します。 ただし、アプリのナビゲーション動作は、多くの場合、アプリの UI 部分とプレゼンテーション部分にまたがります。 ユーザーがビューからナビゲーションを開始することが多く、ナビゲーションの結果としてビューが置き換えられます。 ただし、多くの場合、ビュー モデル内からナビゲーションを開始または調整する必要があります。
- 初期化のためにナビゲーション中にパラメーターを渡す方法。 たとえば、ユーザーが注文の詳細を更新するためにあるビューに移動する場合、正しいデータを表示できるように、そのビューに注文データを渡す必要があります。
- 特定のビジネス ルールに従うようにナビゲーションを調整する方法。 たとえば、ユーザーがビューから移動する前にメッセージを表示して、無効なデータを修正する、または、そのビュー内で行われたデータの変更を送信または破棄するように求めることができるようにする場合があります。
この章では、ビュー モデルの最初のページ ナビゲーションを NavigationService
実行するために使用されるクラスを提示することで、これらの課題に対処します。
注意
アプリで使用される は 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
する必要があることを指定します。 このプロパティは、ナビゲーション スタック内の前のページに関連付けられているビュー モデルの種類を返します。
注意
INavigationService
インターフェイスでは、通常、GoBackAsync
メソッドも指定します。これは、ナビゲーション スタック内の前のページに戻るためにプログラムで使用します。 ただし、このメソッドは必要ないため、eShopOnContainers モバイル アプリにはありません。
NavigationService インスタンスの作成
インターフェイスをINavigationService
実装する クラスはNavigationService
、次のコード例に示すように、Autofac 依存関係挿入コンテナーにシングルトンとして登録されます。
builder.RegisterType<NavigationService>().As<INavigationService>().SingleInstance();
インターフェイスは INavigationService
、次の ViewModelBase
コード例に示すように、クラス コンストラクターで解決されます。
NavigationService = ViewModelLocator.Resolve<INavigationService>();
これにより、 クラスの NavigationService
メソッドによって InitNavigation
作成される Autofac 依存関係挿入コンテナーに格納されている オブジェクトへの参照が App
返されます。 詳細については、「 アプリの起動時のナビゲーション」を参照してください。
ViewModelBase
クラスによって、種類が INavigationService
の NavigationService
プロパティに NavigationService
インスタンスが格納されます。 したがって、 クラスから ViewModelBase
派生したすべてのビュー モデル クラスは、 プロパティを NavigationService
使用して、 インターフェイスで指定されたメソッドに INavigationService
アクセスできます。 これにより、Autofac 依存関係挿入 NavigationService
コンテナーから各ビュー モデル クラスにオブジェクトを挿入するオーバーヘッドが回避されます。
ナビゲーション要求の処理
Xamarin.Forms は、 クラスを NavigationPage
提供します。これは、ユーザーが必要に応じてページ間を移動したり、前後に移動したりできる階層ナビゲーション エクスペリエンスを実装します。 階層ナビゲーションの詳細については、「階層ナビゲーション」を参照してください。
次のコード例に示すように、クラスをNavigationPage
直接使用するのではなく、eShopOnContainers アプリによって クラス内CustomNavigationView
の クラスがラップNavigationPage
されます。
public partial class CustomNavigationView : NavigationPage
{
public CustomNavigationView() : base()
{
InitializeComponent();
}
public CustomNavigationView(Page root) : base(root)
{
InitializeComponent();
}
}
このラップの目的は、 クラスの XAML ファイル内のインスタンスの NavigationPage
スタイルを簡単に設定することです。
ナビゲーションは、次のコード例に示すように、移動するページの NavigateToAsync
ビュー モデルの種類を指定して、いずれかのメソッドを呼び出すことによって、ビュー モデル クラス内で実行されます。
await NavigationService.NavigateToAsync<MainViewModel>();
次のコード例は、 NavigateToAsync
クラスによって提供されるメソッドを NavigationService
示しています。
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
実行します。 このメソッドは、指定したビュー モデルの種類に対応するビューを検索し、このビューの種類のインスタンスを作成して返します。 ビュー モデルの種類に対応するビューを検索するには、規則ベースのアプローチを使用します。これは、次のことを前提としています。
- ビューは、ビュー モデルの種類と同じアセンブリ内にあります。
- ビューは にあります。子名前空間を表示します。
- ビュー モデルは にあります。ViewModels 子名前空間。
- ビュー名はビュー モデル名に対応し、"Model" は削除されます。
ビューがインスタンス化されると、対応するビュー モデルに関連付けられます。 この方法の詳細については、「 ビュー モデル ロケーターを使用してビュー モデルを自動的に作成する」を参照してください。
作成されるビューが の LoginView
場合は、 クラスの新しいインスタンス CustomNavigationView
内にラップされ、 プロパティに Application.Current.MainPage
割り当てられます。 それ以外の CustomNavigationView
場合、インスタンスが取得され、null でない場合は、 メソッドが呼び出され、 PushAsync
作成されているビューがナビゲーション スタックにプッシュされます。 ただし、取得した CustomNavigationView
インスタンスが の場合、 null
作成されるビューは クラスの CustomNavigationView
新しいインスタンス内にラップされ、 Application.Current.MainPage
プロパティに割り当てられます。 このメカニズムにより、ナビゲーション中に、ページが空の場合とデータが含まれている場合の両方で、ナビゲーション スタックにページが正しく追加されます。
ヒント
ページのキャッシュを検討してください。 ページ キャッシュを使用すると、現在表示されていないビューのメモリ消費量が生じます。 ただし、ページ キャッシュを使用しない場合、新しいページに移動するたびに XAML によるページとそのビュー モデルの解析と構築が行われ、複雑なページのパフォーマンスに影響する可能性があります。 過剰な数のコントロールを使用しない適切に設計されたページの場合は、パフォーマンスが十分である必要があります。 ただし、ページの読み込み時間が遅い場合は、ページ キャッシュが役立つ場合があります。
ビューが作成され、 に移動すると、ビューに InitializeAsync
関連付けられているビュー モデルの メソッドが実行されます。 詳細については、「 ナビゲーション中にパラメーターを渡す」を参照してください。
アプリの起動時のナビゲーション
アプリが起動されると、 クラスの InitNavigation
App
メソッドが呼び出されます。 以下のコード例はこのメソッドを示しています。
private Task InitNavigation()
{
var navigationService = ViewModelLocator.Resolve<INavigationService>();
return navigationService.InitializeAsync();
}
メソッドは、Autofac 依存関係挿入コンテナーに新しい NavigationService
オブジェクトを作成し、その InitializeAsync
メソッドを呼び出す前に、そのオブジェクトへの参照を返します。
注意
インターフェイスが 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
。 ビュー モデル クラスにナビゲーション ロジックを配置することは、自動テストを通じてロジックを実行できることを意味します。 さらに、ビュー モデルでは、ナビゲーションを制御するロジックを実装して、特定のビジネス ルールが確実に適用されるようにすることができます。