为了存储和加载导航状态,类NavigationSuspensionManager定义了方法SetNaviagationStateAsync和GetNavigationStateAsync。导航的页面堆栈可以在单个字符串中表示。这个字符串写入本地缓存文件中,用一个常数给它命名。如果应用程序以前运行时文件已经存在,就覆盖它。不需要记住应用程序多个运行之间的页面导航:
public class NavigationSuspensionManager { private const string NavigationStateFile = "NavigationState.txt"; public async Task SetNavigationStateAsync(string navigationState) { var file = await ApplicationData.Current.LocalCacheFolder.CreateFileAsync( NavigationStateFile, CreationCollisionOption.ReplaceExisting); Stream output_stream = await file.OpenStreamForWriteAsync(); using (var writer = new StreamWriter(output_stream)) { await writer.WriteLineAsync(navigationState); } } public async Task GetNavigationStateAsync() { var stream = await ApplicationData.Current.LocalCacheFolder.OpenStreamForReadAsync(NavigationStateFile); using (var reader = new StreamReader(stream)) { await reader.ReadLineAsync(); } } }注意:
NavigationSuspensionManager类利用Windows运行库API和.NET的Stream类读写文件的内容。
1. 暂停应用程序
为了在暂停应用程序时保存状态,在OnSuspending事件处理程序中设置App类的Suspending事件。当应用程序进入暂停模式时触发事件:
public App() { this.InitializeComponent(); this.Suspending += OnSuspending; }OnSuspending是一个事件处理程序方法,因此声明为返回void。这有一个问题。只要方法完成,应用程序就可以终止。然而,因为方法声明为void,所以不可能等待方法完成。因此,收到的SuspendingEventArgs参数定义了一个SuspendingDeferral,通过调用GetDeferral方法可以检索它。一旦完成代码的异步功能,需要调用Complete方法来完成延迟。这样,调用者知道方法已经完成,应用程序可以终止:
private void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); //TODO: 保存应用程序状态并停止任何后台活动 deferral.Complete(); }在OnSuspending方法的实现中,页面堆栈写入临时缓存。使用Frame的BackStackDepth属性可以在页面堆栈上检索页面。这个属性返回PageStackEntry对象的列表,其中每个实例代表类型、导航参数和导航过度信息。为了用SetNavigationStateAsync方法存储页面跟踪,只需要一个字符串,其中包含完整的页面堆栈信息。这个字符串可以通过调用Frame的GetNavigationState方法来检索:
private async void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); var frame = Window.Current.Content as Frame; if (frame?.BackStackDepth >= 1) { var suspensionManager = new NavigationSuspensionManager(); string navigateState = frame.GetNavigationState(); if (navigateState != null) { await suspensionManager.SetNavigationStateAsync(navigateState); } } //TODO: 保存应用程序状态并停止任何后台活动 deferral.Complete(); }默认情况下,在应用程序终止前,只暂停几秒钟。但是,可以延长这个时间,以进行网络调用,从服务中检索数据,给服务上传数据,或跟踪位置。为此,只需要在OnSuspending方法内创建一个ExtendedExecutionSession,设置理由,比如ExtendedExecutionReason.SavingData。调用RequestExecutionAsync来请求扩展。只要没有拒绝延长应用程序的执行,就可以继续扩展的任务。
2. 激活暂停的应用程序
GetNavigationState返回的字符串用逗号分隔,列出了页面堆栈的完整信息,包括类型信息和参数。不应该解析字符串,获得其中的不同部分,因为在Windows运行库的更新实现中,这可能会改变。仅仅使用这个字符串恢复状态,用SetNavigationState恢复页面堆栈是可行的。如果字符串格式在未来的版本中有变化,这两个方法也会改变。
在启动应用程序时,为了设置页面堆栈,需要更改OnLauched方法。这个方法在Application基类中重写,在启动应用程序时调用。参数LauchActivatedEventArgs给出了应用程序启动方式的信息。Kind属性返回一个ActivationKind枚举值,通过它可以读取应用程序的启动方式:由用户单击磁贴,启动一个语音命令,或在Windows中启动,例如把它启动为一个共享目标。这个场景需要PreviousExecutionState,它返回一个ApplicationExecutionState枚举值,来提供之前应用程序结束方式的信息。如果应用程序用ClosedByUser值结束,就不需要特殊操作,应用程序应重新开始。然而,如果应用程序之前是被终止的,PreviousExecutionState就包含Terminated值。这个状态可用于将应用程序返回到之前用户退出时的状态。这里,页面堆栈从NavigationSuspensionManager中检所,给方法SetNavigationState传递以前保存的字符串,来设置根框架:
protected async override void OnLaunched(LaunchActivatedEventArgs e) { Frame rootFrame = Window.Current.Content as Frame; // 不要在窗口已包含内容时重复应用程序初始化, // 只需确保窗口处于活动状态 if (rootFrame == null) { // 创建要充当导航上下文的框架,并导航到第一页 rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) { //TODO: 从之前挂起的应用程序加载状态 var suspensionManager = new NavigationSuspensionManager(); string navigationState = await suspensionManager.GetNavigationStateAsync(); rootFrame.SetNavigationState(navigationState); } // 将框架放在当前窗口中 Window.Current.Content = rootFrame; } if (e.PrelaunchActivated == false) { if (rootFrame.Content == null) { // 当导航堆栈尚未还原时,导航到第一页, // 并通过将所需信息作为导航参数传入来配置 // 参数 rootFrame.Navigate(typeof(MainPage), e.Arguments); } // 确保当前窗口处于活动状态 Window.Current.Activate(); } }为了测试再次打开应用程序,是否会加载关闭前打开的页面,可以使用 ApplicationExecutionState.ClosedByUser :
protected async override void OnLaunched(LaunchActivatedEventArgs e) { Frame rootFrame = Window.Current.Content as Frame; // 不要在窗口已包含内容时重复应用程序初始化, // 只需确保窗口处于活动状态 if (rootFrame == null) { // 创建要充当导航上下文的框架,并导航到第一页 rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; if (e.PreviousExecutionState == ApplicationExecutionState.ClosedByUser) { //TODO: 从之前挂起的应用程序加载状态 var suspensionManager = new NavigationSuspensionManager(); string navigationState = await suspensionManager.GetNavigationStateAsync(); rootFrame.SetNavigationState(navigationState); } // 将框架放在当前窗口中 Window.Current.Content = rootFrame; } if (e.PrelaunchActivated == false) { if (rootFrame.Content == null) { // 当导航堆栈尚未还原时,导航到第一页, // 并通过将所需信息作为导航参数传入来配置 // 参数 rootFrame.Navigate(typeof(MainPage), e.Arguments); } // 确保当前窗口处于活动状态 Window.Current.Activate(); } }3. 测试暂停
现在启动该应用程序,导航到另一个页面,然后打开另一个应用程序,并等待前一个应用程序终止。如果将Status Values选项设置为"Show Suspended Status",则可以在任务管理器的Details视图中看到暂停的应用程序。但是,在测试暂停时,这不是一个简单的方法(因为应用程序可能在很久之后才暂停),但可以调试不同的状态。
使用调试器则不同。如果应用程序一旦失去焦点就会暂停,那么每到达一个断点就会暂停,因此在调试器中运行时,暂停是被禁用的,正常的暂停机制不会起作用。但是,模拟暂停很容易。打开Debug Location工具栏,可以看到3个按钮:Suspend、Resume和Suspend and shutdown。如果选择Suspend and shutdown,然后再次启动应用程序,那么应用程序将从前一个状态ApplicationExecutionState.Terminated继续运行,因此会打开用户之前打开的页面。
4. 页面状态
用户输入的任何数据也应该恢复。为了进行演示,在Page1上创建两个输入字段:
<StackPanel Spacing="5"> <TextBlock Text="Page 1" Style="{StaticResource HeaderTextBlockStyle}"/> <TextBlock Text="{x:Bind ReceivedContent,Mode=OneTime}" Style="{StaticResource BodyTextBlockStyle}"/> <TextBox Text="{x:Bind Parameter,Mode=TwoWay}"/> <Button Content="Navigate to Page 2" Click="{x:Bind GoToPage2,Mode=OneTime}"/> <TextBox Header="Session State 1" Text="{x:Bind Data.Session1,Mode=TwoWay}"/> <TextBox Header="Session State 2" Text="{x:Bind Data.Session2,Mode=TwoWay}"/> </StackPanel>这个输入字段的数据表示由DataManager类定义,从Data属性中返回,如下面的代码片段所示:
public DataManager Data { get; } = DataManager.Instance;DataManager类定义了属性Session1和Session2,其值存储在Dictionary中:
public class DataManager : INotifyPropertyChanged { private const string SessionStateFile = "TempSessionState.json"; private Dictionary<string, string> _state = new Dictionary<string, string> { [nameof(Session1)] = string.Empty, [nameof(Session2)] = string.Empty }; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler PropertyChanged; public static DataManager Instance { get; } = new DataManager(); public string Session1 { get => _state[nameof(Session1)]; set { _state[nameof(Session1)] = value; OnPropertyChanged(); } } public string Session2 { get => _state[nameof(Session2)]; set { _state[nameof(Session2)] = value; OnPropertyChanged(); } } }为了加载和存储会话状态,定义了SaveTempSessionAsync和LoadTemSessionAsync方法。其实现代码使用Json.Net将字典序列化为JSON格式。但是,可以使用任何序列化:
public async Task SaveTemSessionAsync() { var file = await ApplicationData.Current.LocalCacheFolder.CreateFileAsync( SessionStateFile, CreationCollisionOption.ReplaceExisting); var output_Stream = await file.OpenStreamForWriteAsync(); var serializer = new JsonSerializer(); using (var writer = new StreamWriter(output_Stream)) { serializer.Serialize(writer, _state); } } public async Task LoadTemSessionAsync() { var intput_Stream = await ApplicationData.Current.LocalCacheFolder.OpenStreamForReadAsync(SessionStateFile); var serizlizer = new JsonSerializer(); using (var reader = new StreamReader(intput_Stream)) { string json = await reader.ReadLineAsync(); Dictionary<string, string> state = JsonConvert.DeserializeObject<Dictionary<string, string>>(json); _state = state; //foreach (var item in state) //{ // OnPropertyChanged(item.Key); //} } }剩下的就是调用SaveTempSessionAsync和LoadTempSessionAsync方法,暂停、激活应用程序。这些方法添加到OnSuspending和OnLaunched方法中读写页面堆栈的地方:
private async void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); var frame = Window.Current.Content as Frame; if (frame?.BackStackDepth >= 1) { var suspensionManager = new NavigationSuspensionManager(); string navigateState = frame.GetNavigationState(); if (navigateState != null) { await suspensionManager.SetNavigationStateAsync(navigateState); } } await DataManager.Instance.SaveTemSessionAsync(); //TODO: 保存应用程序状态并停止任何后台活动 deferral.Complete(); }protected async override void OnLaunched(LaunchActivatedEventArgs e) { Frame rootFrame = Window.Current.Content as Frame; // 不要在窗口已包含内容时重复应用程序初始化, // 只需确保窗口处于活动状态 if (rootFrame == null) { // 创建要充当导航上下文的框架,并导航到第一页 rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; if (e.PreviousExecutionState == ApplicationExecutionState.ClosedByUser) { //TODO: 从之前挂起的应用程序加载状态 var suspensionManager = new NavigationSuspensionManager(); string navigationState = await suspensionManager.GetNavigationStateAsync(); rootFrame.SetNavigationState(navigationState); try { await DataManager.Instance.LoadTemSessionAsync(); } catch (FileNotFoundException ex) { Debug.WriteLine(ex.Message); } } // 将框架放在当前窗口中 Window.Current.Content = rootFrame; } if (e.PrelaunchActivated == false) { if (rootFrame.Content == null) { // 当导航堆栈尚未还原时,导航到第一页, // 并通过将所需信息作为导航参数传入来配置 // 参数 rootFrame.Navigate(typeof(MainPage), e.Arguments); } // 确保当前窗口处于活动状态 Window.Current.Activate(); } }
现在,可以运行应用程序,在Page2中输入状态,暂停和终止应用程序,再次启动它,再次显示状态。
在应用程序的生命周期中,需要为Windows应用程序进行特殊的编程,以考虑电池的耗费。下一节将讨论在应用程序间共享数据,这也可以用于手机平台。