前一段开发UWP应用的时候因为系统返回按钮事件(SystemNavigationManager.GetForCurrentView().BackRequested)浪费了不少时间。现象就是在手机版的详细页面跳转到其他应用,然后再返回应用,点击系统的返回按钮时应用关闭而不是返回主页面,如果应用不跳转就没有问题。最后调查发现是注销系统返回按钮事件(SystemNavigationManager.GetForCurrentView().BackRequested)的位置放错了,特在此给各位分享一下。
备注:之所以在各个ViewModel注册注销BackRequested事件,是由于各个页面在返回时处理需求不一样(弹确认对话框等等)。
起因
我们都知道Page中如下方法可以重载:
public sealed partial class SecondPage : Page { public SecondPage() { this.InitializeComponent(); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { base.OnNavigatingFrom(e); } protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); } protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); } }
他们的执行顺序如下:
- 在此页将要在Frame中显示时:OnNavigatedTo
- 当此页即将不再是Frame中的活动页面时:OnNavigatingFrom
- 当此页不再在Frame中显示时:OnNavigatedFrom
根据NavigationHelper的做法在MVVMLight里面我们也会做一次封装让Viewmodel可以调用:
#region 进程生命期管理 private String _pageKey; /// <summary> /// 在当前页上注册此事件以向该页填入 /// 在导航过程中传递的内容以及任何 /// 在从以前的会话重新创建页时提供的已保存状态。 /// </summary> public event LoadStateEventHandler LoadState; /// <summary> /// 在当前页上注册此事件以保留 /// 与当前页关联的状态,以防 /// 应用程序挂起或从导航缓存中丢弃 /// 该页。 /// </summary> public event SaveStateEventHandler SaveState; /// <summary> /// 在此页将要在 Frame 中显示时进行调用。 /// 此方法调用 <see cref="LoadState"/>,应在此处放置所有 /// 导航和进程生命周期管理逻辑。 /// </summary> /// <param name="e">描述如何访问此页的事件数据。Parameter /// 属性提供要显示的组。</param> protected override void OnNavigatedTo(NavigationEventArgs e) { var frameState = SuspensionManager.SessionStateForFrame(this.Frame); this._pageKey = "Page-" + this.Frame.BackStackDepth; if (e.NavigationMode == NavigationMode.New) { // 在向导航堆栈添加新页时清除向前导航的 // 现有状态 var nextPageKey = this._pageKey; int nextPageIndex = this.Frame.BackStackDepth; while (frameState.Remove(nextPageKey)) { nextPageIndex++; nextPageKey = "Page-" + nextPageIndex; } // 将导航参数传递给新页 if (this.LoadState != null) { this.LoadState(this, new LoadStateEventArgs(e.Parameter, null)); } } else { // 通过将相同策略用于加载挂起状态并从缓存重新创建 // 放弃的页,将导航参数和保留页状态传递 // 给页 if (this.LoadState != null) { this.LoadState(this, new LoadStateEventArgs(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey])); } } } /// <summary> /// 当此页不再在 Frame 中显示时调用。 /// 此方法调用 <see cref="SaveState"/>,应在此处放置所有 /// 导航和进程生命周期管理逻辑。 /// </summary> /// <param name="e">描述如何访问此页的事件数据。Parameter /// 属性提供要显示的组。</param> protected override void OnNavigatedFrom(NavigationEventArgs e) { var frameState = SuspensionManager.SessionStateForFrame(this.Frame); var pageState = new Dictionary<String, Object>(); if (this.SaveState != null) { this.SaveState(this, new SaveStateEventArgs(pageState)); } frameState[_pageKey] = pageState; } #endregion
OnNavigatedFrom中调用SaveState(保存页面状态),OnNavigatedTo中调用LoadState(复原页面状态)。
在ViewModel的LoadState里面可能会写入事件绑定(例:系统返回按钮事件)、通知消息等等,自然的我们将事件、通知消息的注销处理写在SaveState里面。
public void LoadState(object navParameter, Dictionary<string, object> state) { if(state!=null) { WelcomeTitle = state[nameof(WelcomeTitle)].ToString(); } // 注册事件 SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested } public void SaveState(Dictionary<string, object> state) { state[nameof(WelcomeTitle)] = WelcomeTitle; // 注销事件 SystemNavigationManager.GetForCurrentView().BackRequested -= OnBackRequested }
如果应用之间不跳转确实没有问题,但是应用之间跳转就出现问题(事件无效),为什么?
原因
因为跳转的时候触发应用App.xaml.cs的Suspending事件,从而调用OnSuspending处理:
private async void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); // 保存应用状态 await SuspensionManager.SaveAsync(); deferral.Complete(); }
而SuspensionManager.SaveAsync中最后使用frame.GetNavigationState()方法调用OnNavigatedFrom处理(即SaveState),返回的时候驻留在内存的应用直接复原不执行OnNavigatedTo处理(即LoadState)。这样在OnNavigatedFrom处理(即SaveState)中注销的事件和通知消息就得不到复原而导致应用出问题。(特殊情况:如果应用被内存回收掉的话将会执行OnNavigatedTo处理(即LoadState)而没有问题。)
模拟过程
1,启动应用
2,挂起应用(注意:不是挂起并关闭)
到达OnSuspending处理
3,执行frame.GetNavigationState()方法
直接跳转到OnNavigatedFrom处理(即SaveState)
4,复原应用
OnNavigatedTo处理(即LoadState)没有触发
总结
正是由于应用在挂起的时候会出现这个问题所以应该将事件或者通信消息的注销处理写在OnNavigatingFrom里面。Viewmodel可以封装一个SavingState方法让OnNavigatingFrom调用:
public void SavingState() { // 注销事件 SystemNavigationManager.GetForCurrentView().BackRequested -= OnBackRequested }