Share via


エンタープライズ アプリ ナビゲーション

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 クラスによって、種類が INavigationServiceNavigationService プロパティに 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 オブジェクトへの参照を返します。

次のコード例は、NavigationServiceInitializeAsync メソッドを示しています。

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 イベントが発生し、LoginViewModelNavigateCommand が実行されます。 既定では、このイベントのイベント引数がコマンドに渡されます。 このデータは、ソースとターゲットの間で渡される際に、EventArgsConverter プロパティで指定されたコンバーターによって変換され、WebNavigatingEventArgs から Url が返されます。 そのため、NavigationCommand が実行されると、Web ページの URL がパラメーターとして登録された Action に渡されます。

そして、次のコード例に示すように、NavigationCommandNavigateAsync メソッドを実行します。

private async Task NavigateAsync(string url)  
{  
    ...          
    await NavigationService.NavigateToAsync<MainViewModel>();  
    await NavigationService.RemoveLastFromBackStackAsync();  
    ...  
}

このメソッドは、MainViewModel へのナビゲーションを呼び出し、ナビゲーションに続いて、ナビゲーション スタックから LoginView ページを削除します。

ナビゲーションの確認または取り消し

場合によって、アプリで、ナビゲーション操作中にユーザーと対話し、ユーザーがナビゲーションを確認または取り消すことができるようにする必要があります。 これが必要になるのは、たとえば、ユーザーがデータ入力ページを完了する前に移動しようとしたときです。 この場合、ユーザーが、そのページから移動する、またはナビゲーション操作が発生する前に取り消すことができるようにアプリから通知する必要があります。 これは、ビュー モデル クラスで、通知からの応答を使ってナビゲーションを呼び出すかどうかを制御することで実現できます。

まとめ

Xamarin.Forms では、ページ ナビゲーションがサポートされています。これは通常、ユーザーの UI 操作によって、または内部ロジックによって状態が変わる結果としてアプリ自体から生じます。 ところが、MVVM パターンを使用するアプリで実装するナビゲーションは複雑になる場合があります。

この章では、ビューモデルからビューモデル優先ナビゲーションを実行するために使用される NavigationService クラスについて説明しました。 ビュー モデル クラスにナビゲーション ロジックを配置すると、自動テストを通じてロジックを実行できます。 さらに、ビュー モデルでは、特定のビジネス ルールが確実に適用されるようにナビゲーションを制御するロジックを実装できます。