保存和恢復導航堆棧
許多多頁面應用程序的頁面體系結構比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;
}
}
測試程序的一種簡單方法是逐步導航到幾個無模式頁面,然後模態頁面,在每頁上的步進器上設置不同的值。 然後從手機或仿真器終止應用程序(如前所述)並重新啓動它。 您應該與您離開的頁面位於同一頁面上,並在返回頁面時看到相同的步進器值。