第二十四章:页面导航(十六)

保存和恢复导航堆栈

许多多页面应用程序的页面体系结构比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;
    }
}

测试程序的一种简单方法是逐步导航到几个无模式页面,然后模态页面,在每页上的步进器上设置不同的值。 然后从手机或仿真器终止应用程序(如前所述)并重新启动它。 您应该与您离开的页面位于同一页面上,并在返回页面时看到相同的步进器值。

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