前言html
以前在 剁手黨也有春天 -- 淘寶 UWP 」比較「功能誕生記 這篇隨筆中介紹了一下 UWP 淘寶的「比較」新功能呱呱墜地的過程。在鮮活的文字背後,其實都是程序員不眠不休的血淚史(有血有淚有史)……因此咱們此次就要在看似好玩的 UWP 多窗口實現背後,挖掘一些咱們也是首次接觸的幹活「新鮮熱辣」地放鬆給你們。但願能使你們在想要將本身的 APP 開新窗口的時候,能從本文中獲得一些啓發,而不是老是發現 C# 關於 UWP 開新窗口可供參考的文章只有 Is it possible to open a new window in UWP apps? 這一篇。程序員
---------我是幹(一聲)活(四聲)的分割線--------sql
多開窗口的實現windows
因爲主窗口各功能趨於穩定,並且很難騰出一塊較大的空間給比較功能,並且若是須要再額外劃分出一塊空間的話,勢必會增長用戶來回切換空間的操做,從時間成本和學習成原本說都是不夠高效的,因此咱們決定利用一下 UWP 的新的功能,新打開一個窗口,這樣能夠在新窗口中完總體驗比較功能。多線程
因此本文最主要的目的,固然就是借咱們的新的比較功能,談一談 UWP 新窗口功能的實現,以及窗口直接信息的傳遞和互動。要實現多窗口操做,首先「你得有一個女友」……不對,是你得有一個新窗口。那麼如何打開新窗口呢? app
UWP 開啓新窗口
UWP 新開啓第二窗口的步驟不算難,異步
CoreApplicationView newView = CoreApplication.CreateNewView();
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { var newWindow = Window.Current; var newAppView = ApplicationView.GetForCurrentView(); frame = new Frame(); frame.Navigate(typeof(ComparisonPage)); newWindow.Content = frame; newWindow.Activate(); newViewId = newAppView.Id; }); viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
簡單幾句就能夠打開一個新窗口,而且在新窗口中切換到事先寫好的「比較頁面」。可是這樣打開的新窗口還比較「粗糙」,很大的概率會出問題,例如打開了更多的窗口。那麼須要咱們一步一步完善:async
1. 樣式問題:sqlserver
新窗口中,窗口的標題欄是 Windows 當前主題的顏色,和主窗口的淘寶主題色很不搭調。怎麼辦?post
加入這麼幾行代碼:
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)