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 集合有內容被改變(新增、刪除)時發出事件通知,
在實作該類別時需要實作的事件:
負責提供當實作 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 可以看到它:
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;
由上圖可以得知,SuspensionManager為註冊的 Frame,利用 Frame.SetValue 給予二個值:FrameSessionStateKeyProperty 與 FrameSessionStateProperty。
b. NavigationHelper 在 LoadState 與 SaveState 操作的即是 frameState 中的 pageState,利用 Page Key 為值別:
總之的運作原理如同上面二張圖所示,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)
〉Navigation patterns (Windows Store apps) (重要)
〉Navigation, orientation, and gestures (Windows Phone Store apps)
〉Navigation patterns (Windows Phone Store apps) (重要)