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

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 クラスによって、種類が INavigationServiceNavigationService プロパティに 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 関連付けられているビュー モデルの メソッドが実行されます。 詳細については、「 ナビゲーション中にパラメーターを渡す」を参照してください。

アプリが起動されると、 クラスの InitNavigationApp メソッドが呼び出されます。 以下のコード例はこのメソッドを示しています。

private Task InitNavigation()  
{  
    var navigationService = ViewModelLocator.Resolve<INavigationService>();  
    return navigationService.InitializeAsync();  
}

メソッドは、Autofac 依存関係挿入コンテナーに新しい NavigationService オブジェクトを作成し、その InitializeAsync メソッドを呼び出す前に、そのオブジェクトへの参照を返します。

注意

インターフェイスが 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呼び出し、ユーザーがページで選択した順序を表すインスタンスをProfileViewOrderします。 クラスが をNavigationServiceOrderDetailView作成すると、 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 。 ビュー モデル クラスにナビゲーション ロジックを配置することは、自動テストを通じてロジックを実行できることを意味します。 さらに、ビュー モデルでは、ナビゲーションを制御するロジックを実装して、特定のビジネス ルールが確実に適用されるようにすることができます。