导航

提示

此内容摘自电子书《使用 .NET MAUI 的企业应用程序模式》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

《使用 .NET MAUI 的企业应用程序模式》电子书包含缩略图。

.NET MAUI 包括对页面导航的支持,这通常是用户与 UI 交互或由于内部逻辑驱动的状态更改应用本身导致的结果。 但是,在使用模型-视图-视图模型 (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 启动应用时,导航到两个页面之一。
NavigateToAsync(string route, IDictionary<string, object> routeParameters = null) 使用注册的导航路由执行到指定页面的分层导航。 可以选择传递指定的路由参数以用于在目标页面上进行处理
PopAsync 从导航堆栈中删除当前页面。

注意

INavigationService 接口通常还会指定 GoBackAsync 方法,该方法用于以编程方式返回到导航堆栈中的上一页。 但是,eShop 多平台应用中缺少此方法,因为该方法不是必需的。

创建 MauiNavigationService 实例

实现 INavigationService 接口的 MauiNavigationService 类在 MauiProgram.CreateMauiApp() 方法中使用依赖项注入容器注册为单一实例,如以下代码示例所示:

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

然后,可以通过将 INavigationService 接口添加到视图和视图模型的构造函数中对其进行解析,如以下代码示例所示:

public AppShell(INavigationService navigationService)

这将返回对存储在依赖项注入容器中的 MauiNavigationService 对象的引用。

ViewModelBase 类将 MauiNavigationService 实例存储在 INavigationService 类型的 NavigationService 属性中。 因此,从 ViewModelBase 派生的类的所有视图模型类都可以使用 NavigationService 属性来访问由 INavigationService 接口指定的方法。

处理导航请求

.NET MAUI 提供多种在应用程序中导航的方法。 传统的导航方法是使用 NavigationPage 类,该类实现分层导航体验,用户可以根据需要在页面中向前和向后导航。 eShop 应用使用 Shell 组件作为应用程序的根容器和导航主机。 有关 Shell 导航的详细信息,请参阅 Microsoft 开发人员中心上的 Shell 导航

通过调用 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>

在此示例中,ShellContentTabBar 用户界面对象正在设置其 Route 属性。 这是为由 Shell 控制的用户界面对象注册路由的首选方法。

如果我们有稍后将添加到导航堆栈的对象,则将需要通过代码隐藏添加这些对象。 以下示例显示在代码隐藏中注册路由。

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

在代码隐藏中,我们将调用 Routing.RegisterRoute 方法,该方法将路由名称作为第一个参数,将视图类型作为第二个参数。 当视图模型使用 NavigationService 属性进行导航时,应用程序的 Shell 对象将查找已注册的路由并将其推送到导航堆栈上。

在创建并导航到视图后,将执行视图的关联视图模型的 ApplyQueryAttributesInitializeAsync 方法。 有关详细信息,请参阅在导航过程中传递参数

启动应用时,将 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 路由的导航,并将订单编号信息传递给用户所选的订单。 当依赖项注入框架为 OrderDetail 路由创建 OrderDetailView 和分配给视图的 BindingContextOrderDetailViewModel 类时, 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 导航到网页时,将触发 Navigating 事件,该事件将执行 LoginViewModel 中的 NavigateCommand。 默认情况下,将事件的事件参数传递给命令。 此数据在通过 EventArgsConverter 属性中指定的转换器在源和目标之间传递时进行转换,该属性返回 WebNavigatingEventArgs 中的 Url。 因此,在执行 NavigationCommand 时,会将网页的 Url 以参数形式传递给注册的 Action。

反过来,NavigationCommand 执行 NavigateAsync 方法,如以下代码示例所示:

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 类,用于从视图模型执行视图模型优先导航。 将导航逻辑置于视图模型类中,这意味着可以通过自动测试来执行逻辑。 此外,视图模型可以实现用于控制导航的逻辑,确保强制执行某些业务规则。