次の方法で共有


「ナビゲーション」

ヒント

この内容は電子ブック『.NET MAUI を使用したエンタープライズ アプリケーション パターン』からの抜粋です。これは .NET Docs で閲覧することも、無料の PDF をダウンロードしてオフラインで読むこともできます。

電子ブック『.NET MAUI を使用したエンタープライズ アプリケーション パターン』の表紙のサムネイル。

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

  • ナビゲート先のビューを、ビュー間の緊密な結合と依存関係を導入しないアプローチを使用して特定する。
  • ナビゲート先のビューがインスタンス化および初期化されるプロセスを調整する。 MVVM を使用する場合、ビューとビュー モデルをインスタンス化し、ビューのバインド コンテキストを介して相互に関連付ける必要があります。 アプリで依存関係挿入コンテナーを使用している場合、ビューとビュー モデルのインスタンス化で特定の構築メカニズムが必要になる可能性があります。
  • ビュー優先ナビゲーションを実行するのか、ビューモデル優先ナビゲーションを実行するのか。 ビュー優先ナビゲーションでは、ナビゲート先のページから、ビューの種類の名前を参照します。 ナビゲーション中に、指定したビューは、対応するビューモデルやその他の依存サービスと共にインスタンス化されます。 別の方法として、ビューモデル優先ナビゲーションを使用します。このとき、ナビゲート先のページから、ビューモデルの種類の名前を参照します。
  • ビューとビューモデル全体でアプリのナビゲーション動作を明確に分離する方法を決定する。 MVVM パターンによって、アプリの UI とアプリのプレゼンテーションおよびビジネス ロジックが分離されますが、それらを結び付けるための直接のメカニズムは用意されていません。 ところが、アプリのナビゲーション動作は、多くの場合、アプリの UI 部分とプレゼンテーション部分に及びます。 ユーザーがビューからナビゲーションを開始することが多く、ナビゲーションの結果としてビューが置き換えられます。 一方、ビューモデル内からナビゲーションを開始または調整する必要があることも多くあります。
  • 初期化のためにナビゲーション中にパラメーターを渡す方法を決定する。 たとえば、ユーザーが注文の詳細を更新するためにあるビューに移動する場合、正しいデータを表示できるように、そのビューに注文データを渡す必要があります。
  • ナビゲーションを特定のビジネス ルールに確実に従うように調整する。 たとえば、ユーザーがビューから移動する前にメッセージを表示して、無効なデータを修正する、または、そのビュー内で行われたデータの変更を送信または破棄するように求めることができるようにする場合があります。

この章では、ビューモデル優先のページ ナビゲーションを実行するために使用される MauiNavigationService という名前のナビゲーション サービス クラスを示して、これらの課題に対処します。

注意

このアプリで使用される MauiNavigationService は単純化されており、可能なナビゲーションの種類のすべては網羅していません。 ご自分のアプリケーションで必要なナビゲーションの種類によっては、追加機能が必要です。

ナビゲーション ロジックは、ビューのコードビハインドまたはデータ バインドされたビューモデル内に存在できます。 ビューにナビゲーション ロジックを配置するのが最も単純な方法ですが、単体テストでテストするのは簡単ではありません。 ビューモデル クラスにナビゲーション ロジックを配置することは、単体テストを通じてロジックを検証できることを意味します。 さらに、ビューモデルでは、特定のビジネス ルールが確実に適用されるようにナビゲーションを制御するロジックを実装できます。 たとえば、アプリによっては、入力したデータが有効であることが保証されない限り、ユーザーはページから移動できません。

ナビゲーション サービスは、通常、テスト容易性を高めるために、ビューモデルから呼び出されます。 ただし、ビューモデルからビューにナビゲートするには、ビューモデルから、ビュー、特にアクティブなビューモデルが関連付けられていないビューを参照する必要がありますが、これは推奨されません。 そのため、ここに示す MauiNavigationService は、ナビゲート先としてビューモデルの種類を指定します。

eShop マルチプラットフォーム アプリは、MauiNavigationService クラスを使ってビュー モデル優先ナビゲーションを提供します。 このクラスを使用して、次のコード例に示される INavigationService インターフェイスを実装します。

public interface INavigationService
{
    Task InitializeAsync();

    Task NavigateToAsync(string route, IDictionary<string, object> routeParameters = null);

    Task PopAsync();
}

このインターフェイスで、実装クラスで次のメソッドを提供する必要があることを指定します。

メソッド 目的
InitializeAsync アプリの起動時に、2 つのページのいずれかへのナビゲーションを実行します。
NavigateToAsync(string route, IDictionary<string, object> routeParameters = null) 登録済みのナビゲーション ルートを使用して、指定されたページへの階層ナビゲーションを実行します。 必要に応じて、ナビゲーション先ページ上の処理に使用する名前付きルート パラメーターを渡すことができます
PopAsync ナビゲーション スタックから現在のページを削除します。

注意

INavigationService インターフェイスでは、通常、GoBackAsync メソッドも指定します。これは、ナビゲーション スタック内の前のページに戻るためにプログラムで使用します。 ただし、このメソッドは必要ないため、eShop マルチプラットフォーム アプリにはありません。

MauiNavigationService インスタンスの作成

次のコード例に示すように、MauiNavigationService クラスは、INavigationService インターフェイスを実装し、MauiProgram.CreateMauiApp() メソッドで、依存関係挿入コンテナーを持つシングルトンとして登録されます。

mauiAppBuilder.Services.AddSingleton<INavigationService, MauiNavigationService>();;

INavigationService インターフェイスは、次のコード例に示すように、ビューとビューモデルのコンストラクターに追加することで解決できます。

public AppShell(INavigationService navigationService)

これにより、依存関係挿入コンテナーに格納されている MauiNavigationService オブジェクトへの参照が返されます。

ViewModelBase クラスによって、種類が INavigationServiceNavigationService プロパティに MauiNavigationService インスタンスが格納されます。 そのため、すべてのビューモデル クラスが、ViewModelBase クラスから派生し、NavigationService プロパティを使用して、INavigationService インターフェイスで指定されたメソッドにアクセスできます。

ナビゲーション要求の処理

.NET MAUI には、アプリケーション内でナビゲートするための複数の方法が用意されています。 従来のナビゲーションは、NavigationPage クラスを使用する方法で、ユーザーが必要に応じて前後にページを移動できる階層ナビゲーション エクスペリエンスを実装します。 eShop アプリは、アプリケーションのルート コンテナー、およびナビゲーション ホストとして、Shell コンポーネントを使います。 シェル ナビゲーションの詳細については、Microsoft デベロッパー センターの シェル ナビゲーションに関するページを参照してください。

ナビゲーションは、次のコード例に示すように、ナビゲーション先のページのルート パスを指定して、NavigateToAsync メソッドのいずれかを呼び出すと、ビューモデル クラス内で実行されます。

await NavigationService.NavigateToAsync("//Main");

次のコード例は、MauiNavigationService クラスから提供される NavigateToAsync メソッドを示しています。

public Task NavigateToAsync(string route, IDictionary<string, object> routeParameters = null)
{
    return
        routeParameters != null
            ? Shell.Current.GoToAsync(route, routeParameters)
            : Shell.Current.GoToAsync(route);
}

.NET MAUIShell コントロールはルート ベースのナビゲーションを既に熟知しているため、NavigateToAsync メソッドは、この機能をマスクするように動作します。 NavigateToAsync メソッドで、ナビゲート先のビューモデルに渡す引数としてナビゲーション データを指定でき、これは通常、初期化の実行に使用されます。 詳細については、「ナビゲーション中にパラメーターを渡す」を参照してください。

重要

.NET MAUI でナビゲーションを実行するには、複数の方法があります。 MauiNavigationService は、Shell で動作するように特別にビルドされています。 NavigationPageTabbedPage、または別のナビゲーション メカニズムを使用している場合、これらのコンポーネントを使用して機能するように、このルーティング サービスを更新する必要があります。

MauiNavigationService のルートを登録するには、XAML またはコードビハインドでルート情報を指定する必要があります。 次の例は、XAML を介したルートの登録を示しています。

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:views="clr-namespace:eShop.Views"
    x:Class="eShop.AppShell">

    <!-- Omitted for brevity -->

    <FlyoutItem >
        <ShellContent x:Name="login" ContentTemplate="{DataTemplate views:LoginView}" Route="Login" />
    </FlyoutItem>

    <TabBar x:Name="main" Route="Main">
        <ShellContent Title="CATALOG" Route="Catalog" Icon="{StaticResource CatalogIconImageSource}" ContentTemplate="{DataTemplate views:CatalogView}" />
        <ShellContent Title="PROFILE" Route="Profile" Icon="{StaticResource ProfileIconImageSource}" ContentTemplate="{DataTemplate views:ProfileView}" />
    </TabBar>
</Shell>

この例では、ShellContent および TabBar ユーザー インターフェイス オブジェクトで Route プロパティを設定しています。 Shell によって制御されるユーザー インターフェイス オブジェクトのルートを登録する場合は、この方法をお勧めします。

後でナビゲーション スタックに追加されるオブジェクトがある場合は、コードビハインドを介してそれらを追加する必要があります。 次の例は、コードビハインドのルートの登録を示しています。

Routing.RegisterRoute("Filter", typeof(FiltersView));
Routing.RegisterRoute("Basket", typeof(BasketView));

コードビハインドでは、ルート名を第 1 パラメーター、ビューの種類を第 2 パラメーターとして受け取る Routing.RegisterRoute メソッドを呼び出します。 ビューモデルで NavigationService プロパティを使用してナビゲートする場合は、アプリケーションの Shell オブジェクトで、登録済みのルートを探してナビゲーション スタックにプッシュします。

ビューが作成されてナビゲートされると、ビューの関連付けられたビューモデルの ApplyQueryAttributes メソッドと InitializeAsync メソッドが実行されます。 詳細については、「ナビゲーション中にパラメーターを渡す」を参照してください。

アプリを起動すると、Shell オブジェクトがアプリケーションのルート ビューとして設定されます。 設定されたら、Shell がルート登録を制御するために使用され、今後、アプリケーションのルートに存在するようになります。 Shell が作成されたら、OnParentSet メソッドを使用してナビゲーション ルートを初期化することで、アプリケーションにアタッチされるのを待つことができます。 以下のコード例はこのメソッドを示しています。

protected override async void OnParentSet()
{
    base.OnParentSet();

    if (Parent is not null)
    {
        await _navigationService.InitializeAsync();
    }
}

このメソッドでは、依存関係挿入からコンストラクターが提供される INavigationService のインスタンスを使用し、その InitializeAsync メソッドを呼び出します。

次のコード例は、MauiNavigationService.InitializeAsync メソッドの実装を示しています。

public Task InitializeAsync()
{
    return NavigateToAsync(string.IsNullOrEmpty(_settingsService.AuthAccessToken)
        ? "//Login"
        : "//Main/Catalog");
}

アプリに、キャッシュされたアクセス トークンがあり、これを認証に使用する場合は、//Main/Catalog ルートにナビゲートされます。 それ以外の場合は、//Loginルートにナビゲートされます。

ナビゲーション中にパラメーターを渡す

INavigationService インターフェイスで指定する NavigateToAsync メソッドでは、ナビゲーション先のビューモデルに渡されるデータの IDictionary<string, object> としてナビゲーション データを指定でき、これは通常、初期化の実行に使用されます。

たとえば、ProfileViewModel クラスには、ユーザーが ProfileView ページで注文を選択したときに実行される OrderDetailCommand が含まれています。 そして、次のコード例に示すように、OrderDetailAsync メソッドが実行されます。

private async Task OrderDetailAsync(Order order)
{
    if (order is null)
    {
        return;
    }

    await NavigationService.NavigateToAsync(
        "OrderDetail",
        new Dictionary<string, object>{ { "OrderNumber", order.OrderNumber } });
}

このメソッドから、OrderDetail ルートへのナビゲーションが呼び出され、ユーザーが選択した注文の注文番号情報が渡されます。 依存関係挿入フレームワークによって、ビューの BindingContext に割り当てられている OrderDetailViewModel クラスと共に OrderDetail ルートの OrderDetailView が作成されます。 OrderDetailViewModel には、次のコード例に示すように、ナビゲーション サービスからデータを受信できるようにする属性が追加されています。

[QueryProperty(nameof(OrderNumber), "OrderNumber")]
public class OrderDetailViewModel : ViewModelBase
{
    public int OrderNumber { get; set; }
}

QueryProperty 属性を使用すると、値をマップするプロパティのパラメーターと、クエリ パラメーター ディクショナリから値を検索するためのキーを指定できます。 この例では、NavigateToAsync 呼び出し中にキー "OrderNumber" と注文番号の値が指定されました。 ビューモデルで、"OrderNumber" キーが見つかり、その値が OrderNumber プロパティにマップされました。 OrderNumber プロパティを後で使用して、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 がパラメーターとして登録済みアクションに渡されます。

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

private async Task NavigateAsync(string url)
{
    // Omitted for brevity.
    if (!string.IsNullOrWhiteSpace(accessToken))
    {
        _settingsService.AuthAccessToken = accessToken;
        _settingsService.AuthIdToken = authResponse.IdentityToken;
        await NavigationService.NavigateToAsync("//Main/Catalog");
    }
}

このメソッドにより、NavigationService アプリケーションが //Main/Catalog ルートにルーティングされます。

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

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

まとめ

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

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