页面导航,是App中的基本功,一般的App,一来一去,只需要简单的Navigate + Back就行了,一个复杂的App可能需要很多导航模式的混合才能实现最佳用户体验。
我们先从最开始的SplashScreen说起吧。如果你把启动屏幕做成一个Page,启动时先显示一下,然后假装忙乎两秒,跳到下一个主页面开始进入正题,这个好像看上去也很美好。但是当用户玩命儿按Back键时,哦,露出马脚了,启动页面被唤出了。不过这个bug倒是不妨作为一个新奇的体验。
MSDN里有一节专门讲了如何添加一个SplashScreen,但是说实话,我试了两次,都没成功,人太笨!
算了,自己想办法吧!如果在MainPage里加一个开关会不会简单一些呢?于是这样改装MainPage.xaml:
<Page x:Name="page"
… > <Grid> <Grid x:Name="grid_Splash"> <Image Height="100" Source="ms-appx:///Assets/Logo.100.White.png" /> </Grid> <Grid x:Name="grid_Main" Visibility="Collapsed">
……content here…… </Grid> </Grid> </Page>
这里把不重要的代码都删除了,只看干货:<Grid x:Name=”grid_Splash”>,这一项定义了一个Grid, 盖在了主要内容的前面,因为下面的<Grid x:Name=grid_Main Visibility=”Collapsed”>在初始状态被搞成隐藏了,如此一来,grid_Splash中的Image就会在应用启动后,首先映入眼帘。
什么时候把它从用户眼中抠走呢?有两种方法可供选择。
1)在MainPage的构造函数里开始装载你的data,比如是从远程,考虑到网络状况,可能需要几秒钟。那么你就在Splash里放一个ProgressRing,让它转啊转,转啊转……差不多等用户烦了,你的远程数据也拿回来了,然后写一句:
this.grid_Splash.Visibility = Windows.UI.Xaml.Visibility.Collapsed; this.grid_Main.Visibility = Windows.UI.Xaml.Visibility.Visible;
如此一来Splash被隐藏,你的主要内容在数据来到后也化妆完毕(绑定好了),可以出来见公婆了。
2)如果你不需要从远程调用数据,而是从本地取数据,那么上面的过程就会一闪而过,晃瞎用户的K金G眼,体验很糟糕。这时你可以采用第二种办法:在启动应用时启动一个2秒的计时器:
ThreadPoolTimer.CreateTimer(SplashTimeOut, new TimeSpan(0, 0, 2));
其中定义了回调函数(应该叫做delegate),当两秒时间到时,在函数SplashTimeOut里面:
void SplashTimeOut(......) { this.grid_Splash.Visibility = Windows.UI.Xaml.Visibility.Collapsed; this.grid_Main.Visibility = Windows.UI.Xaml.Visibility.Visible; }
这样会和第一种方法的体验一样,只不过是假装忙活了一下,干等2秒而已,目的是让用户有一种有人替他干活的虚荣感,而且让你漂亮的启动页面给用户流下深刻印象。
除了MainPage以外,如果你还有其它二级页面需要添加的话,请在VS中选择这里:
你如果偏爱Blank Page我也不拦着你,但是Basic Page这个选择能帮你省老鼻子事儿了,它在你的项目中自动添加了这些帮助文件:
其中,NavigationHelper.cs里面实现了在页面上处理Back按键的事件,即,当用户在底层页面按Back键时,会回到上级页面。但是在主页面中不建议处理Back键,而是让系统自动处理,把App放到后台。
基本页面导航MSDN里有,和Silverlight有很大不同。记得SL理面有个什么NavigationService,听上去蛮不错的,到了WinRT里,一律用这个:
this.Frame.Navigate(typeof(NewsReadingPage), obj);
如果在Control里面做页面导航,比如按了个自定义Control,而上层代码又不太容易能得到具体是按了哪个Control,就只好在Control里触发导航动作了,尽管我们不建议这样做:
Frame frame = Window.Current.Content as Frame; frame.Navigate(typeof(....),....);
这里的Frame很模糊,看上去像个全局的,但是在前面那个例子里,又是用this.Frame,就是说在Page对象里还有个Frame。有个文档专门说这事儿,但是绕来绕去的我没看懂,人太笨!如果有搞明白了这事儿的园友们可以给大家一个说明,谢谢先!
基本页面导航中的obj,就可以当作参数传递给下级页面。但是有时候一些下级页面需要返回一些信息回来给调用者,怎么做呢?因为在页面的GoBack()方法中没有参数。有三种方法可以解决这个问题:
1)利用好那个obj,把它即作为[in],又作为[out],下级页面给obj里面的字段赋值,上级页面通过解析obj里的约定字段来得到参数。
2)在下级页面中用static字段,返回之前给它赋值,然后上级页面可以使用。
3)弄个外部类,比如一个静态类,或者一个单例的类,弄个变量在里面当参数。注意用完之后带上手套把指纹擦掉,把该变量“归零”就可以了,隐藏你的作案痕迹,避免下次使用时搞不清当前状态。
z博客园UAP里没有这个例子,用我们做的另一个App--豆瓣一刻来举例说明吧(顺便做一广告,豆瓣一刻 for WP已经上线了,名字叫做“一刻”,link is here:一刻)。
咱们看图说话(注意,以下逻辑很绕,没有耐心的可能看不懂):
按正常逻辑,阅读文章时,可以查看评论;如果想发表评论,点击下方按钮,进入“写评论”页面。但是此时用户可能还没有登录,不能匿名发表评论,所以需要自动跳到登录页面,登录成功后,自动进入写评论页面。
每当PM说起“自动”这个词时,我就头大!什么所谓自动,都是我们程序员手动搞出来的!有时候自动能实现,有时候实现不了,这个要和PM讲清楚。
针对这个具体例子,有几种方式备选,我们先看第一种:
1)看评论页->点击发表评论->发现没登录->进入登录页->登录成功->进入发表评论页
这个流程是最朴素的想法了,但是先别动手,仔细想想:在用户提交评论后,页面该回到哪里?从目前的情况看,是回到登录成功的页面了,会让用户感到困惑。而且,在点击发表评论按钮后,还需要做一个分支判断:如果用户登录了,直接到写评论页;如果用户没登录,要进入登录页面。
看看第二种:
2)看评论页->点击发表评论->进入写评论页->发现没登录->进入登录页->登录成功->“自动”返回写评论
这个看上去好一些,没有第一种方式的两个缺点。用户写完评论后,从stack看,是能直接回到最开始的看评论页的。但是需要解决的问题是如何“自动“返回写评论页?我的刁民小计是:
a) 在CommentWritingPage.Page_Loaded事件中,判断用户是否登录:
private void Page_Loaded(object sender, RoutedEventArgs e) { if (!Settings.Current.IsLogin) { if (this.backFromLogonPage) { } else { this.Frame.Navigate(typeof(SettingsPage2), new DataModel.SettingNavigationParameter() { targetPivot = TargetPivotItemName.LogonAndBack, targetCss = 0 }); } } }
如果登录了,啥也不做,留在当前写评论页面即可;如果没登录,跳到SettingsPage.Logon页面,并且带上一个参数: LogonAndBack。
b) 在SettingsPage.Logon页面,当有登录成功的事件返回后(因为登录是一个异步过程),判断参数是不是LogonAndBack:
void Current_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (Settings.Current.IsLogin && this.logonAndBack) { Settings.Current.PropertyChanged -= Current_PropertyChanged; this.Frame.GoBack(); } }
如果是LogonAndBack,就用this.Frame.GoBack()返回调用者页面,也就是发表评论页面。
如此一来,当用户发表完评论后,自动回阅读评论页面了,行云流水(本来想说飞檐走壁,但是想了想,觉得还没那么离谱)!
需要特别说明的是,在CommentWritingPage中,要在Page_Loaded事件中才能跳转到其它页,如果在OnNavigatedTo()事件中调用this.Frame.Navigate(),nothing happened,没用,什么也不会发生。
让我们来看一个更复杂的例子。下图是我们开发的另一个App,还没有弄完,功能类似个浏览器。
第一张图是在一个WebViewPage里,已经加载了一个页面,点击右上角的窗口管理,进入第二张窗口管理页面(比较丑陋,因为designer休假了,还没完工)。可以看到一共6个窗口,只有第一个窗口被使用了。此时我们点击第二个窗口,想启动一个新窗口来浏览其它网站。按理说应用程序应该把第二个窗口激活,但是窗口中啥都没有,大白板一个,用户体验很差,应该自行惭愧地立刻返回到主控页让用户有更多的选择项(就是目前所显示的第三张蓝色页面)。这个如何做?
一个很自然的想法就是,从窗口管理页Navigate到主控页。不行滴!如此一来,当用户在主控页按Back时,会回到窗口管理页,而窗口管理页只是一个辅助页面,类似弹出式菜单,不应该在stack里存留。主控页是程序的根,按Back时必须要退出应用。
解决办法是在窗口管理页里收到点击事件后,返回到WebViewPage(第一张图):
// 窗口管理页的点击事件
private void lv_ItemClick(object sender, ItemClickEventArgs e) { WebViewHelper wvh = e.ClickedItem as WebViewHelper; this.pool.SetActiveWindow(wvh); if (this.Frame.CanGoBack) { this.pool.BackFromStatusPage = true; this.Frame.GoBack(); } }
在此页面中,判断当前活动窗口是否为空,注意,也是要在Page_Loaded事件中处理:
private void Page_Loaded(object sender, RoutedEventArgs e) { Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed; if (this.pool.BackFromStatusPage) { this.pool.BackFromStatusPage = false; if (this.pool.GetActiveWindow().IsEmptyView) { // back to main page to wait for input if (this.Frame.CanGoBack) { this.Frame.GoBack(); } } else { // stay at webview page to show current web content } } else { this.ctrl_Input.Url = this.wvActive.Url; } }
如果IsEmptyView == true,再调用GoBack返回到主控页面(第三张图),而不是跳转(前进)到主控页面。
这个solution的基本思路,或者说是设计理念,就是窗口控制页面(第二张图)一定是个叶子节点,不能让它作为中间导航节点。
我和一些桥牌的初学者讲过很多次:打每一张牌都要有你的思路,不能说”红桃花色没打过,我试试看“,而是说”从叫牌过程分析,我的同伴有红桃大牌,我要帮助他一下,穿过明手的红桃Q”。写程序做设计也一样,当有多种选择时,一定要首先确定一个设计理念,然后再确定解决方法。
Windows Phone Store App link:
http://www.windowsphone.com/zh-cn/store/app/博客园-uap/500f08f0-5be8-4723-aff9-a397beee52fc
Windows Store App link:
http://apps.microsoft.com/windows/zh-cn/app/c76b99a0-9abd-4a4e-86f0-b29bfcc51059
GitHub open source link:
https://github.com/MS-UAP/cnblogs-UAP
MSDN Sample Code:
https://code.msdn.microsoft.com/CNBlogs-Client-Universal-477943ab
MS-UAP
2015/2