天天看點

第二十四章:頁面導航(三)

探索力學

如您所見,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來完成這項工作的代碼。

繼續閱讀