My last post in this series covered the CoreApplicationService (CAS) and how it handles app lifecycle which is part of a broad series discussing how to simplify Windows Phone development. In the lifecycle post navigation was discussed only lightly. This post aims to cover the CAS and how it handles navigation. It will also, cover a few open questions about lifecycle from my previous post. The CAS was originally created and used for navigation exclusively. Desires for centralized lifecycle, tombstoning, and navigation management drove us to buff out the former NavigationService and rename it as CoreApplicationService. Since the NavigationService, we have optimized, simplified, and fundamentally improved navigation on Windows Phone applications.
Lesson #1: Centralize Data
The most important of lessons that we learned was to make sure data is centralized in the app. Any object reference that needs to be passed to the next page can only do so if the reference to that object is available from a centralized data provider. We won’t cover data providers here, but I wanted to bring this lesson to the forefront in order to help bring clarity to the thought process behind the navigation paradigm used in the CAS. We had unknowingly painted ourselves into a corner during our development one of the phone apps we developed. We desired to modify the same object reference in two related pages. We made the mistake of storing the object in the PhoneApplicationService.Current.State dictionary. What we didn’t realized until the final weeks of development and testing was that State dictionary serializes the objects stored in it. That means when obtaining an object stored in the State dictionary it was being deserialized as a new object. We would then modify that object and return back to the first page. The first page would then assume the object was updated when in reality a different object reference had been used and updated. This was a huge bummer to realize so late and taught us to never pass objects via the State dictionary if we need the original object reference.
With that said lets get into some navigation topics.
Navigation Infrastructure
Those familiar with Windows Phone development know that in order to perform any kind of navigation you must use the PhoneApplicationFrame. When initializing the CAS in App.xaml we pass in the PhoneApplicationFrame. This gives us the ability to navigate without requiring a PhoneApplicationPage which is the built in intermediary to the PhoneApplicationFrame. The navigation framework in the CAS is essentially a more developer friendly API wrapper for the PhoneApplicationFrame navigation framework. Our goal was to expose all of the PhoneApplicationFrame navigation framework while buffing out the points where it was lacking.
public class CoreApplicationService
{
private PhoneApplicationFrame _frame;
public event EventHandler<NavigatingCancelEventArgs> Navigating;
private void OnNavigating(NavigatingCancelEventArgs e)
{
var handler = Navigating;
if (handler != null) handler(this, e);
}
public event EventHandler<NavigationEventArgs> Navigated;
private void OnNavigated(NavigationEventArgs e)
{
var handler = Navigated;
if (handler != null) handler(this, e);
}
public Dictionary<object, string> Mappings { get; private set; }
public void Initialize(PhoneApplicationFrame frame, bool isLaunching)
{
LittleWatson.CheckForPreviousException();
_frame = frame;
_frame.Navigated += FrameNavigatedHandler;
_frame.Navigating += FrameNavigatingHandler;
PersistTombstoningValue(IS_LAUNCHING_KEY, isLaunching);
}
private void FrameNavigatedHandler(object sender, NavigationEventArgs e)
{
OnNavigated(e);
}
private void FrameNavigatingHandler(object sender, NavigatingCancelEventArgs e)
{
OnNavigating(e);
}
}
Navigation Mappings
This is one are of functionality where we’re trying to optimize still. Currently we have a dictionary of object to string mappings. The string represents the Uri to a page while the object is some arbitrary key that maps to that page. Managing the keys is the difficult part that we haven’t quite figured out yet. Here is how we register the mappings in LionHeart.
public partial class App : Application
{
private void InitializeNavigationMappings()
{
CoreApplicationService.Instance.Mappings.Add(ViewKeys.HOME_VIEW_KEY, "/Views/HomeView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.MY_VIEW_KEY, "/Views/MyView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.ALL_CLIENTS_VIEW_KEY, "/Views/AllClientsView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.CLIENT_VIEW_KEY, "/Views/ClientView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.MY_REPORTS_VIEW_KEY, "/Views/MyReportsView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.REPORT_VIEW_KEY, "/Views/ReportView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.ALL_SESSIONS_VIEW_KEY, "/Views/AllSessionsView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.MY_SESSIONS_VIEW_KEY, "/Views/MySessionsView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.SESSION_VIEW_KEY, "/Views/SessionView.xaml");
CoreApplicationService.Instance.Mappings.Add(ViewKeys.SETTINGS_VIEW_KEY, "/Views/SettingsView.xaml");
}
}
By using this dictionary of mappings we gain a huge amount of flexibility. This allows the developer to expose and manage navigation keys in lower tiers of the app if desired. In the sample code we use a struct named ViewKeys to manage or contain all view keys. We’ve tried using object types as the key, but we always seemed to come to a point where we were mixing types into ViewModels that shouldn’t know about certain types and tightly coupling types to views. That doesn’t sound all bad, but from experience a struct of string keys has been one of the cleanest and readable approaches.
Note on Managing Views and ViewModels in the CAS: The PhoneApplicationFrame has a BackStack property that manages all pages that are alive in the app. We didn’t want to manage Views or ViewModels in the CAS because that work was already being done by the PhoneApplicationFrame. We also didn’t want to tightly couple the CAS the UI tier, in fact that’s exactly why we need the CAS is that the PhoneApplicationFrame is only available in the UI tier unless passed into a different tier. The CAS is immediately available to ViewModels and any other tier above the very base tier.
Here’s a look at ViewKeys:
public struct ViewKeys
{
public static readonly string HOME_VIEW_KEY = "HOME_VIEW_KEY";
public static readonly string MY_VIEW_KEY = "MY_VIEW_KEY";
public static readonly string ALL_CLIENTS_VIEW_KEY = "ALL_CLIENTS_VIEW_KEY";
public static readonly string CLIENT_VIEW_KEY = "CLIENT_VIEW_KEY";
public static readonly string MY_REPORTS_VIEW_KEY = "MY_REPORTS_VIEW_KEY";
public static readonly string REPORT_VIEW_KEY = "REPORT_VIEW_KEY";
public static readonly string ALL_SESSIONS_VIEW_KEY = "ALL_SESSIONS_VIEW_KEY";
public static readonly string MY_SESSIONS_VIEW_KEY = "MY_SESSIONS_VIEW_KEY";
public static readonly string SESSION_VIEW_KEY = "SESSION_VIEW_KEY";
public static readonly string SETTINGS_VIEW_KEY = "SETTINGS_VIEW_KEY";
}
Not very exciting… And moving on.
Performing Navigation
Now we get into the exciting stuff. How we perform a navigation. Here is some code from LionHeart in HomeVM and helper class NavigationDefinition to help illustrate how to do this.
<ListBox x:Name="Menu"
ItemsSource="{Binding NavigationOptions}"
SelectedItem="{Binding NavigationTarget, Mode=TwoWay}"
DisplayMemberPath="Name"
toolkit:TiltEffect.IsTiltEnabled="True"/>
public class HomeVM : PageViewModel
{
private List<NavigationDefinition> _navigationOptions;
public List<NavigationDefinition> NavigationOptions
{
[DebuggerStepThrough]
get { return _navigationOptions; }
set
{
if (value != _navigationOptions)
{
_navigationOptions = value;
OnPropertyChanged("NavigationOptions");
}
}
}
private NavigationDefinition _navigationTarget;
public NavigationDefinition NavigationTarget
{
[DebuggerStepThrough]
get { return _navigationTarget; }
set
{
if (value != _navigationTarget)
{
_navigationTarget = value;
OnPropertyChanged("NavigationTarget");
Navigate(_navigationTarget);
}
}
}
private void InitializeMenu()
{
if (NavigationOptions == null)
{
NavigationOptions = new List<NavigationDefinition>(new[]
{
new NavigationDefinition(MY_SESSIONS_MENU_ITEM_NAME,
ViewKeys.MY_VIEW_KEY,
new Dictionary<string,string>{{NavigationKeys.PIVOT_ITEM_KEY, MyPivotItem.Sessions.ToString()}}),
new NavigationDefinition(MY_CLIENTS_MENU_ITEM_NAME,
ViewKeys.MY_VIEW_KEY,
new Dictionary<string,string>{{NavigationKeys.PIVOT_ITEM_KEY, MyPivotItem.Clients.ToString()}}),
new NavigationDefinition(MY_REPORTS_MENU_ITEM_NAME,
ViewKeys.MY_VIEW_KEY,
new Dictionary<string,string>{{NavigationKeys.PIVOT_ITEM_KEY, MyPivotItem.Reports.ToString()}}),
new NavigationDefinition(CREATE_REPORT_MENU_ITEM_NAME,
ViewKeys.REPORT_VIEW_KEY),
new NavigationDefinition(SETTINGS_MENU_ITEM_NAME,
ViewKeys.SETTINGS_VIEW_KEY),
});
}
}
private void Navigate(NavigationDefinition definition)
{
if (definition != null)
{
NavigationTarget = null; // reset selection
CoreApplicationService.Navigate(definition.Key, definition.Parameters);
}
}
}
public class NavigationDefinition
{
public NavigationDefinition(string name, object key, Dictionary<string, string> parameters = null)
{
Name = name;
Key = key;
Parameters = parameters;
}
public string Name { get; private set; }
public object Key { get; private set; }
public Dictionary<string, string> Parameters { get; private set; }
}
NavigationDefinition
This simple helper class is just a wrapper for the desired ViewKey, a dictionary of parameter keys and values, and a display friendly name. This class helps to with creating a menu of navigation options in the UI such as the one created in HomeVM.InitializeMenu() in the sample code above.
When we navigate we can append parameters to the page Uri when we pass it to the PhoneApplicationFrame. In HomeVM.InitializeMenu() we create parameters for each NavigationDefinition with a key of ViewKeys.MY_VIEW_KEY. In this case the parameters will be used to determine which pivot item is in view when the user navigates to MyView. How this is done will be covered in a future post.
In HomeView there is a ListBox which displays each NavigationDefinition. The selection of the ListBox is bound to HomeVM.NavigationTarget. When the user selects a definition and NavigationTarget is set we call Navigate() from it’s setter. Navigate() in turn calls CoreApplicationService.Navigate() passing it the view key and the dictionary of parameters.
Processing a Navigation Request
public class CoreApplicationService
{
/// <summary>
/// Navigates the specified viewKey.
/// </summary>
/// <param name="viewKey">The viewKey.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="countToRemoveFromBackStackAfterNavigation">The count to remove from back stack. (ignored if clearBackStack == <c>true</c>)</param>
/// <param name="clearBackStack">if set to <c>true</c> [clear back stack].</param>
public void Navigate(object viewKey, Dictionary<string, string> parameters = null,
int countToRemoveFromBackStackAfterNavigation = 0, bool clearBackStack = false)
{
string target;
if (viewKey != null && Mappings.TryGetValue(viewKey, out target))
{
parameters = parameters ?? new Dictionary<string, string>();
parameters.Add(IS_RUNNING_KEY, true.ToString());
ICollection<string> parameterKeyValueStrings =
parameters.Keys.Select(key => string.Format("{0}={1}", key, parameters[key])).ToArray();
target += string.Format("?{0}", string.Join("&", parameterKeyValueStrings));
if (clearBackStack)
{
_frame.Navigated += NavigationReadyToClearBackStackHandler;
}
else if (countToRemoveFromBackStackAfterNavigation > 0)
{
_countToRemoveFromBackStackAfterNavigation = countToRemoveFromBackStackAfterNavigation;
_frame.Navigated += NavigationReadyToRemoveCountFromBackStack;
}
_frame.Navigate(new Uri(target, UriKind.RelativeOrAbsolute));
}
else
{
throw new ArgumentException(string.Format("Cannot navigate to target view. Driver={0}", viewKey));
}
}
}
Here we find the meat of navigation processing. The first thing done here is a check to validate the viewKey is a valid key to a page Uri then acquires the target page Uri. If no parameters were passed an empty parameters dictionary is created and immediately we add a parameter for IS_RUNNING_KEY which is always set to true if this method is called. The IS_RUNNING_KEY key is used by ViewModels in order to know how to initialize. Again, another topic that will be covered in a future blog post. After the parameters collection is finalized the CAS then appends each parameter to the target Uri forming the finalized target Uri.
The next step is to hook up handlers for the PhoneApplicationFrame.Navigated event when necessary. Sometimes it is desirable to perform a navigation that clears the entire PhoneApplicationFrame.BackStack. Other times, it is desirable to remove a specified number of pages from the PhoneApplicationFrame.BackStack. If clearBackStack is true then after navigation is complete the CAS clears the PhoneApplicationFrame.BackStack. If clearBackStack is false and countToRemoveFromBackStackAfterNavigation is greater than 0 then after navigation completes the CAS removes the specified number of page entries from the PhoneApplicationFrame.BackStack. This functionality is helpful for navigating to menu pages and in app navigation resets.
Finally, the CAS calls PhoneApplicationFrame.Navigate() allowing Windows Phone to take over.
ViewBase and Handling Navigation in Pages
First, there is currently a disconnect in terminology. Windows Phone calls each View a Page, and here at Interknowlogy we’re in the habit of calling each Page a View. So each term means exactly the same thing and we’re working on simplifying our terminology. With that aside lets look at ViewBase.
ViewBase becomes the new base class of each View in the app. This cuts down on duplicating this code for each view. Because of that maintainability goes up, which is always a good thing.
public partial class HomeView : ViewBase
{
public HomeView()
{
InitializeComponent();
}
}
public class ViewBase : PhoneApplicationPage
{
public PageViewModel PageViewModel { get { return DataContext as PageViewModel; } }
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (PageViewModel != null)
{
PageViewModel.Initialize(NavigationContext.QueryString);
}
base.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
if (PageViewModel != null)
{
PageViewModel.Uninitialize(e.NavigationMode == NavigationMode.Back);
}
base.OnNavigatedFrom(e);
}
protected override void OnRemovedFromJournal(JournalEntryRemovedEventArgs e)
{
if (PageViewModel != null)
{
PageViewModel.Uninitialize(true);
}
base.OnRemovedFromJournal(e);
}
}
ViewBase is by no means required, but it is a nice piece of reusable code. Each page desiring to leverage the navigation framework must have a ViewModel derived from PageViewModel as its DataContext. ViewBase is responsible for knowing what to do with its ViewModel and when to do it. It knows that in OnNavigatedTo the ViewModel needs to be Initialized with the specified parameters, and that in OnNavigatedFrom and OnRemovedFromJournal the ViewModel needs to be Uninitialized and how. OnRemovedFromJournal is called on a Page when it is removed from the PhoneApplicationFrame.BackStack.
Conclusion
The CAS makes navigation more powerful and flexible. It’s simple to request navigation via the CAS and it’s reliable. The CAS does not replace existing navigation options, instead it builds upon the existing framework and extends possibilities. The CAS also makes navigation more reusable and readable inside ViewModels. I hope this post helps inspire innovation with Windows Phone.