The next thing I wanted to work through was state management. The VisualStudio templates have a nice implementation of state management through SuspensionManager and LayoutAwarePage. LayoutAwarePage isn't an ideal match as a View for Caliburn.Micro MVVM. It tries to implement it's own MVVM pattern with the DefaultViewModel property, it contains a whole nested observable class definition, LoadState/SaveState logic in the View, and just plain does too much non-view stuff.
With that said, LayoutAwarePage has many things going for it with VisualState switching management and hooking key and mouse navigation. LayoutAwarePage is only loosely coupled to SuspensionManager, so I figured I could fix these issues while still using SuspensionManager. To start out, I copied the code from LayoutAwarePage into a new class called AppPage. Then, trimmed out the DefaultViewModel and the observable collection definition.
AppPage.cs (interesting bits)
namespace CaliburnMicro_IoC_Sample.Code
{
public class AppPage : Page
{
.
.
.
#region Process lifetime management
private String _pageKey;
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached. The Parameter
/// property provides the group to be displayed.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Returning to a cached page through navigation shouldn't trigger state loading
if (this._pageKey != null)
return;
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
this.LoadState(e.Parameter, null, e);
}
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
this.LoadState(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey], e);
}
}
.
.
.
/// <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="navigationParameter">The parameter value passed to
/// <see cref="Frame.Navigate(Type, Object)"/> when this page was initially requested.
/// </param>
/// <param name="pageState">A dictionary of state preserved by this page during an earlier
/// session. This will be null the first time a page is visited.</param>
protected virtual void LoadState(Object navigationParameter, Dictionary<String, Object> pageState, NavigationEventArgs e)
{
if (DataContext == null)
{
var svc = ServiceLocator.Current.GetInstance<inavigationservice>() as SuspensionFrameAdapter;
if (svc != null) svc.DoNavigated(this, e);
}
if (DataContext is IHaveState && pageState != null && pageState.Count() > 0)
{
var haveState = DataContext as IHaveState;
haveState.LoadState(Convert.ToString(navigationParameter), pageState);
}
}
/// <summary>
/// Preserves state associated with this page in case the application is suspended or the
/// page is discarded from the navigation cache. Values must conform to the serialization
/// requirements of <see cref="SuspensionManager.SessionState"/>.
/// </summary>
/// <param name="pageState">An empty dictionary to be populated with serializable state.</param>
protected virtual void SaveState(Dictionary<String, Object> pageState)
{
if (DataContext is IHaveState)
{
var haveState = DataContext as IHaveState;
haveState.SaveState(pageState, SuspensionManager.KnownTypes);
}
}
#endregion
}
You may notice a strange DataContext null test in LoadState, that has to do with a problem with Frames and SuspensionManager, described below. It is not my best work, and I should probably refactor it to utilize another interface, rather than a little hidden knowledge that the INavigationService was implemented by SuspensionFrameAdapter.