企业应用导航

注意

本电子书于 2017 年春季出版,之后再未更新。 书中有许多内容仍然很有价值,但有些材料已经过时。

Xamarin.Forms 包括对页面导航的支持,这通常是用户与 UI 交互或由于内部逻辑驱动的状态更改应用本身导致的结果。 但是,在使用模型-视图-视图模型 (MVVM) 模式的应用中实现导航可能会很复杂,因为必须满足以下挑战:

  • 如何使用不会在视图之间引入紧密耦合和依赖项的方法来识别要导航到的视图。
  • 如何协调要导航到的视图的实例化和初始化过程。 当使用 MVVM 时,需要对视图和视图模型进行实例化并通过视图的绑定上下文相互关联。 当应用使用依赖项注入容器时,视图和视图模型的实例化可能需要特定的构造机制。
  • 是执行视图优先导航还是视图模型优先导航。 对于视图优先导航,要导航到的页面是指视图类型的名称。 在导航过程中,将对指定的视图及其对应的视图模型和其他相关服务进行实例化。 另一种方法是使用视图模型优先导航,在此方案中,要导航到的页面是指视图模型类型的名称。
  • 如何在视图和视图模型之间清楚地区分应用的导航行为。 利用 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 启动应用时,导航到两个页面之一。
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>();

这会返回对 Autofac 依赖项注入容器中存储的 NavigationService 对象的引用,该对象由 App 类中的 InitNavigation 方法创建。 有关详细信息,请参启动应用时导航

ViewModelBase 类将 NavigationService 实例存储在 INavigationService 类型的 NavigationService 属性中。 因此,从 ViewModelBase 派生的类的所有视图模型类都可以使用 NavigationService 属性来访问由 INavigationService 接口指定的方法。 这可以避免将 NavigationService 对象从 Autofac 依赖项注入容器注入到每个视图模型类而产生的开销。

处理导航请求

Xamarin.Forms 提供 NavigationPage 类,该类实现分层导航体验,用户可以根据需要向前和向后导航页面。 有关分层导航的详细信息,请参阅 分层导航

eShopOnContainers 应用将 NavigationPage 类包装在 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>();

以下代码示例显示由 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 方法来执行分层导航。 此外,利用第二个 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 方法之前返回对其的引用。

注意

ViewModelBase 类解析 INavigationService 接口时,容器将返回对调用 InitNavigation 方法时创建的 NavigationService 对象的引用。

下面的代码示例说明 NavigationServiceInitializeAsync 方法:

public Task InitializeAsync()  
{  
    if (string.IsNullOrEmpty(Settings.AuthAccessToken))  
        return NavigateToAsync<LoginViewModel>();  
    else  
        return NavigateToAsync<MainViewModel>();  
}

如果应用具有用于身份验证的缓存访问令牌,则会导航到 MainView。 否则,将导航到 LoginView

有关 Autofac 依赖项注入容器的详细信息,请参阅依赖项注入简介

在导航过程中传递参数

INavigationService 接口指定的其中一个 NavigateToAsync 方法支持将导航数据指定为传递给要导航到的视图模型的参数(通常用于执行初始化)。

例如,ProfileViewModel 类包含当用户在 ProfileView 页面上选择订单时执行的 OrderDetailCommand。 反过来,这将执行 OrderDetailAsync 方法,如以下代码示例所示:

private async Task OrderDetailAsync(Order order)  
{  
    await NavigationService.NavigateToAsync<OrderDetailViewModel>(order);  
}

此方法调用到 OrderDetailViewModel 的导航,并传递一个 Order 实例,该实例表示用户在 ProfileView 页上选择的订单。 当 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 导航到网页时,将触发 Navigating 事件,该事件将执行 LoginViewModel 中的 NavigateCommand。 默认情况下,将事件的事件参数传递给命令。 此数据在通过 EventArgsConverter 属性中指定的转换器在源和目标之间传递时进行转换,该属性返回 WebNavigatingEventArgs 中的 Url。 因此,在执行 NavigationCommand 时,会将网页的 URL 以参数形式传递给注册的 Action

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

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

此方法调用到 MainViewModel 的导航,并在导航后从导航堆栈中移除 LoginView 页。

确认或取消导航

应用可能需要在导航操作期间与用户交互,这样用户就可以确认或取消导航。 例如,当用户尝试在完全完成数据输入页面之前导航时,需要这样做。 在这种情况下,应用应提供一个通知,允许用户离开页面或在导航操作发生之前取消导航操作。 在视图模型类中通过使用来自通知的响应来控制是否调用导航,也可以实现此目的。

总结

Xamarin.Forms 包括对页面导航的支持,这通常是用户与 UI 交互或由于内部逻辑驱动的状态更改应用本身导致的结果。 但是,在使用 MVVM 模式的应用中实现导航可能会很复杂。

本章介绍了 NavigationService 类,用于从视图模型执行视图模型优先导航。 将导航逻辑置于视图模型类中,这意味着可以通过自动测试来执行逻辑。 此外,视图模型可以实现用于控制导航的逻辑,确保强制执行某些业务规则。