Universal App - NavigationHelper 與 SuspendedManager

Universal App–NavigationHelper 與 SuspendedManager

在介紹了<Universal App - Frame、Page、Window 的關係>時我發現 Visual Studio 2013 在建立 Universal App 專案時,

如果選擇的是「Basic Project」或是「Basic Page」 時,Visual Studio 會自動建立以下內容:

一個 Common 目錄,裡面有四個檔案;

‧NavigationHelper

‧ObservableDictionary

‧RelayCommand

‧SuspensionManager

 

這四個元素究竟在 App 裡扮演著什麼樣的角色與任務,以下再加以介紹:

 

》ObservableDictonary:

    該類別實作 IObservableMap 介面,為的是當該類別中的 _dictionary 集合有內容被改變(新增、刪除)時發出事件通知,

在實作該類別時需要實作的事件:

MapChanged

   負責提供當實作 IObservableMap 介面中有 <key, value/> 項目被變動時,要觸發該事件。

   觸發事件時會送出 IMapChangedEventArgs<K> 將 key 送出通知註冊該事件的程式可得知何種內容改變。

 

該類別的目是提供給 NavigationHelper 可以儲存與還原指定 Page 中的內容物,所以被當作為 default view model 物件來使用。

例如:

public sealed partial class HubPage : Page
{
    private readonly ObservableDictionary defaultViewModel = new ObservableDictionary();
 
    /// <summary>
    /// Gets the view model for this <see cref="Page"/>.
    /// This can be changed to a strongly typed view model.
    /// </summary>
    public ObservableDictionary DefaultViewModel
    {
        get { return this.defaultViewModel; }
    }
 
    /// <summary>
    /// Populates the page with content passed during navigation.  Any saved state is also
    /// provided when recreating a page from a prior session.
    /// </summary>
    /// <param name="sender">
    /// The source of the event; typically <see cref="NavigationHelper"/>
    /// </param>
    /// <param name="e">Event data that provides both the navigation parameter passed to
    /// <see cref="Frame.Navigate(Type, Object)"/> when this page was initially requested and
    /// a dictionary of state preserved by this page during an earlier
    /// session.  The state will be null the first time a page is visited.</param>
    private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
    {
        // TODO: Create an appropriate data model for your problem domain to replace the sample data
        var sampleDataGroups = await SampleDataSource.GetGroupsAsync();
        this.DefaultViewModel["Groups"] = sampleDataGroups;
    }
}

可看到 defaultViewModel 用來保存畫面中的 data source,當然是否需要使用到 ObservableDictonary 來保存就看需求。

我在自己的專案裡有使用,因為 ObservableDictonary 提供 <key, value/> 結構,很方便協助保存與復原資料。

 

 

》RelayCommand

    該類別定義協助 NavigationHelper 實作 GoBackCommand 的使用,裡面有二個私用的 delegate,分別為:Action 與 Fun<T>,

負責接收實例化該類別的人,可以加入自訂義的事件與回傳值。

該類別被用在處理 NavigationHelper 在請求Frame 做 GoBack 或是 GoForward 的任務:

a. NavigationHelper 宣告二個 RelayCommand:_goBackCommand 與 _goForwardCommand;

b. _goBackCommand 在初始化時,註冊二個 method:GoBack() (for Action);CanGoBack() (for Fun<bool>);

c. 如果是 Windows Store App 通常 GoBackCommand 用來 binding 左上角的 back Button's Command property;

d. 在 NavigationHelper 註冊當用戶按下 Back 鍵時會呼叫註冊好的 RelayCommand 來執行。

 

ICommand

   提供定義 command 的介面。需要實作三個項目:

類型 名稱 說明
Method bool CanExecute(object parameter) Defines the method that determines whether the command can execute in its current state.
用來判斷要指執行的 Command 是否可以被執行。
  void Execute(object parameter) Defines the method to be called when the command is invoked.
執行註冊處理的 Command。
Event EventHandler CanExecuteChanged; Occurs when changes occur that affect whether or not the command should execute.

 

 

NavigationHelper

    負責處理 Page 在 Frame 中導向與返回時需要處理的事件,提供 Commands 負責在 Windows Store App 上透過鍵盤與滑鼠處理

協助 navigate back 與 forward 或是 Windows Phone 的實體 Back 鍵。並且整合 SuspensionManger 處理 Page 生命週期與狀態的管理。

使用方式如下範例:

a. 在 Page 的建構式實例化一個 NavigationHelper 物件,並且註冊 LoadState 與 SaveState 二個事件;

private readonly NavigationHelper navigationHelper;
public NavigationHelper NavigationHelper
{
    get { return this.navigationHelper; }
}
 
public HubPage()
{
    this.InitializeComponent();
    // 實例化 NaivgationHelper 與註冊必要的事件;
    this.navigationHelper = new NavigationHelper(this);
    this.navigationHelper.LoadState += this.NavigationHelper_LoadState;
    this.navigationHelper.SaveState += this.NavigationHelper_SaveState;
}        

 

b. 在覆寫 Page 的 OnNavigatedTo 與 OnNavigatedFrom 分別呼叫 NavigationHelper 的 OnNavigatedTo 與  OnNavigatedFrom;

    呼叫 NavigationHelper 的 OnNavigatedTo 與  OnNavigatedFrom,它會請求 SuspensionManager 將 Page 狀態的值還原與保存,

   然後觸發 LoadState 與 SaveState 事件;

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    this.navigationHelper.OnNavigatedTo(e);
}
 
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    this.navigationHelper.OnNavigatedFrom(e);
}
 
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
    // 從 LoadStateEventArgs 取得保存起來的狀態值; PageState
}
 
private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
    // 將要保存的狀態值加以寫入;PageState
}

 

重要事件、方法與屬性說明:

類型 名稱 說明
屬性 Page Page 物件,於 NavigationHelper 內使用,在 NavigationHelper 建構式時取得 Page 物件,並且註冊其 Loaded 與 UnLoaded 事件。

‧Loaded:識別是 Windows Store App 或 Windows Phone,分別註冊它們對應的事件;
                  =>Windows Store App:只有在全螢幕時才註冊 AcceleratorKeyActivated 與 PointerPressed 事件;
                  =>Windows Phone:註冊 HardwareButtons.BackPressed 事件;

‧UnLoaded:識別是 Windows Store App 或 Windows Phone,分別解註冊在 Loaded 註冊的事件;
  Frame Frame 物件,於 NavigationHelper 內使用,在 NavigationHelper 建構式時取得 Page 物件後,取得該 Page 對應的 Frame 物件。
功能:
a. 判斷該 Frame 的 Navigation history 是否可以 GoBack 或 GoForward;
b. 搭配 SuspensionManager 取得該 Frame 所保存操作過 Page 的 PageState;
c. Frame 用於取得集合,Page 對應的 Key ("Page-" + this.Frame.BackStackDepth) 才能取得 PageState;
Field _pageKey 做為實例化該 NavigationHelper 時傳入 Page 的識別 Key。作用於 SuspensionManager 在搭配 Frame 取得集合後,進行 Key 取值得到 PageState 專用。
  GoBackCommand Read/Write,在取得時如果 _goBackCommand 為 null 會重新建立 RelayCommand 物件,以 this.GoBack() 傳入至 Action,this.CanGoBack() 傳入至 Fun<T>。
  GoForwardCommand Read/Write,在取得時如果 __goForwardCommand 為 null 會重新建立 RelayCommand 物件,以 this.GoForward() 傳入至 Action,this.CanGoForward() 傳入至 Fun<T>。
方法 NavigationHelper(Page page) 建構式,利用傳入的 Page 參數,註冊 Loaded 與 Unloaded。
  CanGoBack() 根據 Frame 物件識別是否可以進行 GoBack 方法。
  CanGoForward() 根據 Frame 物件識別是否可以進行 GoForward 方法。
  GoBack() 根據 Frame 物件進行 GoBack 方法。
  GoForward() 根據 Frame 物件進行 GoForward 方法。
  OnNavigatedTo 搭配 Page.OnNavigationTo 事件時執行的方法。其內容:
a. 從 SuspensionManager.SessionStateForFrame 取得目前 Frame 的集合;
b. 建立 _pageKey 為 "Page-" + this.Frame.BackStackDepth;
c. 識別 e.NavigationMode == NavigationMode.New:
    =>代表是新開啟該 Page,識別集合中是否已存在相同 Page 的 key,存在則先刪除,再引動 LoadState 事件;
    =>如果識別不是 NavigationMode.New:從集合中取得該 Page 對應的值傳入 Page中;
  OnNavigatedFrom 搭配 Page.OnNavigatedFrom 事件時執行的方法。其內容:
a. 從 SuspensionManager.SessionStateForFrame 取得目前 Frame 的集合;
b. 建立一個 new Dictionary<String, Object>(); 物件,並引用 SaveState 事件;
c. 並且把物件儲存於集合中。
事件 LoadState LoadStateEventHandler:處理完 OnNavigatedTo(NavigationEventArgs e) 方法後,觸發該事件。
  SaveState SaveStateEventHandler:處理完 OnNavigatedFrom(NavigationEventArgs e) 方法後,觸發該事件。

 

相關 NavigationHelper 處理生命週期的邏輯如下:

#region Process lifetime management
 
private String _pageKey;
 
/// <summary>
/// Register this event on the current page to populate the page
/// with content passed during navigation as well as any saved
/// state provided when recreating a page from a prior session.
/// </summary>
public event LoadStateEventHandler LoadState;
/// <summary>
/// Register this event on the current page to preserve
/// state associated with the current page in case the
/// application is suspended or the page is discarded from
/// the navigaqtion cache.
/// </summary>
public event SaveStateEventHandler SaveState;
 
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.  
/// This method calls <see cref="LoadState"/>, where all page specific
/// navigation and process lifetime management logic should be placed.
/// </summary>
/// <param name="e">Event data that describes how this page was reached.  The Parameter
/// property provides the group to be displayed.</param>
public void OnNavigatedTo(NavigationEventArgs e)
{
    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    this._pageKey = "Page-" + this.Frame.BackStackDepth;
 
    if (e.NavigationMode == NavigationMode.New)
    {
        // Clear existing state for forward navigation when adding a new page to the
        // navigation stack
        var nextPageKey = this._pageKey;
        int nextPageIndex = this.Frame.BackStackDepth;
        while (frameState.Remove(nextPageKey))
        {
            nextPageIndex++;
            nextPageKey = "Page-" + nextPageIndex;
        }
 
        // Pass the navigation parameter to the new page
        if (this.LoadState != null)
        {
            this.LoadState(this, new LoadStateEventArgs(e.Parameter, null));
        }
    }
    else
    {
        // Pass the navigation parameter and preserved page state to the page, using
        // the same strategy for loading suspended state and recreating pages discarded
        // from cache
        if (this.LoadState != null)
        {
            this.LoadState(this, new LoadStateEventArgs(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]));
        }
    }
}
 
/// <summary>
/// Invoked when this page will no longer be displayed in a Frame.
/// This method calls <see cref="SaveState"/>, where all page specific
/// navigation and process lifetime management logic should be placed.
/// </summary>
/// <param name="e">Event data that describes how this page was reached.  The Parameter
/// property provides the group to be displayed.</param>
public void OnNavigatedFrom(NavigationEventArgs e)
{
    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    var pageState = new Dictionary<String, Object>();
    if (this.SaveState != null)
    {
        this.SaveState(this, new SaveStateEventArgs(pageState));
    }
    frameState[_pageKey] = pageState;
}
 
#endregion

 

在 LoadState 與 SaveState 二個 EventHandler 在定義的 delegate 均有各自的 EventArgs,如下:

‧LoadStateEventArgs

    被用於 NavigationHelper 在處理完 OnNavigatedTo(NavigationEventArgs e) 方法後,觸發 LoadState 所夾帶的參數。

    具有二個屬性:

    ‧NavigationParameter:來自 Frame.Navigate 所傳入的參數;

    ‧PageState:該 Page 所保存的所有狀態值,採用 Dictionary<String, Object>() 保存。

   

‧SaveStateEventArgs

    被用於 NavigationHelper 在處理完 OnNavigatedFrom(NavigationEventArgs e) 方法後,觸發 SaveState 所夾帶的參數。

    具有一個屬性:

    ‧PageState:該 Page 所保存的所有狀態值,採用 Dictionary<String, Object>() 保存;

                         PageState 會被保存至 SuspensionManager 所定義的集合中,集合依每一個 Page 為 key,其 value 為 PageState。

 

 

》SuspensionManager

    在 Visual Studio 的以下幾種 project templates 可以看到它:

image

SuspensionManager 的任務在於 App 啟動時,針對主要的 Frame 進行註冊,負責處理當 App 被 Suspending 或重新 Launched 時狀態的保存與還原

<Windows Phone 8.1 - Application lifecycle 概念>與<Windows Phone 8.1 - 操作 App Lifecycle>介紹得知 App 可能被系統或用戶結束,所以需要保存與還原 App狀態。

SuspensionManager 擷取了 App 整個的狀態(透過 rootFrame 註冊與記錄所有在 rootFrame 中執行過的 Page 的狀態),以簡化管理 app 的 lifecycle。

 

[注意]

a. SessionState 可能因某些條件自動被清除,只適用於儲存一些方便於跨不同 session 的資訊,其他資訊建議操作 Application Data;

b. 由於 SessionState 的 SaveAsync() 與 RestoreAsync() 都利用 DataContractSerialization 實現,所以保存的資料型別是要可以被 Serialized 的;

c. SuspensionManager 保存狀態至一個 dicitonary:

   =>該 dictionary 是針對 Frame 來註冊,一個 Frame 對應一個 Key,形成一個 FrameState dictionary;

   =>FrameState dictionary 負責保存該 Frame 下所操作的 Pages,包括 navigate parameters 或是其他用戶想要增加的參數值;

 

[運作原理]

a. Frame 被建立時,如果想要保存這個 Frame 裡的資訊,需要先向 SuspensionManger 註冊呼叫「SuspensionManager.RegisterFrame(rootFrame, "AppFrame")」;

   一個 Frame 對應一個 Key,大部分的 App 只會用到一個 rootFrame,如果您的 App 有第二個 Frame,要在註冊時給予不同的名稱。

b. 當 Frame 被註冊成功後,將會得到二個屬性:

    (1) Key:用來代表這個 Frame 的識別值;

    (2) dictionary of session state: 保存該 Frame 的所有狀態;

    =>如果是註冊過的 Frame 它這二個屬性會被直接復原;如果 Frame 反註冊後,它的資料(navigation state and history )將會被清掉。

c. 對應於 App.xaml.cs 需要注意:

   App.xaml.cs 在建構式時:

       a). 註冊整個 App 的 Suspending 事件;

       b). 實作 Suspending 事件,請求在 Suspending 時需要延遲關閉的事件,並進行 SuspensionManager.SaveAsync();

  App.xaml.cs  在 OnLaunched 時:

       a). 先將 rootFrame 註冊至 SuspensionManager;

       b). 識別上次關閉程式的原因如果是:ApplicationExecutionState.Terminated,需要進行 SuspensionManager.RestoreAsync();

 

SuspensionManager 的重要方法與屬性:

類型 名稱 說明
屬性 SessionState Dictionary<string, object>()。
提供存取指定 Frame 中的所有 Page 操作的 session 狀態。其資料會搭配 SaveAsync() 與 RestoreAsync() 二個方法分別保存與還原。
  KnownTypes List<Type>。
儲存自訂義類型(custom types)的清單,提供在 SaveAsync() 與 RestoreAsync() 時 DataContractSerializer 的方法。預設是空白,可依預求來自定義其序列化的方法。
方法 SaveAsync() 負責保存完整的 Session State,包括所有註冊 Frame 的 NavigationState、操作的 SessionState。其保存的內容將被序列化以後保存至一個實體檔案放在 LocalFolder中。
  RestoreAsync(String sessionBaseKey = null) 讀取先前保存的 SessionState。所有註冊的 Frames 將會被還原至先前的 navigation state,將也有機會還原 Page 的狀態。
  RegisterFrame 傳入指定的 Frame 與 sessionStateKey,將 Frame 註冊至 _registeredFrames 清單。
  UnregisterFrame 傳入指定的 Frame 向 _registeredFrames 進行返註冊。
  SessionStateForFrame 傳入指定的 Frame 取得被儲存在 Frame.GetValue 中的 FrameSessionStateKeyProperty,如果不存在將會自動建立一個 Dictionary<String, Object>() 集合,並寫入 Frame 中的 SetValue。
  RestoreFrameNavigationState 還原 Fame 的 navigation state,一樣使用返序列化的方式。
  SaveFrameNavigationState 保存 Fame 的 navigation state (frame.GetNavigationState()),一樣使用序列化的方式。

 

從上面讀下來,也許有些混亂,究竟 SuspensionManager 保存的 SessionState 與 NavigationHelper 操作的 frameState 是什麼關係,透過下圖來表示:

 

a. Frame 註冊於 SuspensionManager

   image

   由上圖可以得知,SuspensionManager為註冊的 Frame,利用 Frame.SetValue 給予二個值:FrameSessionStateKeyProperty 與 FrameSessionStateProperty。

 

b. NavigationHelper 在 LoadState 與 SaveState 操作的即是 frameState 中的 pageState,利用 Page Key 為值別

    image

 

總之的運作原理如同上面二張圖所示,Frame 先向 SuspensionManager 註冊取得 Key 與 frameState,在這個 Frame 底上操作的 Page,

可以由該 framePage 裡取得以 Page key 為識別值的 pageState,這二種 State 都是 Dictionary<String, Object>() 的資料結構。

 

c. 如何藉由 LoadState 與 SaveState 操作 pageState

private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
    // 在 Page1 的 LoadState 時取得在離開 Page1 時呼叫 SaveState 所保存的資料
    if (e.PageState != null)
    {
        String myData = e.PageState["myData"].ToString();
    }
}
 
private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
    // 離開畫面時,先將畫面中的資料保存起來
    e.PageState["myData"] = "Pou";
}

上述是一個簡單的範例,需要搭配 NavigationHelper 的 OnNavigateTo 與 OnNavigatedFrom 二個事件,即可以得知,

每一個 Page 的 pageState 在第一次進入該 Page 的 LoadState 會是 null,只有在離開該 Page 的 SaveState 儲存後才有資料,

然而,每一個 Page 的 pageState 會被保存於 frameState,而 frameState 被保存於 SuspensionManager.SessionState,

所以 SuspensionManager 的 SaveStateAsync() 與 RestoreStateAsync 才能完整的保存所有 Frames 中的資料。

 

======

本篇介紹的資訊主要參考<Quickstart: Navigating between pages (XAML)>的介紹,並且補上自己在學習時的一些心得,

希望有助於學習 Universal App 時遇到這些元素而感到疑惑的人。如果有寫錯或不完整的地方,也請多多指教,謝謝。


References

Quickstart: Navigating between pages (XAML)

Navigating between pages (XAML) (重要)

Guidelines for app bars (Windows Store apps)

Your first app - Part 3: Navigation, layout, and views

Your first app - Add navigation and views in a C++ Windows Store app (tutorial 3 of 4)

XAML Navigation sample

Navigation patterns (Windows Store apps) (重要)

Navigation, orientation, and gestures (Windows Phone Store apps)

Navigation patterns (Windows Phone Store apps) (重要)