Windows 8 Metro开发疑难杂症——导航

来源:互联网 发布:贪吃蛇算法 编辑:程序博客网 时间:2024/05/19 14:38

Win8的导航的实在是让我有点郁闷,尤其是像我这样原来做过WP7开发的真的一时难以适应。

Win8导航的问题,目前一共有两个:

  1. 如果在不启用页面缓存的时候,每次返回的时候都会刷新页面(在WP7中页面的所有状态自动保存,离开的时候什么样的,返回的时候还是那样),这就导致用户状态需要开发者自己去保存。你可能会说既然这样可以启动页面缓存啊,是啊,你当然可以启用页面缓存,但是一旦启用了页面的缓存,你这个页面同一时间只能有一个实例,这样对于对应不同的参数显示不同的数据的页面来说就很困难了,因为你同一时间只有一个实例。
  2. 导航的时候用户是可以传递object参数的,我刚看到可以传递object参数的时候我还以开心,觉得微软终于把WP7的导航模型改进了下,可是当我调试挂起状态的时候我发现我错了,直接crash。于是我在微软的官网论坛问了下,给我的回答是,如果传递的参数是复杂类型(即使你已经标记了DataContract和DataMember)那么程序挂起的时候依然会crash,那也就是说你必须把你的复杂类型在传递之前序列化成字符串,然后在目标页把字符串反序列化成相应对象。我觉得微软你这么做有意思吗?尼玛你还不如用WP7导航模型啊!

 

下面针对第一个问题进行讨论。首先如果你的页面不需要根据不同的参数显示不同的数据的,那么完全可以启用页面缓存(页面的NavigationCacheMode设置成Enabled或者Required)。如果你的页面需要根据不同的参数显示不同数据,那么作为开发者要做的事情比起WP7的导航模型来说要多了,首先是页面数据的保存,其次是页面状态的保存(如果有滚动条你得保存滚动条的状态,如果有文本框,你得保存文本框中的文本…..)。现在拿VS2012中的自带的模版做个列子。

 

首先从项目模版中选择Grid APP模版,建立项目以后直接运行。进入首页,滑动滚动条,滑倒最后,然后随便选择一项进入该项详细信息页,然后返回,这时你会发现页面回到的初始状态,滚动条也回到了原点。现在解释下为什么会这样。

首先页面没有启用缓存,那么每次返回的时候页面总是会刷新,如何刷新的?其实就是返回的时候系统重新调用InitializeComponent方法,原来所有的数据和状态都不复存在,就像一个全新的页面一样(其实你应该把它当成一个全新的页面)。

如果只能这样的话我们肯定会抓狂,还好微软留了两个方法,LoadState和SaveState,其实这两个方法是项目模版中自定义的方法。

  •  LoadState方法,当该页面第一次加载(注意不是返回)的时候,参数pageState为null。当页面是从前一页返回的时候加载的,那么参数pageState参数包含了你在SaveState方法里保存的数据。这时你就可以恢复页面的数据和状态了。
  •  SaveState方法,当从当前页导航进入其他页的时候,会在当前页调用SaveSate方法,这时候你需要把页面的数据和状态保存到pageState中,供LoadState的时候使用保存的 数据

我们以GroupedItemsPage为例,首先我们需要在SaveState里面添加一些代码:

复制代码
 protected override void SaveState(Dictionary<string, object> pageState)        {            base.SaveState(pageState);            //把DefaultViewModel中的所有数据转移到pageState中            foreach (var item in this.DefaultViewModel)            {                pageState[item.Key] = item.Value;            }            //保存滚动条滚动状态,这里只保存HorizontalOffset            var scroll = itemGridView.GetVisualDescendants<ScrollViewer>().FirstOrDefault();            pageState["HorizontalOffset"] = scroll == null ? 0 : scroll.HorizontalOffset;        }
复制代码

然后在LoadState里面添加如下代码:

复制代码
  protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)        {            //pageState不为null,那么表明是返回状态,这时候需要恢复数据和状态            if (pageState != null)            {                itemGridView.LayoutUpdated += itemGridView_LayoutUpdated;                if (pageState != null)                {                    foreach (var item in pageState)                    {                        this.DefaultViewModel[item.Key] = item.Value;                    }                }            }            else            {                // pageState为null,表明是第一次加载页面,需要初始化数据                var sampleDataGroups = SampleDataSource.GetGroups((String)navigationParameter);                this.DefaultViewModel["Groups"] = sampleDataGroups;            }        }
复制代码

后面添加itemGridView_LayoutUpdated事件的方法:

复制代码
  //页面的滚动条        ScrollViewer scroll;        void itemGridView_LayoutUpdated(object sender, object e)        {            if (scroll == null)                scroll = itemGridView.GetVisualDescendants<ScrollViewer>().FirstOrDefault();            if (scroll != null)            {                if (this.DefaultViewModel.ContainsKey("HorizontalOffset"))                {                    double d = (double)this.DefaultViewModel["HorizontalOffset"];                    scroll.ScrollToHorizontalOffset(d);                    //滚动条的状态恢复不是一下子就恢复的,可能需要多次调用ScrollToHorizontalOffset才能准确恢复                    if (d == scroll.HorizontalOffset)                        itemGridView.LayoutUpdated -= itemGridView_LayoutUpdated;                }                else//如果DefaultViewModel中没有HorizontalOffset数据,那么就不需要恢复滚动条状态了                    itemGridView.LayoutUpdated -= itemGridView_LayoutUpdated;            }        }
复制代码

添加完这些代码就可以直接运行了,这时你会发现,不管是页面数据还是页面状态都能准确的恢复到上一次的状态。

上面代码中涉及到的GetVisualDescendants方法是一个扩展方法,具体代码如下:

复制代码
 public static class VisualTreeExtensions    {        public static IEnumerable<T> GetVisualDescendants<T>(this DependencyObject element) where T : DependencyObject        {            return element.GetVisualDescendants().OfType<T>();        }        /// <summary>        /// 获取某个元素的所有子孙节点        /// </summary>        public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject element)        {            if (element == null)            {                throw new ArgumentNullException("element");            }            return GetVisualDescendantsAndSelfIterator(element).Skip(1);        }        private static IEnumerable<DependencyObject> GetVisualDescendantsAndSelfIterator(DependencyObject element)        {            Queue<DependencyObject> remaining = new Queue<DependencyObject>();            remaining.Enqueue(element);            while (remaining.Count > 0)            {                DependencyObject obj = remaining.Dequeue();                yield return obj;                foreach (DependencyObject child in obj.GetVisualChildren())                {                    remaining.Enqueue(child);                }            }        }        public static IEnumerable<DependencyObject> GetVisualChildren(this DependencyObject element)        {            if (element == null)            {                throw new ArgumentNullException("element");            }            return GetVisualChildrenAndSelfIterator(element).Skip(1);        }        private static IEnumerable<DependencyObject> GetVisualChildrenAndSelfIterator(this DependencyObject element)        {            yield return element;            int count = VisualTreeHelper.GetChildrenCount(element);            for (int i = 0; i < count; i++)            {                yield return VisualTreeHelper.GetChild(element, i);            }        }    }
复制代码

 

////              导航 每次 返回 都需要 重新 加载 的原因。。。。。。。。。。。。。。。。。。。。。。。。。、、、、、、、、、、、、、、、、、、、、、、、


我在这个系列的第一篇就讲到了导航的问题,不过可能有些问题还没讲明白,所以这里再写一篇关于导航的博客。

这里有两个问题:

1.为什么在调用Frame.Navigate方法的时候不能直接传简单类型以外的对象?

2.为什么页面每次back的时候都会初始化页面?

我先来讲下VS的项目模版,在我们建立项目的时候如果选的不是空白项目,VS自动会创建一个包含Common文件夹的项目,而每个页面都继承自LayoutAwarePage,LayoutAwarePage帮我们做了很多必要的处理,看里面的代码你会发现LayoutAwarePage重写了NavigateTo和NavigateFrom方法,而多了两个LoadState和SaveState两个虚方法。这样我们几乎可以忘了NavigateTo和NavigateFrom方法了,直接用LoadState和SaveState方法就行了。这里还得说下为什么LayoutAwarePage重写了两个方法,看LayoutAwarePage的代码我们可以发现,LayoutAwarePage在NavigateTo方法里面做了数据恢复的处理,当处理完了会调用LoadState方法,并把原来保存的数据传给LoadState方法,这里的数据只有当back的时候才会有,因为只有在back的时候才会需要恢复数据。LayoutAwarePage在NavigateFrom方法里面做了数据保存的处理,当从当前页导航到其他页面的时候就会保存你的数据,具体保存什么数据是需要你自己处理的。

然后我们再看下项目模版是如何帮我们恢复好保存数据的.

我们所有经过SaveState方法保存的数据都是SuspensionManager这个类来处理的,当程序启动的时候,会在APP类的OnLaunched方法里面会调用 SuspensionManager.RegisterFrame初始化SuspensionManager,如果程序是从挂起状态恢复的那么还会调用SuspensionManager.RestoreAsync方法来恢复数据。而当程序处于挂起状态的时候,会在APP类的OnSuspending方法里面调用SuspensionManager.SaveAsync来保存数据(数据的保存是以文件形式保存的)。

到这数据的恢复和保存已经讲完了,然后我要将下前面提到的第一个问题,为什么在调用Frame.Navigate方法的时候不能直接传简单类型以外的对象?

问题就出在SuspensionManager.SaveAsync方法里面,因为这个方法会调用Frame.GetNavigationState方法,这时候如果你页面传递的参数是复杂对象,那么直接crash,因为Frame.GetNavigationState无法序列化你的对象,就算你已经为你的类加上[DataContract]和[DataMenber]标记也没用,对于这个问题我也问了微软的相关技术人员,给我的答复是在传递对象的时候把对象先序列化成字符串,然后再在使用的时候反序列化成对象,这不是坑爹的吗!!!

 

下面讲上面提及的第二个问题,为什么页面每次back的时候都会初始化页面?

对于这个问题我是这么认为的(纯属个人观点,有臆测成分,欢迎拍砖)。

我觉得之所以每次back的时候页面都会初始化,很大程度上是从内存的使用率来考虑的。我们从商店下载的APP,很大部分的APP都是以图片来展示内容的,由于平板的屏幕肯定比手机大,那同时显示的图片数量势必会比较多,就算你启用了虚拟化技术还是会有这个问题的,一张图片的体积差不多能顶得上一部小说了,而一个页面同时显示较多的图片势必会占用较多的内存,而一个APP会有很多的页面,如果每一个页面都这样的话那么一个APP所占用的内存将会有多大!现在当从当前页导航到其他页面的时候我认为系统会把原来页面释放掉以节约内存开销,这样你的APP配合数据虚拟化技术在内存的开销上始终会处于一个稳定的状态(我们目前开发的APP内存基本维持在60-80MB之间)。

另外我要说下什么是数据虚拟化技术,数据的虚拟化是列表类控件的专有的。假如你有1000条数据作为数据源给列表控件,如果你没有启用数据虚拟化(默认是开启的),那么列表控件将会加载1000个item,每一个item都会占用内存资源,可想而知你的app要占用掉多少的资源,其实不只是内存的开销,还有就是在你绑定数据源的时候出现卡的现象(因为列表控件要一下子初始化1000个item,不卡才怪!)。幸好目前的gridview,listview,listbox,flipview都默认启用数据虚拟化的,这样你有1000条数据的情况下,系统只要初始化10条或者更少的item(具体初始化多少条系统计算的),这样一个减少了内存开销,还一个是加快了列表控件的初始化速度。

数据的虚拟化其实是依靠列表控件的容器(ItemsPanel)来实现的,这些列表控件默认的容器都是从VirtualizingPanel继承过来的,在win8种WrapGrid和VirtualizingStackPanel都是支持数据虚拟化的容器。我们自己也是可以自定义支持虚拟化的容器的,不过这个过程是很复杂的。

目前我用了一款社交类的APP,我可以确定这个APP没用启用虚拟化技术,它有分页功能,当我一直往下加载页面的时候内存蹭蹭的往上涨,当我大概浏览到100页的时候内存占用已经有1G了,你没看错,是1G。



原创粉丝点击