前言
頁面導航,是App中的基本功,一般的App,一來一去,只需要簡單的Navigate + Back就行了,一個復雜的App可能需要很多導航模式的混合才能實現最佳用戶體驗。
SplashScreen 啟動屏幕
我們先從最開始的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秒而已,目的是讓用戶有一種有人替他干活的虛榮感,而且讓你漂亮的啟動頁面給用戶流下深刻印象。
Basic頁面
除了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