Performing Navigation
You Will Learn
- How to adapt the NavigationService SDK class and consume the adapted class.
- How to adapt the MessageBox SDK class and consume the adapted class.
- How to create a mock of the NavigationService class and write unit tests that test view model business logic by consuming the mock class.
- How to create a mock of the adapted MessageBox class and write unit tests that test view model business logic by consuming the mock adapted class.
Applies To
- Silverlight for Windows Phone OS 7.1
On this page: | Download: |
---|---|
The Application | Adapting the NavigationService SDK Class | Adapting the MessageBox SDK Class | Consuming the Adapters | Unit Testing the Application | Summary |
The purpose of this sample application is to demonstrate how to build a testable Windows Phone application that performs navigation and displays a message to the user.
The Application
This sample application displays a page that lists the users' name, and demonstrates navigating to another page where the user can enter their name. In addition, the application demonstrates how to persist state to the application's state dictionary. You can download the code for this sample application at the following link:
When the application is launched, the first page in the following illustration is shown. Clicking the button in the application bar brings the user to an edit page where he can enter his name. If the user attempts to navigate away from the edit page without saving the changes, a message is displayed informing him that he hasn't saved his changes. When the user saves the changes, the entered name is saved into the application's state dictionary, and the application navigates to the main page where the entered name is loaded from the application's state dictionary.
Adapting the NavigationService SDK Class
The PhoneApplicationFrame class exposes a series of methods that use the NavigationService class, which facilitates navigating between pages in a Windows Phone application. However, this NavigationService is not easily testable. To create a testable version of the NavigationService you could adapt the class and then create a mock for it. However, due to the relative complexity of the class, a facade is created over it, with a mock then being created.
The interface for the NavigationService class is not available in the Windows Phone 7.1 SDK. Therefore, the INavigationService interface was generated from the NavigationService SDK class. The following code example shows the INavigationService interface, which contains the properties, events, and method signatures that are implemented by the NavigationService class.
public interface INavigationService
{
// Events
event NavigatedEventHandler Navigated;
event NavigatingCancelEventHandler Navigating;
event FragmentNavigationEventHandler FragmentNavigation;
event EventHandler<JournalEntryRemovedEventArgs> JournalEntryRemoved;
event NavigationFailedEventHandler NavigationFailed;
event NavigationStoppedEventHandler NavigationStopped;
// Methods
void GoBack();
void GoForward();
bool Navigate(Uri source);
JournalEntry RemoveBackEntry();
void StopLoading();
// Properties
bool CanGoBack { get; }
bool CanGoForward { get; }
Uri CurrentSource { get; }
IEnumerable<JournalEntry> BackStack { get; }
Uri Source { get; set; }
}
The INavigationService interface is implemented by the ApplicationFrameNavigationService class. The ApplicationFrameNavigationService class creates a facade over the NavigationService class from the Windows Phone 7.1 SDK. A service class is a facade that exposes a loosely coupled unit of functionality that implements one action. Instead of using the methods exposed by the PhoneApplicationFrame class for navigation, you should use the ApplicationFrameNavigationService class. The following code example shows the ApplicationFrameNavigationService class.
public class ApplicationFrameNavigationService : INavigationService
{
private readonly PhoneApplicationFrame frame;
public ApplicationFrameNavigationService(PhoneApplicationFrame frame)
{
this.frame = frame;
this.frame.Navigated += frame_Navigated;
this.frame.Navigating += frame_Navigating;
this.frame.NavigationFailed += frame_NavigationFailed;
this.frame.NavigationStopped += frame_NavigationStopped;
this.frame.FragmentNavigation += frame_FragmentNavigation;
this.frame.JournalEntryRemoved += frame_JournalEntryRemoved;
}
public bool CanGoBack
{
get { return this.frame.CanGoBack; }
}
public bool CanGoForward
{
get { return this.frame.CanGoForward; }
}
public Uri CurrentSource
{
get { return this.frame.CurrentSource; }
}
public IEnumerable<JournalEntry> BackStack
{
get { return this.frame.BackStack; }
}
public Uri Source
{
get { return this.frame.Source; }
set { this.frame.Source = value; }
}
public void GoBack()
{
this.frame.GoBack();
}
public void GoForward()
{
this.frame.GoForward();
}
public bool Navigate(Uri source)
{
return this.frame.Navigate(source);
}
public JournalEntry RemoveBackEntry()
{
return this.frame.RemoveBackEntry();
}
public void StopLoading()
{
this.frame.StopLoading();
}
public event NavigatedEventHandler Navigated;
void frame_Navigated(object sender, NavigationEventArgs e)
{
var handler = this.Navigated;
if (handler != null)
{
handler(sender, e);
}
}
…
}
The ApplicationFrameNavigationService class creates an instance of the PhoneApplicationFrame class. All methods and properties called on the ApplicationFrameNavigationService class call the respective methods and properties on the PhoneApplicationFrame instance. The events that are supported by the ApplicationFrameNavigationService class are Navigated, Navigating, NavigationFailed, NavigationStopped, FragmentNavigation, and JournalEntryRemoved. The Navigated event occurs when the content that is being navigated to has been found. The Navigating event occurs when a new navigation is requested. The NavigationFailed event occurs when an error is encountered while navigating to the requested content. The NavigationStopped event occurs when a navigation is terminated either by calling the StopLoading method, or when a new navigation is requested while the current navigation is in progress. The FragmentNavigation event occurs when navigation to a content fragment begins. The JournalEntryRemoved event occurs during a RemoveBackEntry operation or during a normal back navigation after the Navigated event has been raised.
Adapting the MessageBox SDK Class
The MessageBox class displays a message to the user and optionally prompts for a response. However, this class is not easily testable because it contains some static members. To create a testable version of this class you should adapt the class and then create a mock for it.
The interface for the MessageBox class is not available in the Windows Phone 7.1 SDK. Therefore, the IMessageBox interface was generated from the MessageBox SDK class. The following code example shows the IMessageBox interface, which contains two method signatures.
public interface IMessageBox
{
MessageBoxResult Show(string messageBoxText);
MessageBoxResult Show(string messageBoxText, string caption,
MessageBoxButton button);
}
The IMessageBox interface is implemented by the MessageBoxAdapter class. The MessageBoxAdapter class adapts the MessageBox class from the Windows Phone 7.1 SDK. Adapter classes pass parameters and return values to and from the underlying Windows Phone 7.1 SDK class. Any classes that want to consume the MessageBox class to display a message to the user should consume the MessageBoxAdapter class instead. The following code example shows the MessageBoxAdapter class.
public class MessageBoxAdapter : IMessageBox
{
public MessageBoxResult Show(string message)
{
return MessageBox.Show(message);
}
public MessageBoxResult Show(string messageBoxText, string caption,
MessageBoxButton button)
{
return MessageBox.Show(messageBoxText, caption, button);
}
}
The MessageBoxAdapter class implements two Show methods. The first Show method displays a message box that contains the specified text and an OK button. The second Show method displays a message box that contains the specified text, title bar caption, and response buttons.
Consuming the Adapters
The following code example shows the MainPage view, which comprises an ApplicationBar and a series of TextBlocks that display text on the page.
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton IconUri="/images/appbar.edit.rest.png"
Text="edit" Click="EditButton_Click"/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneAccentBrush}">
…
<!--TitlePanel contains the name of the application and page title-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="PageTitle" Text="hello" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
<TextBlock x:Name="ApplicationTitle" Text="MY NAME IS"
Style="{StaticResource PhoneTextNormalStyle}"/>
</StackPanel>
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="24,0,24,24" Background="White">
<TextBlock TextWrapping="Wrap" Text="{Binding UserName}"
Margin="12" Foreground="Black" FontSize="36"/>
</Grid>
</Grid>
The ApplicationBar contains an ApplicationBarIconButton that calls an event handler when the Click event is raised. The event handler simply invokes the NavigateToEditPage method in the MainPageViewModel class, which navigates the user to the EditPage view. The TextBlock inside the Grid binds to the UserName property of the MainPageViewModel.
The ViewModelLocator class connects the view to the view model by setting the view's DataContext to the value of the MainPageViewModel property in the ViewModelLocator class. The following code example shows the MainPageViewModel property.
private MainPageViewModel mainPageViewModel;
…
public MainPageViewModel MainPageViewModel
{
get
{
if (mainPageViewModel == null)
{
mainPageViewModel = new MainPageViewModel
(
new ApplicationFrameNavigationService(((App)Application.Current)
.RootFrame)
);
}
return mainPageViewModel;
}
}
The code checks to see if the mainPageViewModel field is null, and only if it is does the MainPageViewModel property return a new instance of the MainPageViewModel class and pass a new instance of the ApplicationFrameNavigationService class into the MainPageViewModel constructor. This check for null ensures that a new instance of the MainPageViewModel class isn't created each time the MainPage view is navigated to.
The MainPageViewModel class uses an instance of the INavigationService interface. The following code example shows how the constructor receives these instances from the ViewModelLocator class.
private INavigationService navigationService;
private readonly Uri pageUri;
…
public MainPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
pageUri = new Uri("/MainPage.xaml", UriKind.Relative);
this.navigationService.Navigated += navigationService_Navigated;
}
The constructor sets the navigationService field to the instance of the ApplicationFrameNavigationService class it receives from the ViewModelLocator class. Initializing the class in this way enables view model testability, as a mock implementation of the ApplicationFrameNavigationService class can be passed into the constructor. The constructor then sets the pageUri field to MainPage.xaml and registers an event handler method to the Navigated event of the instance of the ApplicationFrameNavigationService class.
When the user clicks the ApplicationBarIconButton on the MainPage view, the NavigateToEditPage method in the MainPageViewModel is called. The following code example shows this method, along with the event handler method that is called when the Navigated event is raised in the instance of the ApplicationFrameNavigationService class.
public void NavigateToEditPage()
{
navigationService.Navigate(new Uri("/EditPage.xaml", UriKind.Relative));
}
void navigationService_Navigated(object sender, NavigationEventArgs e)
{
if (e.Uri == pageUri)
{
if (PhoneApplicationService.Current.State.ContainsKey("username"))
{
UserName = PhoneApplicationService.Current.State["username"].ToString();
}
}
}
The NavigateToEditPage method simply calls the Navigate method from the instance of the ApplicationFrameNavigationService class to navigate to the EditPage view. The Navigated event occurs when the content that is being navigated to has been found, and so the navigationService_Navigated method, which handles this event, sets the UserName property by retrieving the username from the application's state dictionary, but only if the MainPage view is being navigated to. This ensures that the user name entered in the EditPage view is displayed on the MainPage view.
The following code example shows the EditPage view, which also comprises an ApplicationBar, a series of TextBlocks that display text on the page, and a TextBox for entering a username.
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton IconUri="/images/appbar.save.rest.png"
Text="save" Click="SaveButton_Click"/>
<shell:ApplicationBarIconButton IconUri="/images/appbar.cancel.rest.png"
Text="cancel" Click="CancelButton_Click"/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
…
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="24,0,24,24"
Background="White">
<TextBox x:Name="UserNameTextBox" TextWrapping="Wrap"
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=Explicit}" … />
</Grid>
</Grid>
The ApplicationBar contains two ApplicationBarIconButtons that call event handlers when their respective Click event is raised. The event handlers simply invoke methods on the EditPageViewModel class. The TextBox uses a TwoWay binding to bind to the UserName property on the EditPageViewModel class.
The ViewModelLocator class connects the view to the view model by setting the view's DataContext to the value of the EditPageViewModel property in the ViewModelLocator class. The following code example shows the EditPageViewModel property.
private EditPageViewModel editPageViewModel;
…
public EditPageViewModel EditPageViewModel
{
get
{
if (editPageViewModel == null)
{
editPageViewModel = new EditPageViewModel
(
new ApplicationFrameNavigationService(((App)Application.Current)
.RootFrame),
new MessageBoxAdapter()
);
}
return editPageViewModel;
}
}
The code checks to see if the editPageViewModel field is null, and if it is, the EditPageViewModel property returns a new instance of the EditPageViewModel class and passes new instances of the ApplicationFrameNavigationService and MessageBoxAdapter classes into the EditPageViewModel constructor. This check for null ensures that a new instance of the EditPageViewModel class isn't created each time the EditPage view is navigated to.
The EditPageViewModel class uses instances of the INavigationService and IMessageBox interfaces. The following code example shows the constructor receiving these instances from the ViewModelLocator class.
private INavigationService navigationService;
private IMessageBox messageBox;
private readonly Uri pageUri;
…
public EditPageViewModel(INavigationService navigationService,
IMessageBox messageBox)
{
this.navigationService = navigationService;
this.messageBox = messageBox;
pageUri = new Uri("/EditPage.xaml", UriKind.Relative);
this.navigationService.Navigated += navigationService_Navigated;
this.navigationService.Navigating += navigationService_Navigating;
}
The constructor sets the navigationService and messageBox fields to the instances of the ApplicationFrameNavigationService and MessageBoxAdapter classes it receives from the ViewModelLocator class. Initializing the class in this way enables view model testability, as mock implementations of the ApplicationFrameNavigationService and MessageBoxAdapter classes can be passed into the constructor. The constructor then sets the pageUri field to EditPage.xaml and registers event handler methods to the Navigated and Navigating events of the instance of the ApplicationFrameNavigationService class.
When the user clicks the Save ApplicationBarIconButton on the EditPage view, the SaveName method in the EditPageViewModel is called. Similarly, when the user clicks the Cancel ApplicationBarIconButton on the EditPage view, the CancelEdit method in the EditPageViewModel is called. The following code example shows these methods, along with the event handler methods that are called when the Navigating and Navigated events are raised in the instance of the ApplicationFrameNavigationService class.
public void SaveName()
{
PhoneApplicationService.Current.State["username"] = UserName;
navigationService.GoBack();
}
public void CancelEdit()
{
navigationService.GoBack();
}
void navigationService_Navigating(object sender, NavigatingCancelEventArgs e)
{
if (pageUri == currentPageUri)
{
if (IsDataDirty)
{
var result = messageBox.Show(
"Are you sure you want to go back without saving the change?",
"Name has changed", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.Cancel)
{
e.Cancel = true;
}
}
}
}
void navigationService_Navigated(object sender, NavigationEventArgs e)
{
if (e.Uri == pageUri)
{
if (PhoneApplicationService.Current.State.ContainsKey("username"))
{
UserName = PhoneApplicationService.Current.State["username"].ToString();
}
}
currentPageUri = e.Uri;
}
The SaveName method simply saves the value of the UserName property to the application's state dictionary, and then calls the GoBack method from the instance of the ApplicationFrameNavigationService class in order to return the user to the previous page. Similarly, the CancelEdit method also returns the user to the previous page by calling the GoBack method from the instance of the ApplicationFrameNavigationService class.
The Navigating event occurs when a new navigation is requested, and so the navigationService_Navigating method, which handles this event, displays a message box to the user asking if she would like to save her changes, but only if the current page is the EditPage view and the IsDataDirty property is true. This ensures that if the user attempts to navigate away from the EditPage view, she is given the option to save the username.
The Navigated event occurs when the content that is being navigated to has been found, and so the navigationService_Navigated method, which handles this event, sets the UserName property by retrieving the username from the application's state dictionary, but only if the EditPage view is being navigated to. This ensures that when navigating to the EditPage view, the previously entered username is displayed.
Unit Testing the Application
The Microsoft.Practices.Phone.Testing project contains mock implementations of the Windows Phone 7.1 SDK adapter and facade classes contained in the Microsoft.Practices.Phone.Adapters project. These mocks were developed for general purpose use, with many of them having properties that accept delegates. Delegate accepting properties enable the execution of any desired behavior necessary for the unit test.
The following code example shows the MockNavigationService class.
public class MockNavigationService : INavigationService
{
public MockNavigationService()
{
this.NavigationMode = NavigationMode.Forward;
StopLoadingTestCallback = () => { };
GoBackTestCallback = () => { };
GoForwardTestCallback = () => { };
RemoveBackEntryTestCallback = () => null;
}
…
public NavigationMode NavigationMode { get; set; }
public Action StopLoadingTestCallback { get; set; }
public void StopLoading()
{
StopLoadingTestCallback();
}
public Action GoBackTestCallback { get; set; }
public void GoBack()
{
GoBackTestCallback();
}
public Action GoForwardTestCallback { get; set; }
public void GoForward()
{
GoForwardTestCallback();
}
public Func<JournalEntry> RemoveBackEntryTestCallback { get; set; }
public JournalEntry RemoveBackEntry()
{
return RemoveBackEntryTestCallback();
}
public bool Navigate(Uri source)
{
var handler = Navigating;
if (handler != null)
handler(null, new NavigatingCancelEventArgs(source, NavigationMode));
var handler2 = Navigated;
if (handler2 != null)
handler2(null, new NavigationEventArgs(null, source));
return true;
}
…
public event NavigatedEventHandler Navigated;
public event NavigatingCancelEventHandler Navigating;
…
}
The MockNavigationService class implements the INavigationService interface and is an example of how a general-purpose mock can be given behavior using delegates. The StopLoadingTestCallback, GoBackTestCallback, GoForwardTestCallback, and RemoveBackEntryTestCallback properties can be set with a delegate or a lambda expression so that calls to the StopLoading, GoBack,GoForward, and RemoveBackEntry methods can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock. In addition, the Navigate method executes the event handlers that are registered to the Navigating and Navigated events.
Unit tests can then be written that use the mocks to test aspects of the view model business logic. The following code example shows the LoadUserNameWhenNavigatedTo test method, which follows the standard arrange, act, assert pattern. The unit test validates that the MainPageViewModel instance will load a username when the MainPage view is navigated to.
[TestMethod]
public void LoadUserNameWhenNavigatedTo()
{
var userName = "Benny";
var uri = new Uri("/MainPage.xaml", UriKind.Relative);
PhoneApplicationService.Current.State["username"] = userName;
var navigationService = new MockNavigationService();
var target = new MainPageViewModel(navigationService);
navigationService.Navigate(uri);
Assert.AreEqual(userName, target.UserName);
}
The test method first creates a username and a Uri object that contains the Uri of the MainPage view. It then stores the username in the application's state dictionary before creating an instance of the MockNavigationService class. The test method then creates an instance of the MainPageViewModel class, passing the instance of the MockNavigationService to the MainPageViewModel constructor. The Navigate method of the instance of the MockNavigationService class is then called, and in turn calls the navigationService_Navigated method in the instance of the MainPageViewModel class. This is because the MainPageViewModel constructor registers the navigationService_Navigated event handler method to the Navigated event of the INavigationService instance passed into the constructor, which for the test method is the MockNavigationService instance. The navigationService_Navigated method retrieves the username from the application's state dictionary and stores it in the UserName property of the instance of the MainPageViewModel class. Finally, the test method validates that the username field is identical to the UserName property of the instance of the MainPageViewModel class.
The following code example shows the MockMessageBox class.
public class MockMessageBox : IMessageBox
{
public string MessageBoxText { get; set; }
public MessageBoxResult MessageBoxResult { get; set; }
public bool ShowCalled { get; set; }
public MessageBoxResult Show(string messageBoxText)
{
ShowCalled = true;
MessageBoxText = messageBoxText;
return MessageBoxResult;
}
public MessageBoxResult Show(string messageBoxText, string caption,
MessageBoxButton button)
{
ShowCalled = true;
MessageBoxText = messageBoxText;
return MessageBoxResult;
}
}
The MockMessageBox class implements the IMessageBox interface, with the pattern being that the Show methods set the ShowCalled property to true to indicate that a Show method has been called. The ShowCalled property can then be queried from outside the mock.
Unit tests can then be written that use the mock to test aspects of the view model business logic. The following code example shows the MessageBoxShownWhenDataIsDirty test method, which follows the standard arrange, act, assert pattern. The unit test validates that a message box will be displayed to the user because the EditPageViewModelUserName property is not identical to the username stored in the application's state dictionary.
[TestMethod]
public void MessageBoxShownWhenDataIsDirty()
{
var enteredUserName = "Benny";
var savedUserName = "";
var editUri = new Uri("/EditPage.xaml", UriKind.Relative);
var mainUri = new Uri("/MainPage.xaml", UriKind.Relative);
var navigationService = new MockNavigationService();
var messageBox = new MockMessageBox();
var target = new EditPageViewModel(navigationService, messageBox);
PhoneApplicationService.Current.State["username"] = savedUserName;
navigationService.Navigate(editUri);
target.UserName = enteredUserName;
navigationService.Navigate(mainUri);
Assert.IsTrue(messageBox.ShowCalled);
}
The test method creates two non-identical usernames and Uri objects representing the MainPage and EditPage views. It then creates instances of the MockNavigationService class and the MockMessageBox class. The test method then creates an instance of the EditPageViewModel class, passing in the instances of the two mocks to the EditPageViewModel constructor. It then stores one of the usernames in the application's state dictionary. The instance of the MockNavigationService is then used to navigate to the EditPage view before the UserName property of the instance of the EditPageViewModel class is set to a username. The instance of the MockNavigationService is then used to navigate to the MainPage view. This navigation causes the navigationService_Navigating event handler in the instance of the EditPageViewModel class to be called, which in turn calls the Show method on the instance of the MockMessageBox class. Finally, the test method validates that the ShowCalled property of the instance of the MockMessageBox class is true.
Summary
This sample application has demonstrated how to build a testable Windows Phone application that performs navigation and displays a message to the user. The ApplicationFrameNavigationService class provides a facade over the NavigationService SDK class, and the MessageBoxAdapter class adapts the MessageBox SDK class. The application then consumes the adapter and facade classes. The MockNavigationService, and MockMessageBox classes are used by unit tests to test business logic contained in the view model classes.
Next Topic | Previous Topic | Home
Last built: February 10, 2012