Universal App - Frame、Page、Window 的關係
之前<Windows Phone 7 – Navigation Framework原理概論>,說明了WP 中 Frame 與 Page 之間的關係:
WP 具有一個 PhoneApplicationFrame,內容可切換至多個 PhoneApplicationPage,搭配 Navigation Stack 來管理。
到了 Windows Phone 8.1 之後,針對 Page 與 Frame 的關係變成了什麼樣子呢?
參考<[Universal App] 頁面切換的流程>所介紹的內容,我擷取出了其中的一張圖,如下:
可得知建立一個新的 App,它會擁有一個 Windows 裡面搭配著一個 Frame,Frame 裡可以裝 Page,Page 裡面可在放入
一至多個 Frame,這樣的容圖堆疊正是 XAML 的 DependencyObject 與 VirtualTree 的使用概念。
往下先說明這三個重要元素的關係:
a. Window:
負責呈現 application window 的容器,重要的事件、屬性與方法如下:
類型 | 名稱 | 說明 |
Event | Activated | Occurs when the window has successfully been activated. |
Closed | Occurs when the window has closed. | |
SizeChanged | Occurs when the app window has first rendered or has changed its rendering size. | |
VisibilityChanged | Occurs when the value of the Visible property changes. | |
Method | Activate | Attempts to activate the application window by bringing it to the foreground and setting the input focus to it. |
Close | Closes the application window. | |
Closes the application window. | ||
Property | Bounds | Read-only, Gets the height and width of the application window, as a Rect value. |
Content | Read/write, Gets or sets the visual root of an application window. | |
Current | Read-only, Gets the currently activated window for an application. | |
Dispatcher | Read-only, Gets the CoreDispatcher object for the Window, which is generally the CoreDispatcher for the UI thread. |
Window 本身沒有 xaml 定義。當 App 進入 OnLaunched 要記得呼叫 Activate(),讓 Window 可正常啟動來顯示 UI Thread。
可搭配 SizeChanged event 在 Windows store app 被放入分割的小畫面時,即可以得知進行調整 UI 調整以適應小畫面顯示。
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// Create a Frame to act navigation context and navigate to the first page
var rootFrame = new Frame();
rootFrame.Navigate(typeof(BlankPage));
// Place the frame in the current Window and ensure that it is active
Window.Current.Content = rootFrame;
Window.Current.Activate();
}
b. Frame:
支援 Navigation 的容器,裡面操作的單位為:Page。Frame 支援指定要導向(Navigation)的 Page 與需要傳遞的參數。
Frame 負責維護使用者所操作的 Page 物件(navigation history),可搭配 CurrentSourcePageType 取得現在操作的 Page,
更在每一個 Page 要做導向時,可使用 CanGoBack、CanGoForward 二個屬性了解可導向的方向。
另外,在 IsEnabled 可搭配管理 navigation button。
可以註冊處理這四個事件:Navigating、Navigated、NavigationStopped、NavigationFailed ,負責在導向發生前後可以對於
畫面或是資料做一些處理,或是處理導向等錯誤訊息。
對於在 Page level 的部分,即時處理對應的 OnNavigateTo、OnNavigatingFrom 與 OnNavigatedFrom 事件來完成初始化或
離開 Page 前的資料保存與還原等任務。
重要事件與方法:
類型 | 名稱 | 說明 |
Events | Navigated | Occurs when the content that is being navigated to has been found and is available from the Content property, although it may not have completed loading. |
Navigating | Occurs when a new navigation is requested. | |
NavigationFailed | Occurs when an error is raised while navigating to the requested content. | |
NavigationStopped | Occurs when a new navigation is requested while a current navigation is in progress. | |
Methods | GetNavigationState | Serializes the Frame navigation history into a string. |
GoBack | Navigates to the most recent item in back navigation history, if a Frame manages its own navigation history. | |
GoForward | Navigates to the most recent item in forward navigation history, if a Frame manages its own navigation history. | |
Navigate(TypeName) | Causes the Frame to load content represented by the specified Page. | |
Navigate(TypeName, Object) | Causes the Frame to load content represented by the specified Page, also passing a parameter to be interpreted by the target of the navigation. | |
Navigate(TypeName, Object, NavigationTransitionInfo) | Causes the Frame to load content represented by the specified Page-derived data type, also passing a parameter to be interpreted by the target of the navigation, and a value indicating the animated transition to use.
NavigationTransitionInfo class:Controls how the transition animation runs during the navigation action. |
|
Properties | CacheMode | Read/write, Gets or sets a value that indicates that rendered content should be cached as a composited bitmap when possible. (Inherited from UIElement) |
CacheSize | Read/write, Gets or sets the number of pages in the navigation history that can be cached for the frame. | |
CacheSizeProperty | Read-only, Identifies the CacheSize dependency property. | |
CurrentSourcePageType | Read-only, Gets a type reference for the content that is currently displayed. | |
CurrentSourcePageTypeProperty | Read-only, Identifies the CurrentSourcePageType dependency property. |
[注意]
a. 當 Frame 收到 Navigate,從現有的 Page 導向新的 Page 後,舊的 Page 會被丟棄(Dispose)。
=>例如:Page1 要求導向 Page2,當 Page2 完成 OnNavigatedTo() 後,Page1 會被丟掉,由 Frame 只記下 Navigation Stack;
當 Page2 返回 Page1 時,Frame 從 Navigation Stack 取得目標 Page1 並重新呼叫 Page1建構子,再進入OnNavigatedTo();
b. 預設,每個 Navigate 會針對特定的 Page 建立新的 instance,如果該 Page 曾出現在之前的 Page,那舊的 instance 會被丟掉;
如果該 Page 是頻繁被導向的,可以建立 cache 與 reuse 這個 instance 來提高效能。
「CacheSize」:控制 Frame 管理 Page 實例化的方式。該屬性指定了可緩存多少 Page 數量,被緩存下來的 Page,
將不會再重新實例化就可直接使用。這個屬性值會搭配 Page.NavigationCacheMode 一起使用。
「Page.NavigationCacheMode」:Page 需要設定要求 Cache 才會被記入 CacheSize 之中,其值有二種:Required 跟 Enabled。
Enabled:會被記算至 CacheSize中;Required:則不管 CacheSize 一律 cache;
以 App.xaml.cs 的 OnLaunched 事件時需要初始化 Window 與 Frame 的程式範例來說明:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
// 建立畫面最底層的 Frame,該 Frame 可自行實作 INavigate來改變;
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
rootFrame = new Frame();
// ... other initialization ...
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
if (!rootFrame.Navigate(typeof(MainPage), e.Arguments))
{
throw new Exception("Failed to create initial page");
}
}
// 啟動該 Window
Window.Current.Activate();
}
c. Page:
負責呈現內容,搭配 Frame control 來導向。當 Page 被 Frame.Navigate 執行後,該 Page 會被實例化(建構子先被呼叫),收到 OnNavigatedTo 事件。
Page 是一個 UserControl,因此可以宣告一個 Content 來定義內容物有那些 UIElement。在 Page 二個 Bar 可以定義:TopAppBar 與 BottomAppBar。
類型 | 名稱 | 說明 |
Methods | OnNavigatedFrom | Invoked immediately after the Page is unloaded and is no longer the current source of a parent Frame. |
OnNavigatedTo | Invoked when the Page is loaded and becomes the current source of a parent Frame. | |
OnNavigatingFrom | Invoked immediately before the Page is unloaded and is no longer the current source of a parent Frame. | |
Properties | Dispatcher | Read-only. Gets the CoreDispatcher that this object is associated with. The CoreDispatcher represents a facility that can access the DependencyObject on the UI thread even if the code is initiated by a non-UI thread. (Inherited from DependencyObject) |
NavigationCacheMode | Read/write, Gets or sets the navigation mode that indicates whether this Page is cached, and the period of time that the cache entry should persist. |
提供讓 Page 可以被 Frame 給 cache 起來,搭配 Frame.CacheSize 來使用。二個設定值:
‧Enabled:依賴 Frame.CacheSize 限制而定,如果超過,該 Page 將不會被緩存;
‧Requred:不論 Frame.CacheSize 設定為何,該 Page 都會被緩存並且不計算在 CacheSize 總合裡;
‧Disabled:清除該 Page 被緩存的功能與釋放資源;
如果您想要清掉這個 Page 的緩存,只需要設定 NavigationCacheMode
更多詳細的可參考<Quickstart: Navigating between pages>。
[範例]
1. 實作在 Frame 中從 Page1 執行 Navigate 至 Page2,並且夾帶參數;
a. 在 Page1 實作一個按鈕與輸入框,按下按鈕時會將輸入框的內容夾帶給 Page2;
private void btnGo_Click(object sender, RoutedEventArgs e)
{
Frame.Navigate(typeof(BlankPage2), txtUser.Text);
}
b. 在 Page2 的 OnNavigatedTo 將參數顯示出來;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter != null)
{
MessageDialog dialog = new MessageDialog("BasicPage2",
string.Format("value = {0}, it's from Page1", e.Parameter));
dialog.ShowAsync();
}
}
上述程式碼除了說明如何在 Navigate 時夾帶參數外,也需要注意二個 Page 的生命週期。
當 Page1 要導向 Page2 時,藉由 Frame.Navigate( type, parameters) 的方式,對於二者的生命週期如下:
對於 Page2 的生命週期是:(1) 先進入 Page2 的建構子;(2) Page2 的 OnNavigatedTo;(3) Page1.OnNavigatedFrom;
如下圖說明:
2. 實作註冊 Windows.Phone.UI.Input.HardwareButtons.BackPressed 控制返回功能 (only Windows Phone):
這個部分只有在 Windows Phone 才會需要處理,因為 Windows Store App 沒有實體 Back鍵;
可以選擇在 Page 中註冊 HardwareButtons.BackPressed 事件或是選擇在 App.xaml.cs 中註冊處理,二者的差距在於:
(1) 註冊在 Page,只在在那個 Page 中按下 Back 會有邏輯,在其他 Page 仍會直接離開程式;
(2) 在 App.xaml.cs 註冊 BackPressed,它的層級是最高的,所以按下 BackPressed 時會先觸發 App.xaml.cs
public BlankPage2()
{
this.InitializeComponent();
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
void HardwareButtons_BackPressed(object sender, Windows.Phone.UI.Input.BackPressedEventArgs e)
{
// 註冊 Back 鍵來處理 Frame 中內容 Stack 的切換;
if (Frame.CanGoBack)
{
e.Handled = true;
frameSub.GoBack();
}
}
3. 實作 Page 中加入 Frame,再操作該 Frame 來進行 Navigate();
一個 App 裡有一個 Window.Current.Content 它會擺放一個 rootFrame,做為切換整個當前畫面的主容器;而 Page 是 rootFrame 的 Content;
這個例子讓 Page 中也有一個 Frame 形成一個 sub Frame,這樣一來,就可以讓目前的 Page 為主畫面,利用 sub frame 就可以做不同 Page 的切換;
<Page>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20*" />
<RowDefinition Height="80*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="10">
</StackPanel>
<!-- 在 Page 中加入一個 Frame -->
<Frame Grid.Row="1" x:Name="frameSub" Background="Blue">
</Frame>
</Grid>
</Page>
private void btnGoPage3_Click(object sender, RoutedEventArgs e)
{
// 指定 Page 中的 Frame 導向不同的畫面;
frameSub.Navigate(typeof(SubPage.Page3), "From Page2");
}
private void btnGoPage4_Click(object sender, RoutedEventArgs e)
{
// 指定 Page 中的 Frame 導向不同的畫面;
frameSub.Navigate(typeof(SubPage.Page4), "From Page2");
}
這個範例主要是說明,在 8.1 裡 Frame 可以與 Page 這樣混用,只是在 phone 要額外注意 BackPressed 事件,
如果在 App.xaml.cs 統一註冊的話,那在 Page 裡重新註冊事件,要記得去掉 e.Cancel = true,這樣才能往下傳遞。
4. 測試 Frame.CacheSize 與 Page.NavigationCacheMode 二個的使用情境:
什麼時候會需要做 Frame.CacheSize 與 Page.NavigationCacheMode 呢?舉個例子來說,我做一個書藉的 App,其中有一頁 Page 放書藉的清單(內有圖片與標題),
提供讓用戶選擇書藉往下一頁看書藉的明細,這種 Page 很適合做 Cache,因為用戶會頻繁的來來回回這一個 Page,如果按照 Frame Navigate 的原理,當用戶進入
下一頁,上一頁 Page 是會被釋放等到用戶 Back 時又重新建立,這樣的方式會造成用戶需要花點時間等待,所以加上 Cache 就有更好的體驗。
往下便說明怎麼實作:
a. 建立一個 CategoryPage.xaml,負責載入書藉清單,並在建構式的時候加入 NavigationCacheMode.Enabled;
<!-- 書藉清單用的 ListBox -->
<Grid Grid.Row="1" x:Name="ContentRoot" Margin="19,9.5,19,0">
<StackPanel>
<Image Source="{Binding ImgUrl}" Width="100" Height="100" />
<TextBlock Margin="0,20" Text="{Binding Description}"
FontSize="18" TextWrapping="Wrap" />
</StackPanel>
</Grid>
public CategoryPage()
{
this.InitializeComponent();
this.navigationHelper = new NavigationHelper(this);
this.navigationHelper.LoadState += this.NavigationHelper_LoadState;
this.navigationHelper.SaveState += this.NavigationHelper_SaveState;
// 加上讓這一個 Page 使用 NavigationCacheMode.Enabled;
// 代表如果 Frame 在可限制的 CacheSize 下均可以 Cache 該 Page;
this.NavigationCacheMode = Windows.UI.Xaml.Navigation.NavigationCacheMode.Enabled;
// 載入書藉資料
LoadBookList();
}
private void LoadBookList()
{
List<Book> bookList = new List<Book>();
for (int i = 0; i < 40; i++)
{
// 自訂義 Book 類別來儲存書藉資料;
bookList.Add(new Book
{
BookName = String.Format("WP Book {0}", i),
ImgUrl = new Uri("http://0rz.tw/F3hmz"),
Description = "Following in the footsteps of Charles Petzold's excellent Programming Windows Sixth Edition" +
", O'Reilly is now offering Windows Phone 8 Development Internals ..."
});
}
lstBooks.ItemsSource = bookList;
}
private void lstBooks_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (lstBooks != null && lstBooks.SelectedIndex != -1)
{
// 根據選擇的項目,導向 BookPage.xaml 顯示明細
Book param = lstBooks.SelectedItem as Book;
Frame.Navigate(typeof(BookPage), param);
}
}
b. 建立一個 BookPage.xaml,負責讀入由書藉清單中選擇的書本;
<!-- 定義要顯示書本的內容 -->
<Grid Grid.Row="1" x:Name="ContentRoot" Margin="19,9.5,19,0">
<StackPanel>
<Image Source="{Binding ImgUrl}" Width="100" Height="100" />
<TextBlock Margin="0,20" Text="{Binding Description}"
FontSize="18" TextWrapping="Wrap" />
</StackPanel>
</Grid>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.navigationHelper.OnNavigatedTo(e);
// 根據由 CategoryPage.xaml 所輸入的 Book 來顯示;
if (e.Parameter != null)
{
Book book = e.Parameter as Book;
this.DataContext = book;
}
}
c. 在 App.xaml.cs 中的 rootFrame,設定支援 CacheSize;
if (rootFrame.Content == null)
{
// Removes the turnstile navigation for startup.
if (rootFrame.ContentTransitions != null)
{
this.transitions = new TransitionCollection();
foreach (var c in rootFrame.ContentTransitions)
{
this.transitions.Add(c);
}
}
rootFrame.ContentTransitions = null;
rootFrame.Navigated += this.RootFrame_FirstNavigated;
// 設定 rootFrame 支援的 CacheSize = 1;
rootFrame.CacheSize = 1;
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
if (!rootFrame.Navigate(typeof(CategoryPage), e.Arguments))
{
throw new Exception("Failed to create initial page");
}
}
e. 測試方式,將 debug breakpoint 設定在 CategoryPage.xaml.cs 中的建構,即可以得知如果加上 NavigationCacheMode.Enabled,建構子將不會再被進入;
這樣一來可以簡化返回時需要重新載入大量資料的問題,但也需要注意,如果被 Cache 的 Page 內容是會定期更新的,可以在 OnNavigatedFrom 加入一個標記,
在返回 Page 的 OnNavigatedTo 時做一個 TimeSpan 的比對來更新內容。
======
本篇希望有助於大家在學習 Universal app 時對於 Frame,Page 之間的運作原理有所了解。
當然如果有寫錯的地方,也請大家給予指導,謝謝。
References:
〉[Universal App] 頁面切換的流程 (重要)
〉Part 3: Navigation, layout, and views (重要)
〉Windows 8.1 and Windows 8.1 Phone Convergence. Part 3: App Lifecycle and MVVM
〉Windows 8.1 and Windows 8.1 Phone Convergence. Part 2: App Lifecycle (重要)
〉Windows 8.1 and Windows 8.1 Phone Convergence. Part 1: Controls
〉XAML: Limiting size of control nested in ScrollViewer (to scroll nested within the ScrollViewer)
〉Windows/Phone 8.1–Frame, Page, NavigationHelper, SuspensionManager
〉XAML Navigation sample & Quickstart: Navigating between pages
〉How to share an app bar across pages
〉Windows®8.1 Apps with XAML and C#