前一段開發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 }