| 
 | 
	
 
 
  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 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 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; 
///  
/// 如果传递的对象是复杂类型,那么使用本方法来导航页面 
///  
///  
///  
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)frameState[this._pageKey]); 
} 
else 
// 通过将相同策略用于加载挂起状态并从缓存重新创建 
// 放弃的页,将导航参数和保留页状态传递 
// 给页 
this.LoadState(e.Parameter, (Dictionary)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,也就是说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)]这个来标记。具体看我源码 
   
  好了,到这里对于数据的保存方面的内容告一段落。 
   
  点击源码下载 
   
   |   
 
 
 
 | 
  
 |