从淘宝 UWP 的新功能 -- 比较页面来谈谈 UWP 的窗口多开功能
前言
之前在 剁手黨也有春天 -- 淘寶 UWP ”比較“功能誕生記?這篇隨筆中介紹了一下 UWP 淘寶的“比較”新功能呱呱墜地的過程。在鮮活的文字背后,其實都是程序員不眠不休的血淚史(有血有淚有史)……所以我們這次就要在看似好玩的 UWP 多窗口實現背后,挖掘一些我們也是首次接觸的干活“新鮮熱辣”地放松給大家。希望能使大家在想要將自己的 APP 開新窗口的時候,能從本文中得到一些啟發,而不是總是發現 C# 關于 UWP 開新窗口可供參考的文章只有 Is it possible to open a new window in UWP apps? 這一篇。
---------我是干(一聲)活(四聲)的分割線--------
多開窗口的實現
由于主窗口各功能趨于穩定,而且很難騰出一塊較大的空間給比較功能,而且如果需要再額外劃分出一塊空間的話,勢必會增加用戶來回切換空間的操作,從時間成本和學習成本來說都是不夠高效的,所以我們決定利用一下 UWP 的新的功能,新打開一個窗口,這樣可以在新窗口中完整體驗比較功能。
所以本文最主要的目的,當然就是借我們的新的比較功能,談一談 UWP 新窗口功能的實現,以及窗口直接信息的傳遞和互動。要實現多窗口操作,首先“你得有一個女朋友”……不對,是你得有一個新窗口。那么如何打開新窗口呢??
UWP 開啟新窗口
UWP 新開啟第二窗口的步驟不算難,
簡單幾句就可以打開一個新窗口,并且在新窗口中切換到事先寫好的“比較頁面”。但是這樣打開的新窗口還比較“粗糙”,很大的幾率會出問題,例如打開了更多的窗口。那么需要我們一步一步完善:
1. 樣式問題:
新窗口中,窗口的標題欄是 Windows 當前主題的顏色,和主窗口的淘寶主題色很不搭調。怎么辦?
加入這么幾行代碼:
newAppView.Title = "商品比較";ApplicationViewTitleBar titleBar = newAppView.TitleBar;titleBar.BackgroundColor = ......; titleBar.ForegroundColor = ......;其中,titleBar 的參數是可以充分進行設定的。這樣我們就可以實現和主窗口一樣的色調,使新窗口看起來不那么“山寨”。
2. 用戶回到主界面,再點擊一次“去比較”按鈕,又會新開好多窗口,這個怎么辦呢?
這個問題其實不難解決,我們注意到,最后打開新窗口的 TryShowAsStandaloneAsync 方法會根據是否打開成功返回一個 bool 值,我們可以根據這個 bool 值進行判斷,如果為 true,說明新窗口已經打開了,那我們只需要執行
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);就可以切換到剛才的窗口了。
3. 要是打開的比較窗口被用戶關閉了怎么辦呢?
的確,要是打開新窗口成功,然后關閉的話,僅僅判斷 TryShowAsStandaloneAsync 方法的返回值是不夠的,很有可能出現跳轉到一個不存在的窗口 id 的情況。所以我們再引入一個 bool 值,叫viewClosed,當 viewClosed 為 true 的時候,說明用戶關閉了新的比較窗口,那么再次點擊“去比較”的時候,我們就不能單純跳轉,而是要再次打開剛才的窗口。首次打開新窗口的時候,為新窗口的 Consolidated 事件觸發方法,這樣就可以在用戶關閉新窗口的時候,將 ViewClosed 置為 true。這樣,我們就可以根據 viewClosed 和 viewShown 來判斷當前窗口的情況。從而做出正確的選擇了。
newAppView.Consolidated += NewAppView_Consolidated; ......}private void NewAppView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args){viewClosed = true; }這樣,整體打開新窗口的較完整代碼結構就變成了:
static bool viewShown = false;static bool viewClosed = false; static int newViewId; static int currentViewId; static Frame frame; private async void AppBarFontButton_ComparisonButtonTapped(object sender, bool e) { CoreApplicationView newView = CoreApplication.CreateNewView(); if (viewShown) { if (viewClosed) { await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId); viewClosed = false; } else { await ApplicationViewSwitcher.SwitchAsync(newViewId); } } else { await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { var newWindow = Window.Current; var newAppView = ApplicationView.GetForCurrentView();newAppView.Consolidated += NewAppView_Consolidated; newAppView.Title = "商品比較"; ApplicationViewTitleBar titleBar = newAppView.TitleBar; // Title bar setting ...... frame = new Frame(); frame.Navigate(typeof(ComparisonPage)); newWindow.Content = frame; newWindow.Activate(); newViewId = newAppView.Id; }); viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId); } } private void NewAppView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args) { viewClosed = true; }
這樣,就基本可以做到在主窗口不管怎樣點擊,或者新窗口不管是不是關閉了,都可以一鍵切換到我們的比較窗口了。下一步,我們的目標就是要將當前的商品傳遞到比較窗口進行展示。
參數與事件的互相傳遞
主窗口向子窗口傳遞參數:
由于主窗口是商品詳情頁面,所以當前頁面已經擁有了導航到此商品的全部導航信息。但是如何可以將這些信息傳遞到子窗口呢?我們注意到,剛才子窗口的頁面的導航方法是:
frame = new Frame();frame.Navigate(typeof(ComparisonPage));newWindow.Content = frame;這種導航方式,使得我們很難訪問被導航頁面的信息,從而難以傳遞信息。那是不是就沒有辦法了么?當然不是,這里提供兩種思路,供不同場景下參考:
方法1:靜態參數
將 ComparisonPage 頁面的商品導航參數對象設置為靜態,這樣就可以通過
ComparisonPage._navArgs = _navArgs;的方法,在主頁面直接賦值。然后可以通過觸發其他靜態方法或者為這個導航參數對象繼承 INotifyPropertyChanged 接口,這樣當被賦值的時候可以觸發事件,使得新窗口在比較欄中打開這個新的商品。由于每次只有一個主窗口,也只有一個頁面可以點擊去比較,所以不太可能出現多個頁面同時向一個靜態參數傳遞信息導致沖突的情況發生。
方法2:強行找到這個被導航到的頁面的對象并賦值
這個方法說起來有點拗口,但其實就是找到 frame 實際導航到的頁面,并對其對象(非靜態)進行賦值。這樣,我們需要用到一個方法叫做 FindVisualChildren,其實現如下:
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject{if (depObj != null) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) { DependencyObject child = VisualTreeHelper.GetChild(depObj, i); if (child != null && child is T) { yield return (T)child; } foreach (T childOfChild in FindVisualChildren<T>(child)) { yield return childOfChild; } } } }通過這個方法,我們可以用
foreach (ComparisonPage cp in FindVisualChildren<ComparisonPage>(frame)){cp._navArgs = _navArgs;}來找到這個頁面的參數。我們還可以用這個方法來調用這個頁面的非靜態方法,這樣也就可以很方便地觸發頁面下的商品跳轉功能了。
子窗口與主窗口交互:
子窗口有兩個機會,十分有幸地向上和主窗口進行交互:
??? 一是在商品未填滿所有比較窗口的時候,我們可以一鍵返回主窗口,繼續挑選商品加入比較。
??? 二是點擊待比較商品的店鋪,會在主窗口跳轉到店鋪。
?
1. 子窗口切換到主窗口
這個問題相對簡單,其實在子窗口就是一句代碼的事:?
private async void SwitchToMasterWindow(object sender, int e){await ApplicationViewSwitcher.SwitchAsync(masterWindowId);}但是問題在于,子窗口怎么知道主窗口的 masterWindowId 呢?所以,還是要靠主窗口在創建子窗口的時候,把自己的 id 無私地告訴子窗口:
var currentView = ApplicationView.GetForCurrentView();currentViewId = currentView.Id; ...frame.Navigate(typeof(ComparisonPage), currentViewId);這樣子窗口就可以一鍵回家吃飯了!
?
2. 子窗口通知主窗口跳轉店鋪
這個問題就比單純窗口切換要難一些了。在試過多次子窗口跳轉主窗口然后跳轉店鋪被報線程錯誤但是解決無果后,我只能祭出笨卻實用的老辦法:事件通知。子窗口點擊店鋪的時候,觸發跳轉店鋪事件,同時參數是店鋪的 id,主頁面創建子頁面的時候,注冊這個事件,一旦觸發,就捕捉事件參數(店鋪 id)進行跳轉。至于注冊這個事件,既可以用剛才提過的靜態參數法,也可以用 FindVisualChildren 這個好用的方法,直接把事件從頁面里抓出來進行注冊:
private void Frame_LayoutUpdated(object sender, object e){foreach (ComparisonPage cp in FindVisualChildren<ComparisonPage>(frame)){cp.GoToShop -= Cp_GoToShop;cp.GoToShop += Cp_GoToShop;}}private async void Cp_GoToShop(object sender, string e){await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>{if (!string.IsNullOrEmpty(e)){Nav.To(DataHelper.DataSource.ShopDS.GetH5ShopIndexUrlByShopId(e));}});}?
3. 未登錄狀態打開比較窗口遇到的問題
這是一個在寫作過程中被報的 Bug,?如果在未登錄狀態下打開比較頁面,那么在點擊“登陸”和“加入購物車”的時候程序會崩潰。“哦!我的天哪!我的老伙計,這確實是我的問題。非常感謝你們能把它提出來。”(央視翻譯腔)。由于我在寫代碼和測試過程中,一直是有賬號登陸的狀態,所以確實忽略了未登錄狀態可能遇到的問題。那么為什么會出現這個問題呢?是因為默認的頁面設計是:如果遇到“收藏”或“購物車”這些需要登錄才能進行的操作時,會調用另外的登陸控件填充屏幕,使用戶登錄。而在新窗口中,受到線程的制約(具體情況下文會講到),在調用另外的控件會出現線程間調用的錯誤。而這些“收藏”或“加入購物車”都是控件級別的事件,難以用頁面級別的 UI 線程處理這個問題;同時為了避免在三個比較窗口都彈出登陸提示框(用戶到底登陸哪個算?),我們決定將登陸事件向上傳,傳到比較頁面的頂層,然后提示用戶是否要登陸?如果登陸,則切換回主窗口進行登陸,否則則暫不登陸。
所以這里的處理方法和剛剛提到的子窗口通知主窗口跳轉店鋪很相似,提示跳轉 -> 跳轉 -> 傳遞事件:
private async void Tdp_UserNotLogin(object sender, string e){bool ret = await ShowDialog(string.Format("親,你還沒有登陸,是否要切換到主窗口登陸?"), "去登陸", "先不登陸");if (ret){await ApplicationViewSwitcher.SwitchAsync(masterWindowId);UserWantstoLogin?.Invoke(this, e);}}然后由主頁面處理登陸事件,這樣可以避免同時打開多個登陸窗口造成混亂的情況。
4. 子窗口隨主窗口關閉
這也是一個在寫作過程中被報的 Bug。那就是,關閉了主窗口,子窗口不會隨之關閉,導致整個進程不結束,只有關閉了子窗口才算是全部關閉完成。這個問題其實不難解決,我們首先獲得主窗口的“View”,然后在這個“View”的 Consolidated 事件上加入關閉程序的指令(靜態方法)即可:
var currentView = ApplicationView.GetForCurrentView();currentView.Consolidated += CurrentView_Consolidated; ......private void CurrentView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args){CoreApplication.Exit();}當然如果你是一個懷舊的人,也可以使用較為老派的(非靜態方法)
Application.Current.Exit();參考:
How to exit or close an UWP app programmatically? (Windows 10)
?
和線程作斗爭,一頭亂麻
相信大家都聽過這個關于多線程的著名笑話:“從前我有一個問題,后來我用多線程去解決這個問題,現在我有了兩問個題”。
這個笑話告訴我們多線程最容易帶來混亂,尤其是 UWP 這些數不清的異步方法,稍微一不注意就會拋出異常。很多細心的讀者應該注意到了,我在之前的很多地方的代碼都用到了:?
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>{......});或
await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>{......});這其實就是在通知 UI 線程進行異步操作(這里用 Lambda 表達式代替了過時的代理方法),在新窗口和老窗口的一些交互的地方,例如老窗口創建新窗口,新窗口展示待比較寶貝頁面,如果不使用線程代理的話,都是會提示出錯導致 App 崩潰的,所以都需要用這個方法來通知 UI 線程進行異步操作。如果要寫成同步的,代碼就要麻煩許多。或者還有剛剛提到的新窗口未登錄狀態需要打開登陸頁面的情況,涉及到線程過于復雜,所以干脆就用事件傳遞到主窗口進行處理。如果詳細展開說的話,僅僅這一段就可以再寫好幾篇博客了。所以我們在這里不再討論過于底層的東西,因為這些和 WPF 都是技術相通的,很多人都寫過關于這個的文章,因此我們不再贅述。如果讀者感興趣的話,不妨讀一下關于 UWP 或 WPF?線程的文章,獲取更深層的知識。如果可以達到這個目的,那么也算是我們拋磚引玉了。
總結
UWP 開新窗口不難,但是要想很好的讓新窗口和主窗口老老實實為你工作,就需要花一點心思和不斷地調教他們了(其實都是程序員的自我調教)。我們不但要注意各個窗口的狀態,知道在什么時候使用跳轉什么時候使用打開窗口,還需要通過各種辦法在窗口之間傳遞信息和事件。但即使我們每一點都測試到了,還是容易受到多線程的拖累或者產生一些意想不到的問題。我只能說,和多窗口打交道的日子,絕對是痛并快樂著。
?
參考:
[UWP]Is it possible to open a new window in UWP apps?
Find all controls in WPF Window by type
How to exit or close an UWP app programmatically? (Windows 10)
總結
以上是生活随笔為你收集整理的从淘宝 UWP 的新功能 -- 比较页面来谈谈 UWP 的窗口多开功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【个人笔记】《知了堂》MySQL中的数据
- 下一篇: 系统单据号生成规则推荐