Universal App - Frame、Page、Window 的關係

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] 頁面切換的流程>所介紹的內容,我擷取出了其中的一張圖,如下:

w01_thumb

可得知建立一個新的 App,它會擁有一個 Windows 裡面搭配著一個 FrameFrame 裡可以裝 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 的容器,裡面操作的單位為:PageFrame 支援指定要導向(Navigation)的 Page 與需要傳遞的參數

Frame 負責維護使用者所操作的 Page 物件(navigation history),可搭配 CurrentSourcePageType 取得現在操作的 Page

更在每一個 Page 要做導向時,可使用 CanGoBackCanGoForward 二個屬性了解可導向的方向。

另外,在 IsEnabled 可搭配管理 navigation button。

 

可以註冊處理這四個事件:NavigatingNavigatedNavigationStoppedNavigationFailed ,負責在導向發生前後可以對於

畫面或是資料做一些處理,或是處理導向等錯誤訊息。

對於在 Page level 的部分,即時處理對應的 OnNavigateToOnNavigatingFromOnNavigatedFrom 事件來完成初始化或

離開 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 可以定義:TopAppBarBottomAppBar

類型 名稱 說明
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.

 
NavigationCacheMode

    提供讓 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;

如下圖說明:

image

 

 

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,建構子將不會再被進入

   18345257237911834524679177

 

    這樣一來可以簡化返回時需要重新載入大量資料的問題,但也需要注意,如果被 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

Quickstart: adding app bars

How to share an app bar across pages

Windows®8.1 Apps with XAML and C#