Xamarin.Forms包括对页面导航的支持,这通常是由于内部逻辑驱动的状态更改导致用户与UI或应用程序本身的交互而产生的。 但是,在使用Model-View-ViewModel(MVVM)模式的应用程序中实现导航可能非常复杂,因为必须满足以下挑战:
· 如何识别要导航的视图,使用不引入紧密耦合和视图之间的依赖关系的方法。
· 如何协调将导航视图的过程实例化和初始化。当使用MVVM时,视图和视图模型需要通过视图的绑定上下文来实例化并相互关联。当应用程序使用依赖注入容器时,视图和视图模型的实例化可能需要特定的构造机制。
· 是否执行视图优先导航,或查看模型导航。使用视图优先导航,导航到的页面是指视图类型的名称。在导航期间,指定的视图被实例化,以及其对应的视图模型和其他依赖服务。另一种方法是使用视图模型第一导航,其中导航的页面引用视图模型类型的名称。
· 如何在视图和视图模型之间干净地分离应用程序的导航行为。 MVVM模式提供了应用程序的UI与其表示和业务逻辑之间的分离。但是,应用程序的导航行为通常会跨越应用程序的UI和演示文稿部分。用户将经常从视图启动导航,并且视图将由于导航而被替换。然而,通常还需要在视图模型中启动或协调导航。
· 如何在导航期间传递参数以进行初始化。例如,如果用户浏览到更新订单详细信息的视图,则订单数据将必须传递到视图,以便它可以显示正确的数据。
· 如何协调导航,以确保遵守某些业务规则。例如,在导航远离视图之前可能会提示用户,以便它们可以更正任何无效数据,或者被提示提交或丢弃在视图中进行的任何数据更改。
本章通过介绍用于执行视图模型第一页导航的NavigationService类来解决这些挑战。
注意:应用程序使用的NavigationService仅用于在ContentPage实例之间执行分层导航。 使用该服务在其他页面类型之间导航可能会导致意外的行为。
在页之间导航
导航逻辑可以驻留在视图的代码隐藏或数据绑定视图模型中。将导航逻辑放置在视图中可能是最简单的方法,但是通过单元测试不容易测试。在视图模型类中放置导航逻辑意味着可以通过单元测试来执行逻辑。此外,视图模型可以实现逻辑来控制导航,以确保执行某些业务规则。例如,一个应用程序可能不允许用户在不首先确保输入的数据有效的情况下远离页面。
NavigationService类通常从视图模型中调用,以便提升可测试性。但是,从视图模型导航到视图将需要视图模型来引用视图,特别是主动视图模型未关联的视图,这是不推荐的。因此,此处提供的NavigationService将视图模型类型指定为导航到的目标。
eShopOnContainers移动应用程序使用NavigationService类提供视图模型第一导航。该类实现了INavigationService接口,如下面的代码示例所示:
点击(此处)折叠或打开
-
public interface INavigationService
-
{
-
ViewModelBase PreviousPageViewModel { get; }
-
Task InitializeAsync();
-
Task NavigateToAsync() where TViewModel : ViewModelBase;
-
Task NavigateToAsync(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().As().SingleInstance();
INavigationService接口在ViewModelBase类构造函数中解析,如下面的代码示例所示:
点击(此处)折叠或打开
- NavigationService = ViewModelLocator.Resolve();
这将返回对存储在Autofac依赖注入容器中的NavigationService对象的引用,该容器由App类中的InitNavigation方法创建。 有关详细信息,请参阅。
ViewModelBase类将NavigationService实例存储在一个NavigationService属性中,类型为INavigationService。 因此,从ViewModelBase类派生的所有视图模型类都可以使用NavigationService属性来访问INavigationService接口指定的方法。 这避免了将NavigationService对象从Autofac依赖注入容器注入到每个视图模型类中的开销。
处理导航请求
Xamarin.Forms提供了NavigationPage类,它实现了一种分层导航体验,用户可以根据需要向前和向后浏览页面。 有关分层导航的更多信息,请参阅Xamarin开发人员中心的分层导航。
eShopOnContainers应用程序不直接使用NavigationPage类,而是将CustomPage类包装在CustomNavigationView类中,如以下代码示例所示:
点击(此处)折叠或打开
-
public partial class CustomNavigationView : NavigationPage
-
{
-
public CustomNavigationView() : base()
-
{
-
InitializeComponent();
-
}
-
-
public CustomNavigationView(Page root) : base(root)
-
{
-
InitializeComponent();
-
}
- }
此包装的目的是为了方便对类的XAML文件中的NavigationPage实例进行样式化。
通过调用NavigateToAsync方法之一,在视图模型类中执行导航,指定要导航的页面的视图模型类型,如以下代码示例所示:
点击(此处)折叠或打开
- await NavigationService.NavigateToAsync();
以下代码示例显示了NavigationService类提供的NavigateToAsync方法:
点击(此处)折叠或打开
- await NavigationService.NavigateToAsync();
点击(此处)折叠或打开
-
public Task NavigateToAsync() where TViewModel : ViewModelBase
-
{
-
return InternalNavigateToAsync(typeof(TViewModel), null);
-
}
-
-
public Task NavigateToAsync(object parameter) where TViewModel : ViewModelBase
-
{
-
return InternalNavigateToAsync(typeof(TViewModel), parameter);
- }
每个方法允许从ViewModelBase类派生的任何视图模型类,通过调用InternalNavigateToAsync方法执行分层导航。 另外,第二个NavigateToAsync方法使导航数据能够被指定为传递给正在导航的视图模型的参数,通常用于执行初始化。 有关更多信息,请参阅。
nternalNavigateToAsync方法执行导航请求,并显示在以下代码示例中:
点击(此处)折叠或打开
-
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子命名空间中。
· 查看名称对应于查看模型名称,并删除“模型”。
当视图被实例化时,它与其对应的视图模型相关联。 有关如何发生这种情况的更多信息,请参阅。
如果正在创建的视图是LoginView,它将被包装在CustomNavigationView类的新实例中,并分配给Application.Current.MainPage属性。 否则,将检索CustomNavigationView实例,并且如果它不为null,则调用PushAsync方法将正在创建的视图推送到导航堆栈。 但是,如果检索到的CustomNavigationView实例为空,则正在创建的视图将被包装在CustomNavigationView类的新实例中,并分配给Application.Current.MainPage属性。 这种机制确保在导航期间,页面被正确添加到导航堆栈,当它是空的时候,当它包含数据时。
? 提示:考虑缓存页面。 页面缓存导致当前未显示的视图的内存消耗。 但是,没有页面缓存,这意味着XAML解析和构建页面及其视图模型将在每次导航到新页面时出现,这可能会对复杂页面产生影响。 对于精心设计的页面,不使用过多的控件,性能就足够了。 但是,如果遇到缓慢的页面加载时间,页面缓存可能会有帮助。
视图创建和导航后,视图的关联视图模型的InitializeAsync方法被执行。 有关更多信息,请参阅。
应用程序启动时导航
当应用程序启动时,App类中的InitNavigation方法被调用。 以下代码示例显示了此方法:
点击(此处)折叠或打开
-
private Task InitNavigation()
-
{
-
var navigationService = ViewModelLocator.Resolve();
-
return navigationService.InitializeAsync();
- }
该方法在Autofac依赖注入容器中创建一个新的NavigationService对象,并在调用其InitializeAsync方法之前返回对它的引用。
注意:当InavigationService接口由ViewModelBase类解析时,容器返回对调用InitNavigation方法时创建的NavigationService对象的引用。
以下代码示例显示了NavigationService InitializeAsync方法:
点击(此处)折叠或打开
-
public Task InitializeAsync()
-
{
-
if (string.IsNullOrEmpty(Settings.AuthAccessToken))
-
return NavigateToAsync();
-
else
-
return NavigateToAsync();
- }
如果应用程序具有用于身份验证的缓存访问令牌,则MainView将导航。 否则,导航到LoginView。
有关Autofac依赖注入容器的详细信息,请参阅。
导航时传递参数
由INavigationService接口指定的NavigateToAsync方法之一使导航数据能够被指定为传递给正在导航的视图模型的参数,通常用于执行初始化。
例如,ProfileViewModel类包含一个OrderDetailCommand,当用户在ProfileView页面上选择一个顺序时,它将执行。 反过来,这将执行OrderDetailAsync方法,如下面的代码示例所示:
点击(此处)折叠或打开
-
private async Task OrderDetailAsync(Order order)
-
{
-
await NavigationService.NavigateToAsync(order);
- }
此方法调用导航到OrderDetailViewModel,传递一个Order实例,该实例表示用户在ProfileView页面上选择的顺序。 当NavigationService类创建OrderDetailView时,OrderDetailViewModel类被实例化并分配给视图的BindingContext。 导航到OrderDetailView后,InternalNavigateToAsync方法执行视图关联视图模型的InitializeAsync方法。
InitializeAsync方法在ViewModelBase类中定义为可以覆盖的方法。 此方法指定一个对象参数,表示在导航操作期间要传递给视图模型的数据。 因此,要从导航操作接收数据的视图模型类提供了自己的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>
然后,NavigationCommand会执行NavigateAsync方法,如下面的代码示例所示:
点击(此处)折叠或打开
-
private async Task NavigateAsync(string url)
-
{
-
...
-
await NavigationService.NavigateToAsync();
-
await NavigationService.RemoveLastFromBackStackAsync();
-
...
- }
确认或取消导航
应用程序可能需要在导航操作期间与用户进行交互,以便用户可以确认或取消导航。 这可能是必需的,例如,当用户在完全完成数据输入页面之前尝试导航。 在这种情况下,应用程序应提供允许用户离开页面的通知,或者在发生之前取消导航操作。 这可以在视图模型类中通过使用来自通知的响应来控制是否引用导航来实现。
概要
amarin.Forms包括对页面导航的支持,这通常是由于内部逻辑驱动的状态更改的结果,用户与UI的交互或应用程序本身。 然而,在使用MVVM模式的应用程序中实现导航可能很复杂。
本章介绍了一个NavigationService类,用于从视图模型中执行视图模型优先导航。 在视图模型类中放置导航逻辑意味着逻辑可以通过自动测试来实现。 此外,视图模型可以实现逻辑来控制导航,以确保执行某些业务规则。