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

保存和恢復導航堆棧

許多多頁面應用程序的頁面體系結構比DataTransfer6更復雜,您需要一種通用的方法來保存和恢復整個導航堆棧。此外,您可能希望將導航堆棧的保存與系統方式集成,以保存和恢復每個頁面的狀態,特別是如果您不使用MVVM。
在MVVM應用程序中,通常ViewModel負責保存作爲應用程序各個頁面基礎的數據。但是在缺少ViewModel的情況下,該作業將留給每個單獨的頁面,通常涉及Application類實現的Properties字典。但是,您需要注意不要在兩個或多個頁面中包含重複的字典鍵。如果特定頁面類型可能在導航堆棧中具有多個實例,則特別可能存在重複鍵。
如果導航堆棧中的每個頁面都使用其字典鍵的唯一前綴,則可以避免重複字典鍵的問題。例如,主頁可能對其所有字典鍵使用前綴“0”,導航堆棧中的下一頁可能使用前綴“1”,依此類推。
Xamarin.FormsBook.Toolkit庫有一個接口和一個類,它們協同工作以幫助您保存和恢復導航堆棧,並使用唯一的字典鍵前綴保存和恢復頁面狀態。此接口和類不排除在您的應用程序中使用MVVM。
該接口稱爲IPersistentPage,它具有名爲Save and Restore的方法,其中包含字典鍵前綴作爲參數:

namespace Xamarin.FormsBook.Toolkit
{
    public interface IPersistentPage
    {
        void Save(string prefix);
        void Restore(string prefix);
    }
}

應用程序中的任何頁面都可以實現IPersistentPage。在將項添加到“屬性”字典或訪問這些項時,“保存”和“還原”方法負責使用prefix參數。你很快就會看到例子。
這些保存和恢復方法是從名爲MultiPageRestorableApp的類調用的,該類派生自Application,旨在成爲App類的基類。從MultiPageRestorableApp派生App時,您有兩個職責:

  • 從App類的構造函數中,使用應用程序主頁的類型調用MultiPageRestorableApp的Startup方法。
  • 從App類的OnSleep覆蓋調用基類的OnSleep方法。

使用MultiPageRestoreableApp時還有兩個要求:

  • 應用程序中的每個頁面都必須具有無參數構造函數。
  • 從MultiPageRestorableApp派生App時,此基類成爲從應用程序的可移植類庫公開的公共類型。這意味着所有單個平臺項目也需要引用Xamarin.FormsBook.Toolkit庫。

MultiPageRestorableApp通過循環NavigationStack和ModalStack的內容來實現其OnSleep方法。每個頁面都有一個從0開始的唯一索引,每個頁面都縮減爲一個短字符串,其中包括頁面類型,頁面索引和指示頁面是否爲模態的布爾值:

namespace Xamarin.FormsBook.Toolkit
{
    // Derived classes must call Startup(typeof(YourStartPage));
    // Derived classes must call base.OnSleep() in override
    public class MultiPageRestorableApp : Application
    {
        __
        protected override void OnSleep()
        {
            StringBuilder pageStack = new StringBuilder();
            int index = 0;
            // Accumulate the modeless pages in pageStack.
            IReadOnlyList<Page> stack = (MainPage as NavigationPage).Navigation.NavigationStack;
            LoopThroughStack(pageStack, stack, ref index, false);
            // Accumulate the modal pages in pageStack.
            stack = (MainPage as NavigationPage).Navigation.ModalStack;
            LoopThroughStack(pageStack, stack, ref index, true);
            // Save the list of pages.
            Properties["pageStack"] = pageStack.ToString();
        }
        void LoopThroughStack(StringBuilder pageStack, IReadOnlyList<Page> stack, 
                              ref int index, bool isModal)
        {
            foreach (Page page in stack)
            {
                // Skip the NavigationPage that's often at the bottom of the modal stack.
                if (page is NavigationPage)
                    continue;
                pageStack.AppendFormat("{0} {1} {2}", page.GetType().ToString(), 
                                                     index, isModal);
                pageStack.AppendLine();
                if (page is IPersistentPage)
                {
                    string prefix = index.ToString() + ' ';
                    ((IPersistentPage)page).Save(prefix);
                }
                index++;
            }
        }
    }
}

此外,實現IPersistentPage的每個頁面都會調用其Save方法,並將整數前綴轉換爲字符串。
OnSleep方法通過將包含每頁一行的複合字符串保存到具有鍵“pageStack”的Properties字典來結束。
從MultiPageRestorableApp派生的App類必須從其構造函數中調用Startup方法。 Startup方法訪問Properties字典中的“pageStack”條目。 對於每一行,它實例化該類型的頁面。 如果頁面實現IPersistentPage,則調用Restore方法。 通過調用PushAsync或PushModalAsync將每個頁面添加到導航堆棧。 請注意,PushAsync和PushModalAsync的第二個參數設置爲false以禁止平臺可能實現的任何頁面轉換動畫:

namespace Xamarin.FormsBook.Toolkit
{
    // Derived classes must call Startup(typeof(YourStartPage));
    // Derived classes must call base.OnSleep() in override
    public class MultiPageRestorableApp : Application
    {
        protected void Startup(Type startPageType)
        {
            object value;
            if (Properties.TryGetValue("pageStack", out value))
            {
                MainPage = new NavigationPage();
                RestorePageStack((string)value);
            }
            else
            {
                // First time the program is run.
                Assembly assembly = this.GetType().GetTypeInfo().Assembly;
                Page page = (Page)Activator.CreateInstance(startPageType);
                MainPage = new NavigationPage(page);
            }
        }
        async void RestorePageStack(string pageStack)
        {
            Assembly assembly = GetType().GetTypeInfo().Assembly;
            StringReader reader = new StringReader(pageStack);
            string line = null;
            // Each line is a page in the navigation stack.
            while (null != (line = reader.ReadLine()))
            {
                string[] split = line.Split(' ');
                string pageTypeName = split[0];
                string prefix = split[1] + ' ';
                bool isModal = Boolean.Parse(split[2]);
                // Instantiate the page.
                Type pageType = assembly.GetType(pageTypeName);
                Page page = (Page)Activator.CreateInstance(pageType);
                // Call Restore on the page if it's available.
                if (page is IPersistentPage)
                {
                    ((IPersistentPage)page).Restore(prefix);
                }
                if (!isModal)
                {
                    // Navigate to the next modeless page.
                    await MainPage.Navigation.PushAsync(page, false);
                    // HACK: to allow page navigation to complete!
                    if (Device.OS == TargetPlatform.Windows && 
                            Device.Idiom != TargetIdiom.Phone)
                        await Task.Delay(250);
                }
                else
                {
                    // Navigate to the next modal page.
                    await MainPage.Navigation.PushModalAsync(page, false);
                    // HACK: to allow page navigation to complete!
                    if (Device.OS == TargetPlatform.iOS)
                        await Task.Delay(100);
                }
            }
        }
        __
    }
}

此代碼包含兩個以“HACK”開頭的註釋。 這些表示用於解決Xamarin.Forms中遇到的兩個問題的語句:

  • 在iOS上,嵌套模式頁面無法正確還原,除非有一點時間分隔PushModalAsync調用。
  • 在Windows 8.1上,無模式頁面不包含左箭頭後退按鈕,除非有一點時間將調用分爲PushAsync。

我們來試試吧!
StackRestoreDemo程序有三個頁面,名爲DemoMainPage,DemoModelessPage和DemoModalPage,每個頁面都包含一個Stepper並實現IPersistentPage以保存和恢復與該Stepper關聯的Value屬性。 您可以在每個頁面上設置不同的Stepper值,然後檢查它們是否正確恢復。
App類派生自MultiPageRestorableApp。 它從其構造函數調用Startup並從其OnSleep覆蓋調用基類OnSleep方法:

public class App : Xamarin.FormsBook.Toolkit.MultiPageRestorableApp
{
    public App()
    {
        // Must call Startup with type of start page!
       Startup(typeof(DemoMainPage));
    }
    protected override void OnSleep()
    {
        // Must call base implementation!
        base.OnSleep();
    }
}

DemoMainPage的XAML實例化一個Stepper,一個顯示該Stepper值的Label,以及兩個Button元素:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="StackRestoreDemo.DemoMainPage"
             Title="Main Page">
    <StackLayout>
        <Label Text="Main Page"
               FontSize="Large"
               VerticalOptions="CenterAndExpand"
               HorizontalOptions="Center" />
        <Grid VerticalOptions="CenterAndExpand">
            <Stepper x:Name="stepper"
                     Grid.Column="0"
                     VerticalOptions="Center"
                     HorizontalOptions="Center" />
            <Label Grid.Column="1"
                   Text="{Binding Source={x:Reference stepper},
                                  Path=Value,
                                  StringFormat='{0:F0}'}"
                   FontSize="Large"
                   VerticalOptions="Center"
                   HorizontalOptions="Center" />
        </Grid>
        <Button Text="Go to Modeless Page"
                FontSize="Large"
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="Center"
                Clicked="OnGoToModelessPageClicked" />
        <Button Text="Go to Modal Page"
                FontSize="Large"
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="Center"
                Clicked="OnGoToModalPageClicked" />
    </StackLayout>
</ContentPage>

兩個Button元素的事件處理程序導航到DemoModelessPage和DemoModalPage。 IPersistentPage的實現使用Properties字典保存和恢復Stepper元素的Value屬性。 注意在定義字典鍵時使用prefix參數:

public partial class DemoMainPage : ContentPage, IPersistentPage
{
   public DemoMainPage()
   {
       InitializeComponent();
   }
   async void OnGoToModelessPageClicked(object sender, EventArgs args)
   {
       await Navigation.PushAsync(new DemoModelessPage());
   }
   async void OnGoToModalPageClicked(object sender, EventArgs args)
   {
       await Navigation.PushModalAsync(new DemoModalPage());
   }
   public void Save(string prefix)
   {
       App.Current.Properties[prefix + "stepperValue"] = stepper.Value;
   }
   public void Restore(string prefix)
   {
       object value;
       if (App.Current.Properties.TryGetValue(prefix + "stepperValue", out value))
           stepper.Value = (double)value;
   }
}

DemoModelessPage類與DemoMainPage基本相同,除了Title屬性和顯示與Title相同的文本的Label。
DemoModalPage有些不同。 它還有一個Stepper和一個顯示Stepper值的Label,但是一個Button返回上一頁,另一個Button導航到另一個模態頁面:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="StackRestoreDemo.DemoModalPage"
             Title="Modal Page">
    <StackLayout>
        <Label Text="Modal Page"
               FontSize="Large"
               VerticalOptions="CenterAndExpand"
               HorizontalOptions="Center" />
        <Grid VerticalOptions="CenterAndExpand">
            <Stepper x:Name="stepper"
                     Grid.Column="0"
                     VerticalOptions="Center"
                     HorizontalOptions="Center" />
            <Label Grid.Column="1"
                   Text="{Binding Source={x:Reference stepper},
                                  Path=Value,
                                  StringFormat='{0:F0}'}"
                   FontSize="Large"
                   VerticalOptions="Center"
                   HorizontalOptions="Center" />
        </Grid>
        <Button Text="Go Back"
                FontSize="Large"
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="Center"
                Clicked="OnGoBackClicked" />
        <Button x:Name="gotoModalButton"
                Text="Go to Modal Page"
                FontSize="Large"
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="Center"
                Clicked="OnGoToModalPageClicked" />
    </StackLayout>
</ContentPage>

代碼隱藏文件包含這兩個按鈕的處理程序,還實現了IPersistantPage:

public partial class DemoModalPage : ContentPage, IPersistentPage
{
    public DemoModalPage()
    {
        InitializeComponent();
     }
    async void OnGoBackClicked(object sender, EventArgs args)
    {
        await Navigation.PopModalAsync();
    }
    async void OnGoToModalPageClicked(object sender, EventArgs args)
    {
        await Navigation.PushModalAsync(new DemoModalPage());
    }
    public void Save(string prefix)
    {
        App.Current.Properties[prefix + "stepperValue"] = stepper.Value;
    }
    public void Restore(string prefix)
    {
        object value;
        if (App.Current.Properties.TryGetValue(prefix + "stepperValue", out value))
            stepper.Value = (double)value;
    }
}

測試程序的一種簡單方法是逐步導航到幾個無模式頁面,然後模態頁面,在每頁上的步進器上設置不同的值。 然後從手機或仿真器終止應用程序(如前所述)並重新啓動它。 您應該與您離開的頁面位於同一頁面上,並在返回頁面時看到相同的步進器值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章