保存和恢复导航堆栈
许多多页面应用程序的页面体系结构比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;
}
}
测试程序的一种简单方法是逐步导航到几个无模式页面,然后模态页面,在每页上的步进器上设置不同的值。 然后从手机或仿真器终止应用程序(如前所述)并重新启动它。 您应该与您离开的页面位于同一页面上,并在返回页面时看到相同的步进器值。