探索力学
如您所见,Push和Pop方法返回Task对象。 通常,在调用这些方法时,您将使用await。 这是在ModelessAndModal的MainPage类中调用PushAsync:
await Navigation.PushAsync(new ModelessPage());
假设您在此声明后面有一些代码。 该代码何时执行? 我们知道它在PushAsync任务完成时执行,但是什么时候? 在用户点击ModelessPage上的Back按钮返回MainPage之后?
不,事实并非如此。 PushAsync任务完成得非常快。 此任务的完成并不表示页面导航过程已完成,但它确实指示何时可以安全地获取页面导航堆栈的当前状态。
在PushAsync或PushModalAsync调用之后,会发生以下事件。 但是,这些事件发生的确切顺序取决于平台:
- 调用PushAsync或PushModalAsync的页面通常会调用其OnDisappearing覆盖。
- 正在导航的页面将调用其OnAppearing覆盖。
- SwapAsync或PushModalAsync任务完成。
重复:这些事件发生的顺序取决于平台以及是否导航到无模式页面或模态页面。
在PopAsync或PopModalAsync调用之后,将再次按照与平台相关的顺序发生以下事件:
- 调用PopAsync或PopModalAsync的页面调用其OnDisappearing覆盖。
- 返回的页面通常会调用其OnAppearing覆盖。
- PopAsync或PopModalAsync任务返回。
您会注意到这些描述中“一般”一词的两种用法。当Android设备导航到模态页面时,此单词指的是这些规则的例外情况。调用PushModalAsync的页面不会调用其OnDisappearing覆盖,并且当模式页面调用PopModalAsync时,同一页面不会调用其OnAppearing覆盖。
此外,对OnDisappearing和OnAppearing覆盖的调用不一定表示页面导航。在iOS上,程序终止时在活动页面上调用OnDisappearing覆盖。在Windows Phone Silverlight平台(Xamarin.Forms不再支持)上,当用户在页面上调用Picker,DatePicker或TimePicker时,会收到OnDisappearing调用的页面。由于这些原因,OnDisappearing和OnAppearing覆盖不能被视为页面导航的保证指示,尽管有时必须将它们用于此目的。
定义这些Push和Pop调用的INavigation接口还定义了两个提供对实际导航堆栈的访问的属性:
- NavigationStack,包含无模式页面
- ModalStack,包含模态页面
这两个属性的set访问器不是公共的,属性本身的类型为IReadOnlyList ,因此您无法直接修改它们。 (正如您将看到的,方法可用于以更加结构化的方式修改页面堆栈。)尽管这些属性未使用Stack 类实现,但它们仍然像堆栈一样运行。 索引为零的IReadOnlyList中的项目是最旧的页面,最后一项是最近的页面。
这两个无模式和模态页面集合的存在表明无模式和模态页面导航不能混合,这是真的:无模式页面可以导航到模态页面,但模态页面无法导航到无模式页面。
一些实验表明,不同页面实例的Navigation属性保留了导航堆栈的不同集合。 (特别是,在导航到模态页面后,与该模态页面关联的NavigationStack为空。)最简单的方法是使用由NavigationPage实例的Navigation属性维护的这些集合的实例,这些实例设置为MainPage属性。 App类。
每次调用PushAsync或PopAsync时,NavigationStack的内容都会更改 - 要么将新页面添加到集合中,要么从集合中删除页面。同样,每次调用PushModalAsync或PopModalAsync时,ModalStack的内容都会发生变化。
实验表明,在页面导航进行过程中,在调用OnAppearing或OnDisappearing覆盖期间使用NavigationStack或ModalStack的内容是不安全的。适用于所有平台的唯一方法是等待PushAsync,PushModalAsync,PopAsync或PopModalAsync任务完成。这表明这些堆栈集稳定而准确。
NavigationPage类还定义了一个名为CurrentPage的get-only属性。此页面实例与NavigationPage中可用的NavigationStack集合中的最后一个项目相同。但是,当模态页面处于活动状态时,CurrentPage将继续指示在导航到模态页面之前处于活动状态的最后一个无模式页面。
让我们使用名为SinglePageNavigation的程序探索页面导航的细节和机制,因为程序只包含一个名为SinglePageNavigationPage的页面类。该程序在这一类的各种实例之间导航。
SinglePageNavigation程序的目的之一是准备编写在应用程序挂起或终止时保存导航堆栈的应用程序,并在应用程序重新启动时恢复堆栈。这样做取决于您的应用程序从NavigationStack和ModalStack属性中提取可信信息的能力。
这是SinglePageNavigationPage类的XAML文件:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="SinglePageNavigation.SinglePageNavigationPage"
x:Name="page">
<StackLayout>
<StackLayout.Resources>
<ResourceDictionary>
<Style x:Key="baseStyle" TargetType="View">
<Setter Property="VerticalOptions" Value="CenterAndExpand" />
</Style>
<Style TargetType="Button" BasedOn="{StaticResource baseStyle}">
<Setter Property="HorizontalOptions" Value="Center" />
</Style>
<Style TargetType="Label" BasedOn="{StaticResource baseStyle}">
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
</ResourceDictionary>
</StackLayout.Resources>
<Label Text="{Binding Source={x:Reference page}, Path=Title}" />
<Button x:Name="modelessGoToButton"
Text="Go to Modeless Page"
Clicked="OnGoToModelessClicked" />
<Button x:Name="modelessBackButton"
Text="Back from Modeless Page"
Clicked="OnGoBackModelessClicked" />
<Button x:Name="modalGoToButton"
Text="Go to Modal Page"
Clicked="OnGoToModalClicked" />
<Button x:Name="modalBackButton"
Text="Back from Modal Page"
Clicked="OnGoBackModalClicked" />
<Label x:Name="currentPageLabel"
Text=" " />
<Label x:Name="modelessStackLabel"
Text=" " />
<Label x:Name="modalStackLabel"
Text=" " />
</StackLayout>
</ContentPage>
XAML文件实例化四个Button和四个Label元素。 第一个Label具有数据绑定以显示页面的Title属性,因此无论平台如何以及页面是模态还是无模式,标题都是可见的。 这四个按钮用于导航到无模式或模态页面。 其余三个标签显示代码中的其他信息集。
这大致是代码隐藏文件的前半部分。 请注意构造函数代码,它将Titleproperty设置为文本“Page#”,其中哈希符号表示第一个实例化页面从零开始的数字。 每次实例化此类时,该数字都会增加:
public partial class SinglePageNavigationPage : ContentPage
{
static int count = 0;
static bool firstPageAppeared = false;
static readonly string separator = new string('-', 20);
public SinglePageNavigationPage()
{
InitializeComponent();
// Set Title to zero-based instance of this class.
Title = "Page " + count++;
}
async void OnGoToModelessClicked(object sender, EventArgs args)
{
SinglePageNavigationPage newPage = new SinglePageNavigationPage();
Debug.WriteLine(separator);
Debug.WriteLine("Calling PushAsync from {0} to {1}", this, newPage);
await Navigation.PushAsync(newPage);
Debug.WriteLine("PushAsync completed");
// Display the page stack information on this page.
newPage.DisplayInfo();
}
async void OnGoToModalClicked(object sender, EventArgs args)
{
SinglePageNavigationPage newPage = new SinglePageNavigationPage();
Debug.WriteLine(separator);
Debug.WriteLine("Calling PushModalAsync from {0} to {1}", this, newPage);
await Navigation.PushModalAsync(newPage);
Debug.WriteLine("PushModalAsync completed");
// Display the page stack information on this page.
newPage.DisplayInfo();
}
async void OnGoBackModelessClicked(object sender, EventArgs args)
{
Debug.WriteLine(separator);
Debug.WriteLine("Calling PopAsync from {0}", this);
Page page = await Navigation.PopAsync();
Debug.WriteLine("PopAsync completed and returned {0}", page);
// Display the page stack information on the page being returned to.
NavigationPage navPage = (NavigationPage)App.Current.MainPage;
((SinglePageNavigationPage)navPage.CurrentPage).DisplayInfo();
}
async void OnGoBackModalClicked(object sender, EventArgs args)
{
Debug.WriteLine(separator);
Debug.WriteLine("Calling PopModalAsync from {0}", this);
Page page = await Navigation.PopModalAsync();
Debug.WriteLine("PopModalAsync completed and returned {0}", page);
// Display the page stack information on the page being returned to.
NavigationPage navPage = (NavigationPage)App.Current.MainPage;
((SinglePageNavigationPage)navPage.CurrentPage).DisplayInfo();
}
protected override void OnAppearing()
{
base.OnAppearing();
Debug.WriteLine("{0} OnAppearing", Title);
if (!firstPageAppeared)
{
DisplayInfo();
firstPageAppeared = true;
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
Debug.WriteLine("{0} OnDisappearing", Title);
}
// Identify each instance by its Title.
public override string ToString()
{
return Title;
}
__
}
四个按钮的每个Clicked处理程序都使用Debug.WriteLine显示一些信息。当您在Visual Studio或Xamarin Studio中的调试器下运行该程序时,此文本显示在“输出”窗口中。
代码隐藏文件还会覆盖OnAppearing和OnDisappearing方法。这些很重要,因为它们通常会在页面导航到(OnAppearing)或从(OnDisappearing)导航时告诉您。
但是,如前所述,Android有点不同:调用PushModalAsync的Android页面无法调用其OnDisappearing方法,并且当模态页面返回到该页面时,原始页面不会获得对其OnAppearing的相应调用方法。就像页面在显示模态页面时保持在背景中一样,情况就是如此:如果你回到ModelessAndModal并将模态页面的BackgroundColor设置为Color.From Rgba(0,0,0,0.5) ),你可以看到模态页面后面的上一页。但这只是Android的情况。
SinglePageNavigationPage中的所有Clicked处理程序都会调用名为DisplayInfo的方法。此方法如下所示,显示有关NavigationStack和ModalStack的信息(包括堆栈中的页面)和NavigationPage对象维护的CurrentPage属性。
但是,这些Clicked处理程序不会在页面的当前实例中调用DisplayInfo方法,因为Clicked处理程序正在转换到另一个页面。 Clicked处理程序必须在它们要导航的页面实例中调用DisplayInfo方法。
调用PushAsync和PushModalAsync的Clicked处理程序中的DisplayInfo调用很容易,因为每个Clicked处理程序已经导航到新的页面实例。 调用PopAsync和PopModalAsync的Clicked处理程序中的DisplayInfo调用稍微困难一些,因为它们需要获取返回的页面。 这不是从PopAsync和PopModalAsync任务返回的Page实例。 该Page实例与调用这些方法的页面相同。
相反,调用PopAsync和PopModalAsync的Clicked处理程序从NavigationPage的CurrentPage属性获取返回的页面:
NavigationPage navPage = (NavigationPage)App.Current.MainPage;
((SinglePageNavigationPage)navPage.CurrentPage).DisplayInfo();
重要的是获取此新CurrentPage属性的代码和对DisplayInfo的调用都发生在异步Push或Pop任务完成之后。 这个信息何时生效。
但是,必须在程序首次启动时调用DisplayInfo方法。 正如您将看到的,DisplayInfo利用App类的MainPage属性来获取在App构造函数中实例化的NavigationPage。 但是,当SinglePageNavigationPage构造函数执行时,尚未在App构造函数中设置该MainPage属性,因此页面构造函数无法调用DisplayInfo。 相反,OnAppearing覆盖进行该调用,但仅适用于第一个页面实例:
if (!firstPageAppeared)
{
DisplayInfo();
firstPageAppeared = true;
}
除了显示CurrentPage以及NavigationStack和ModalStack集合的值之外,DisplayInfo方法还启用和禁用页面上的四个Button元素,以便按下启用的按钮始终是合法的。
这是DisplayInfo以及它用于显示堆栈集合的两种方法:
public partial class SinglePageNavigationPage : ContentPage
{
__
public void DisplayInfo()
{
// Get the NavigationPage and display its CurrentPage property.
NavigationPage navPage = (NavigationPage)App.Current.MainPage;
currentPageLabel.Text = String.Format("NavigationPage.CurrentPage = {0}",
navPage.CurrentPage);
// Get the navigation stacks from the NavigationPage.
IReadOnlyList<Page> navStack = navPage.Navigation.NavigationStack;
IReadOnlyList<Page> modStack = navPage.Navigation.ModalStack;
// Display the counts and contents of these stacks.
int modelessCount = navStack.Count;
int modalCount = modStack.Count;
modelessStackLabel.Text = String.Format("NavigationStack has {0} page{1}{2}",
modelessCount,
modelessCount == 1 ? "" : "s",
ShowStack(navStack));
modalStackLabel.Text = String.Format("ModalStack has {0} page{1}{2}",
modalCount,
modalCount == 1 ? "" : "s",
ShowStack(modStack));
// Enable and disable buttons based on the counts.
bool noModals = modalCount == 0 || (modalCount == 1 && modStack[0] is NavigationPage);
modelessGoToButton.IsEnabled = noModals;
modelessBackButton.IsEnabled = modelessCount > 1 && noModals;
modalBackButton.IsEnabled = !noModals;
}
string ShowStack(IReadOnlyList<Page> pageStack)
{
if (pageStack.Count == 0)
return "";
StringBuilder builder = new StringBuilder();
foreach (Page page in pageStack)
{
builder.Append(builder.Length == 0 ? " (" : ", ");
builder.Append(StripNamespace(page));
}
builder.Append(")");
return builder.ToString();
}
string StripNamespace(Page page)
{
string pageString = page.ToString();
if (pageString.Contains("."))
pageString = pageString.Substring(pageString.LastIndexOf('.') + 1);
return pageString;
}
}
当您看到程序显示的某些屏幕时,一些涉及启用和禁用Button元素的逻辑将变得明显。 您可以随时注释掉启用和禁用代码,以便了解按下无效按钮时会发生什么。
一般规则如下:
- 无模式页面可以导航到另一个无模式页面或模态页面。
- 模态页面只能导航到另一个模态页面。
首次运行程序时,您将看到以下内容。 XAML在顶部包含Title属性的显示,因此它在所有页面上都可见:
页面底部的三个Label元素显示NavigationPage对象的CurrentPage属性以及NavigationStack和ModalStack,它们都是从NavigationPage的Navigation属性获得的。
在所有三个平台上,NavigationStack包含一个项目,即主页。 但是,ModalStack的内容因平台而异。 在Android和Windows运行时平台上,模态堆栈包含一个项目(NavigationPage对象),但模式堆栈在iOS上为空。
这就是为什么DisplayInfo方法将noModals布尔变量设置为true,如果模态堆栈的计数为零或者它包含一个项目但该项目是NavigationPage:
bool noModals = modalCount == 0 || (modalCount == 1 && modStack[0] is NavigationPage);
请注意,CurrentPage属性和NavigationStack中的项不是NavigationPage的实例,而是SinglePageNavigationPage的实例,它派生自ContentPage。 SinglePageNavigationPage定义其ToString方法以显示页面标题。
现在按五次Go to Modeless Page按钮,这就是你会看到的内容。 除了iOS屏幕上的模态堆栈外,屏幕也是一致的:
只要按一次“转到无模式页面”按钮,就会启用“从无模式页面返回”按钮。 逻辑是这样的:
modelessBackButton.IsEnabled = modelessCount > 1 && noModals;
简单来说,如果无模式堆栈(原始页面和当前页面)中至少有两个项目,并且当前页面不是模态页面,则应启用“从无模式页面返回”按钮。
如果此时按下“从无模式页面返回”按钮,您将看到NavigationStack缩小,直到您返回到第0页。自始至终,CurrentPage属性继续指示NavigationStack中的最后一项。
如果您再次按“转到无模式页面”,您将看到更多项目添加到NavigationStack,页面编号不断增加,因为正在实例化新的SinglePageNavigationPage对象。
相反,请尝试按“转到模态页面”按钮:
现在ModalStack包含新页面,但CurrentPage仍然引用最后一个无模式页面。 iOS模式堆栈仍然缺少其他平台中存在的初始NavigationPage对象。
如果然后按“从模态页面返回”,则模态堆栈将正确恢复到其初始状态。
多页应用程序通常会在挂起或终止时尝试保存导航堆栈,然后在再次启动时恢复该堆栈。 在本章的最后,您将看到使用NavigationStack和ModalStack来完成这项工作的代码。