APP的掛起狀態我在前面兩篇關於導航的部落格裡面已經有提到,我這麼說吧,目前版本(包括最新的RTM版)都是有一個bug的。下面我會給你示範這個bug。在這之前我先講下這個掛起問題的臨床表現吧。
不知道你們有沒有注意過,就是當你開啟一個APP的時候瀏覽了一會然後切換到其他APP, 過一段時間以後再切換回原來的APP的時候你會發現原來的APP回到首頁了,並不是離開APP的時候那個頁面,這裡有兩個原因會發生這種情況。這種情況在調試裡面叫“掛起並關閉”,怎麼查看APP是否處於這種狀態,很簡單,就是螢幕左邊彈出一列你所有開啟的APP列表,如果有APP的縮圖變成啟動頁表徵圖的時候,那麼說明這個APP處於這種狀態,如果APP的縮圖是你離開APP的時候的頁面的那麼APP處於正常運行狀態。下面我介紹下引起上面提到的問題的原因。
1.APP開發的時候根本就沒有處理掛起狀態
2.APP開發的時候處理了掛起狀態,但是由於系統的一個Bug導致APP在掛起的時候crash,所以當你從掛起狀態恢複的時候由於沒有資料恢複只能從首頁開始
這個導致Crash的API是Frame.GetNavigationState()方法(只有當你導航的時候傳遞的參數是複雜類型的時候才會引發這個bug,這個就是我在前面兩篇部落格中提到的問題),如果你用了VS的項目模版,SuspensionManager這個類裡面的SaveFrameNavigationState這個方法會調用Frame.GetNavigationState()方法,這個方法主要的作用就是儲存Frame的導航狀態,這樣當你從掛起狀態恢複的時候APP才能正確的恢複狀態,也就是你離開APP的時候是哪個頁面回來的時候還會在那個頁面(這個是非常重要的,如果你沒有恢複導航狀態,那麼可以說你的資料就算儲存了也是沒用的,因為APP在恢複的時候根本就沒用到你儲存的資料),恢複導航狀態是調用 Frame.SetNavigationState這個方法。
下面我示範這個bug。
首先使用VS建立一個GridAPP類型的項目。
因為項目模版的三個頁面的傳遞的參數的類型都是字串,所以不會出現這種問題,這裡我們需要做一些改動。先改下GroupedItemsPage裡面的ItemView_ItemClick方法的代碼,原來的代碼是:
void ItemView_ItemClick(object sender, ItemClickEventArgs e) { // 導航至相應的目標頁,並 // 通過將所需資訊作為導航參數傳入來配置新頁 var itemId = ((SampleDataItem)e.ClickedItem).UniqueId; this.Frame.Navigate(typeof(ItemDetailPage), itemId); }
現在我們要改成
void ItemView_ItemClick(object sender, ItemClickEventArgs e) { // 導航至相應的目標頁,並 // 通過將所需資訊作為導航參數傳入來配置新頁 this.Frame.Navigate(typeof(ItemDetailPage), e.ClickedItem); }
就是把原來傳遞ID的現在直接把對象傳遞過去,下面我們還要改下ItemDetailPage裡面LoadState方法的代碼,原來代碼如下:
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState) { // 允許已儲存頁狀態重寫要顯示的初始項 if (pageState != null && pageState.ContainsKey("SelectedItem")) { navigationParameter = pageState["SelectedItem"]; } // TODO: 建立適用於問題域的合適資料模型以替換樣本資料 var item = SampleDataSource.GetItem((String)navigationParameter); this.DefaultViewModel["Group"] = item.Group; this.DefaultViewModel["Items"] = item.Group.Items; this.flipView.SelectedItem = item; }
現在代碼如下:
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState) { // TODO: 建立適用於問題域的合適資料模型以替換樣本資料 var item = (SampleDataItem)navigationParameter; this.DefaultViewModel["Group"] = item.Group; this.DefaultViewModel["Items"] = item.Group.Items; this.flipView.SelectedItem = item; }
現在可以直接運行了,運行後我們點擊一個項進入詳情頁面。下面就開始調試掛起狀態。
在調試的時候在VS的工具列點擊滑鼠右鍵會出來一個toolbar列表,這裡面把調試位置這個toolbar選上(預設是未選擇狀態),
這時候來調試掛起狀態,點擊“掛起並關閉”,
這時候就出問題了,APP直接Crash
因為SaveAsync這個方法調用了前面我提到的Frame.GetNavigationState方法導致的Crash,各位可以自己斷點設定過去看看。由於Frame.GetNavigationState這個bug存在,可以這麼說,你開發的APP幾乎是沒法正真的實現資料儲存和恢複的。而事實上目前商店中的很多APP都有這樣的情況,國外的不說,我只說國內的,國內很多的APP基本上都有這樣的情況(包括我目前開發的一款APP),只要APP進入掛起狀態,那麼你重新切換回來的時候就是從首頁開始的。這裡要說下,APP何時會進入掛起狀態,這個是系統來決定的,如果記憶體不夠了那麼除了當前啟動並執行APP,其他的APP肯定會進入掛起狀態。
那麼這個問題有沒有解決方案呢?答案是有的,但是不完美,如何不完美我後面會提到,我下面先說下如何解決這個問題。
既然我們的參數不能傳遞複雜類型,那麼只能傳遞簡單類型或者沒有參數傳遞。而我目前提供的方法就是“不傳遞參數”,這裡說的“不傳遞參數”並不是真的就不傳了,只是我們需要換一種傳遞參數的方法,也就是我們在使用Frame.Navigate方法的時候不會傳遞參數了,只能自己寫一個方法來完成傳遞參數的目的。
當我們使用VS內建的模版建立項目的時候,都會有一個Common檔案夾的,裡面有一個LayoutAwarePage類,這個類也是我們建立頁面的基類,我們需要對這個類進行改動下以便達到我們的目的。首先我們需要在LayoutAwarePage這個類裡面添加兩個方法,代碼如下:
private static object nextPageParam; /// <summary> /// 如果傳遞的對象是複雜類型,那麼使用本方法來導航頁面 /// </summary> /// <param name="pagetype"></param> /// <param name="obj"></param> public void Navigate(Type pagetype, object obj) { nextPageParam = obj; this.Frame.Navigate(pagetype); } public void Navigate(Type pagetype) { this.Frame.Navigate(pagetype); }
下面還要對裡面的OnNavigatedTo方法中的代碼進行改動,以便我們能正確的傳遞參數,並且能儲存我們傳遞的參數,這樣頁面恢複的時候還能使用原來的參數。代碼如下:
protected override void OnNavigatedTo(NavigationEventArgs e) { // 通過導航返回快取頁面不應觸發狀態載入 if (this._pageKey != null) return; 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; } //如果nextPageParam不為空白,那麼我們需要儲存這個參數以便恢複的時候能正常恢複 if (nextPageParam != null) { string key = this._pageKey + "_NextPageParam"; frameState[key] = nextPageParam; this.LoadState(nextPageParam, null); nextPageParam = null; } else // 將導航參數傳遞給新頁 this.LoadState(e.Parameter, null); } else { string key = this._pageKey + "_NextPageParam"; if (frameState.ContainsKey(key)) { this.LoadState(frameState[key], (Dictionary<String, Object>)frameState[this._pageKey]); } else // 通過將相同策略用於載入掛起狀態並從緩衝重新建立 // 放棄的頁,將導航參數和保留頁狀態傳遞 // 給頁 this.LoadState(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]); } }
只要用上面這段代碼替換原來的代碼就可以了。下面我們得修改下調用的方法,還是修改GroupedItemsPage裡面的ItemView_ItemClick方法,把原來的 this.Frame.Navigate(typeof(ItemDetailPage), e.ClickedItem);改成現在的 this.Navigate(typeof(ItemDetailPage), e.ClickedItem);因為我們在基類裡面添加了Navigate方法,所以我們在使用的時候可以直接使用this.Navigate來導航,現在試著運行APP,你會發現還是Crash,但是Crash的原因不同了,這次的Crash報的錯誤資訊是無法序列化對象SampleDataItem。為什麼無法序列化SampleDataItem對象呢?因為SuspensionManager在儲存資料的時候是使用DataContractSerializer來把一個字典集合序列化儲存到檔案中的,而這個字典的類型是Dictionary<string, object>,也就是說SuspensionManager在序列化字典的時候根本不知道這個字典儲存的類型是什麼類型,這時候就需要手動添加KnownTypes了,也就是我們要把所有儲存到字典中的類型添加到KnownTypes集合中,這樣SuspensionManager在序列化的時候就能正確序列化集合了,這裡我選擇在APP.cs中添加,在APP的OnLaunched方法裡面添加,SuspensionManager.KnownTypes.Add(typeof(Data.SampleDataItem));把這段代碼加進去就行了。
SuspensionManager.RegisterFrame(rootFrame, "AppFrame"); SuspensionManager.KnownTypes.Add(typeof(Data.SampleDataItem)); if (args.PreviousExecutionState == ApplicationExecutionState.Terminated) { // 僅當合適時才還原儲存的工作階段狀態 try { await SuspensionManager.RestoreAsync(); } catch (SuspensionManagerException) { //還原狀態時出現問題。 //假定沒有狀態並繼續 } }
到這裡還沒完,因為能被序列化的只有是被標記了[DataContract]的類才能被序列化(包括所有的父類),到這當然還沒完,既然標記了[DataContract]那麼肯定是要對屬性做標記的,不然沒有被標記的屬性是不會被序列化的。對於做過WCF的肯定會很熟悉如何標記了。標記完了現在就可以直接運行,你會發現現在可以正常掛起了。並且離開的時候是哪個頁面,回來的時候還是在那個頁面。
其實這裡面的標記有點複雜,因為SampleDataGroup和SampleDataItem涉及到循環參考,所以直接用[DataContract]標記是沒用的,必須使用 [DataContract(IsReference = true)]這個來標記。具體看我源碼
好了,到這裡對於資料的儲存方面的內容告一段落。
點擊源碼下載