保存和恢复页面状态
特别是当您开始使用多页面应用程序时,将应用程序的页面视为数据的主要存储库非常有用,而仅仅是作为底层数据的临时可视化和交互式视图。这里的关键词是暂时的。如果您在用户与之交互时保持基础数据是最新的,那么页面可以显示和消失而不必担心。
本系列的最后一个程序是DataTransfer6,它在程序暂停时将AppData(和其他一些信息)的内容保存在应用程序本地存储中 - 因此当程序终止时 - 然后在程序下次启动时检索该数据起来。
除了保存用户辛苦输入的数据外,您可能还希望保存页面导航堆栈的状态。这意味着如果用户在信息页面上输入数据并且程序终止,则下次程序运行时,它将导航到该信息页面并恢复部分输入的数据。
您可能还记得,Application类定义了一个名为Properties的属性,它是一个包含字符串键和对象值的字典。您可以在App类中的OnSleep覆盖之前或期间在“属性”字典中设置项目。然后,下次App构造函数执行时,这些项将可用。
底层平台通过将对象转换为可以将对象保存到文件的形式来序列化“属性”字典中的对象。应用程序员无论是二进制形式还是字符串形式(可能是XML或JSON)都无关紧要。
对于整数或浮点数,对于DateTime值或对于字符串,序列化很简单。在某些平台上,可以将更复杂的类的实例(例如InformationViewModel)直接保存到Properties集合中。但是,这并不适用于所有平台。将类自身序列化为XML或JSON字符串,然后将结果字符串保存在Properties集合中会更安全。随着Xamarin.Forms可移植类库的.NET版本,XML序列化比JSON序列化更容易,这就是DataTransfer6使用的。
执行序列化和反序列化时,您需要注意对象引用。序列化不保持对象相等。让我们看看这可能是一个问题:
DataTransfer5中引入的AppData版本有两个属性:InfoCollection,它是InformationViewModel对象的集合,以及CurrentInfo,它是当前正在编辑的InformationViewModel对象。
该程序依赖于CurrentInfo对象也是InfoCollection中的项目的事实。 CurrentInfo成为info页面的BindingContext,用户以交互方式更改该InformationViewModel实例的属性。但只是因为同一个对象是InfoCollection的一部分,新值才会显示在ListView中。
序列化AppData的InfoCollection和CurrentInfo属性然后反序列化以创建新的AppData会发生什么?
在反序列化版本中,CurrentInfo对象将具有与InfoCollection中的一个项完全相同的属性,但它不是同一个实例。如果恢复程序以允许用户继续编辑信息页面上的项目,则这些编辑中的任何一个都不会反映在ListView集合中的对象中。
通过这种心理准备,现在是时候查看DataTransfer6中的AppData版本了。
public class AppData
{
public AppData()
{
InfoCollection = new ObservableCollection<InformationViewModel>();
CurrentInfoIndex = -1;
}
public ObservableCollection<InformationViewModel> InfoCollection { private set; get; }
[XmlIgnore]
public InformationViewModel CurrentInfo { set; get; }
public int CurrentInfoIndex { set; get; }
public string Serialize()
{
// If the CurrentInfo is valid, set the CurrentInfoIndex.
if (CurrentInfo != null)
{
CurrentInfoIndex = InfoCollection.IndexOf(CurrentInfo);
}
XmlSerializer serializer = new XmlSerializer(typeof(AppData));
using (StringWriter stringWriter = new StringWriter())
{
serializer.Serialize(stringWriter, this);
return stringWriter.GetStringBuilder().ToString();
}
}
public static AppData Deserialize(string strAppData)
{
XmlSerializer serializer = new XmlSerializer(typeof(AppData));
using (StringReader stringReader = new StringReader(strAppData))
{
AppData appData = (AppData)serializer.Deserialize(stringReader);
// If the CurrentInfoIndex is valid, set the CurrentInfo.
if (appData.CurrentInfoIndex != -1)
{
appData.CurrentInfo = appData.InfoCollection[appData.CurrentInfoIndex];
}
return appData;
}
}
}
此版本具有InfoCollection属性和CurrentInfo属性(如前一版本),但它还包含int类型的CurrentInfoIndex属性,并且CurrentInfo属性使用XmlIgnore属性进行标记,这意味着它不会被序列化。
该类还有两个方法,名为Serialize和Deserialize。 Serialize首先将CurrentInfoIndex属性设置为InfoCollection中CurrentInfo的索引。然后,它将类的实例转换为XML字符串并返回该字符串。
反序列化恰恰相反。它是一个带字符串参数的静态方法。假定该字符串是AppData对象的XML表示形式。在将其转换为AppData实例后,该方法基于CurrentInfoIndex属性设置CurrentInfo属性。现在,CurrentInfo再次成为InfoCollection成员之一的相同对象。该方法返回该AppData实例。
从DataTransfer5到DataTransfer6的唯一其他变化是App类。 OnSleep覆盖序列化AppData对象并使用“appData”键将其保存在Properties字典中。但如果用户导航到DataTransfer6InfoPage并且可能正在输入或编辑信息,它还会使用键“isInfoPageActive”保存布尔值。
App构造函数反序列化“appData”属性条目中可用的字符串,或者如果该字典条目不存在,则将AppData属性设置为新实例。如果“isInfoPageActive”条目为true,则它不仅必须将DataTransfer6MainPage实例化为NavigationPage构造函数的参数(像往常一样),还必须导航到DataTransfer6InfoPage:
public class App : Application
{
public App()
{
// Ensure link to Toolkit library.
Xamarin.FormsBook.Toolkit.Toolkit.Init;
// Load previous AppData if it exists.
if (Properties.ContainsKey("appData"))
{
AppData = AppData.Deserialize((string)Properties["appData"]);
}
else
{
AppData = new AppData();
}
// Launch home page.
Page homePage = new DataTransfer6HomePage();
MainPage = new NavigationPage(homePage);
// Possibly navigate to info page.
if (Properties.ContainsKey("isInfoPageActive") &&
(bool)Properties["isInfoPageActive"])
{
homePage.Navigation.PushAsync(new DataTransfer6InfoPage(), false);
}
}
public AppData AppData { private set; get; }
protected override void OnStart()
{
// Handle when your app starts
}
protected override void OnSleep()
{
// Save AppData serialized into string.
Properties["appData"] = AppData.Serialize();
// Save Boolean for info page active.
Properties["isInfoPageActive"] =
MainPage.Navigation.NavigationStack.Last() is DataTransfer6InfoPage;
}
protected override void OnResume()
{
// Handle when your app resumes
}
}
要测试此程序,必须以App类调用其OnSleep方法的方式终止程序。如果您在Visual Studio或Xamarin Studio调试器下运行该程序,请不要从调试器终止该程序。而是在手机上终止应用程序。
也许在手机和手机模拟器上终止程序的最佳方法是首先显示所有当前正在运行的程序:
- 在iOS上,双击“主页”按钮。
- 在Android上,点击(最右侧)MultiTask按钮。
- 在Windows Phone上,按住(最左侧)“后退”按钮。
此操作会导致调用OnSleep方法。然后,您可以终止该程序:
- 在iOS上,向上滑动应用程序。
- 在Android上,将其滑动到一边。
- 在Windows Phone上,将其向下滑动。
在窗口中运行Windows程序时,只需单击“关闭”按钮即可终止程序。在平板电脑模式下,从顶部向下滑动程序。
然后,您可以使用Visual Studio或Xamarin Studio停止调试应用程序(如有必要)。然后再次运行该程序,看它是否“记住”它停止的位置。