Mobile Corner
States, Navigation and Testing with Portable Class Libraries for Windows Phone
How to manage visual states, and perform navigation and testing of your view models from within a Windows Phone PCL.
- By Nick Randolph
- 09/11/2013
There's been a lot of attention over the last year or so around building applications across multiple platforms. This may be across multiple mobile platforms, such as using the Xamarin tools to target Windows Phone, iOS and Android, catering for both Windows Store apps and Windows Phone apps in the Microsoft ecosystem, or even simply to code for both Windows Phone 7.x and Windows Phone 8. One of the notable contributions to solving this problem has come from Microsoft with its iterative release of the Portable Class Library (PCL) project type. The significance of this project type is that it builds on the common aspects across the different target platforms. By electing to include or exclude different platforms (or different versions of platforms), you can determine the right balance between cross-platform support and leveraging new platform features as they become available. In addition, Microsoft has been releasing a number of libraries via NuGet to help extend the older platforms, making features such as the async/await pattern available without having to sacrifice platform support. In this article I'll take a look at how you can separate your view models out of your main application into a PCL. This enforces a clear separation between the UI (your pages or user controls) and the logic contained within your view models. I'll examine the issues this introduces around navigation and visual states, and how you can overcome those issues. Finally, you'll see how you can leverage the unit-testing capability of Visual Studio to write tests against your view models.
I'll begin by getting my basic solution structure set up. In this case I'll create three projects: a Windows Phone app, a PCL and a Windows Phone Unit Test App. In actual fact, because the bulk of the logic is contained within the PCL, it can be tested using any of the unit-testing project types. I'll use the Windows Phone Unit Test App to ensure I'm running the tests on the platform for which I'm developing.
In the Windows Phone app you need to have two pages. Create both of them in a Pages folder: MainPage.xaml and SecondPage.xaml. You'll need to update the startup page by modifying the Navigation Page property in the WMAppManifest.xml file (using the manifest editor in Visual Studio) to "Pages/MainPage.xaml." In the PCL you need to create a folder called ViewModels containing classes MainViewModel and SecondViewModel. These classes represent the view models for MainPage and SecondPage, respectively. The view models can be linked to their associated page in the XAML for the page:
<phone:PhoneApplicationPage.Resources>
<viewModels:MainViewModel x:Key="ViewModel" />
</phone:PhoneApplicationPage.Resources>
<phone:PhoneApplicationPage.DataContext>
<Binding Source="{StaticResource ViewModel}" />
</phone:PhoneApplicationPage.DataContext>
This XAML creates an instance of the MainViewModel in the Resources dictionary of the page. It's then assigned as the DataContext for the page. By setting the view model as the DataContext of the page, elements on the page can be data-bound to properties exposed by the view model.
Initially it might seem that this mechanism works well, because the application logic can all be encased within the view model, with any updates being rendered on the page through data binding to properties on the view model (this assumes the view model implements INotifyPropertyChanged and that each data-bound property raises the PropertyChanged event when it changes). However, issues arise when the code in the view model needs to navigate the application to a new page. The PCL doesn't know or understand the specifics of the Windows Phone navigation system, and thus doesn't provide a mechanism for the view model to initiate a page navigation. Similarly, if the view model wants to change visual state of the page, it has no reference to the VisualStateManager or the page it's data bound to, so it can't invoke the state change.
While these two issues appear similar, I'll take different approaches to solve them. In order for the view models to be able to invoke a navigation, they need to be able to invoke the Navigate method, which resides on the Frame of the application (or on the NavigationService property of a Page). In my PCL I'll define an interface, INavigation, which includes a Navigate method. Rather than accepting a string or Uri of the page to navigate to, the Navigate method accepts a single type parameter. This will be the type of the view model that should be navigated to. Of course, this view model should be one that corresponds to a page in the application:
public interface INavigation
{
void Navigate<T>();
}
In addition, I'll also define an interface, INavigateViewModel, which defines a property, Navigator:
public interface INavigateViewModel
{
INavigation Navigator { get; set; }
}
Each view model will need to implement this interface so that it has a reference to an INavigation object. The INavigation implementation will be used by the view model in order to navigate to a different view model (and thus a different page). To prevent repetition in each of my view models, I can define a base view model class from which all view models can inherit:
public class BaseViewModel : INavigateViewModel
{
public INavigation Navigator { get; set; }
}
public class MainViewModel : BaseViewModel
{
...
}
Within the Windows Phone app, I define an implementation for the INavigation interface. This implementation simply calls the Navigate method on the Frame of the application to a Uri made up of the name of the type parameter supplied (I'll come back and discuss how this will work):
public class Navigator : INavigation
{
public void Navigate<T>()
{
var vm = new Uri("/" + typeof(T).Name, UriKind.Relative);
(Application.Current.RootVisual as Frame).Navigate(vm);
}
}
An instance of the Navigator will need to be set as the Navigator property on each view model. Rather than create a new instance of the Navigator for each page, it makes sense to have one instance created at the application layer, which all view models can reference. There are all manner of dependency injection frameworks that can be used to solve this problem. However, a simple solution that has no reliance on any third-party framework is to create a singleton object at the application level, which can be referenced in order to generate view models for each page. As part of generating the view models, properties such as the Navigator can be set. This object is often referred to as a view model locator. You can see in the code in Listing 1 that there are properties for each view model type, and a CreateViewModel method that's used to create an instance of the specified view model type. This method is also responsible for creating an instance of the Navigator (if it doesn't already exist) and assigning it to the Navigator property on the newly created view model.
Listing 1. A view model Locator.
public class Locator
{
public INavigation Navigator { get; set; }
private TViewModel CreateViewModel<TViewModel>()
where TViewModel : INavigateViewModel, new(){
if (Navigator == null) {
Navigator=new Navigator();
}
return new TViewModel {Navigator = Navigator};
}
public MainViewModel Main {
Get {
return CreateViewModel<MainViewModel>();
}
}
public SecondViewModel Second {
get {
return CreateViewModel<SecondViewModel>();
}
}
}
To make the single instance of the Locator accessible anywhere within the application, it's created in XAML within the resources section of App.xaml. By creating the Locator instance in the Application Resource dictionary, you can reference it in code, or directly in XAML, anywhere in the application:
<Application.Resources>
<statesAndNavigation:Locator x:Key="Locator"/>
<uriMapper:UriMapper x:Key="UriMapper">
<uriMapper:UriMapping Uri="/{page}ViewModel" MappedUri="/Pages/{page}Page.xaml" />
</uriMapper:UriMapper>
</Application.Resources>
You'll notice that I've also added a UriMapper as an application resource. This will handle the automatic translation from a navigation path -- based on the type of view model being navigated to -- into the name of the corresponding page. For example, /MainViewModel will be transposed into /Pages/MainPage.xaml. In order for this UriMapper to be used by the navigation system, it needs to be set on the PhoneApplicationFrame when it's created in App.xaml.cs:
RootFrame = new PhoneApplicationFrame {UriMapper = Resources["UriMapper"] as UriMapper};
The last change I need to make is within each page: I need to replace the direct creation of an instance of a view model with a binding to the appropriate property on the Locator object. For example, in the MainPage, I'd remove the instance of the MainViewModel I created as a page resource, and I'd set the page DataContext as follows:
DataContext="{Binding Main, Source={StaticResource Locator}}"
The DataContext for subsequent pages follows a similar pattern, with the path of Main being replaced with name of the property corresponding to the view model that's being requested (for instance, Second for SecondViewModel).
I've demonstrated how view models not being able to call out directly to the Windows Phone navigation system has been solved by creating an interface that wraps the underlying Navigate method. Changing the visual state of the page can be solved in a different way. In Listing 2, an event is exposed on the view model called StateChanged. I define this method on the BaseViewModel class to make it accessible to all view models.
Listing 2. Exposing the StateChanged event.
public class BaseViewModel : INavigateViewModel
{
public INavigation Navigator { get; set; }
public event EventHandler<StateChangedEventArgs> StateChanged;
protected void OnStateChanged<T>(T state, bool useTransitions = true) where T:struct
{
if (StateChanged != null)
{
StateChanged(this, new StateChangedEventArgs{
StateName = state.ToString(),
UseTransitions = useTransitions});
}
}
}
public class StateChangedEventArgs : EventArgs
{
public string StateName { get; set; }
public bool UseTransitions { get; set; }
}
What's interesting about the OnStateChanged method (used to invoke the StateChanged event) is that it doesn't take a string argument, which would be used to specify to which state to transition. Instead it specifies a Type argument, which has to be a struct. The intended behavior when using this method is that the Type argument is an enum, where the enum values correspond to the visual states defined in the XAML. The XAML in Listing 3 defines three visual states for the loading of data on a page: Loading, Loaded and NotAbleToLoad.
Listing 3. Three visual states for the loading of data on a page.
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LoadingStates">
<VisualState x:Name="Loading">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="TB_Loading">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Loaded">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="TB_Loaded">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="NotAbleToLoad">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="TB_NotAbleToLoad">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
In the MainViewModel I define an enumeration called LoadingStates (note that this doesn't have to match the state group name in the XAML) with values that correspond to the visual states. Keeping the enumeration values in sync with the names of the visual states is a manual step, but it helps eliminate the use of string literals throughout the view model logic.
Listing 4 illustrates some sample code that changes to the Loading state initially, runs some long-running task, and then either changes to Loaded or, in the case of an error, changes to NotAbleToLoad.
Listing 4. Illustrating the Loading, Loaded and NotAbleToLoad states.
public class MainViewModel : BaseViewModel
{
private enum LoadingStates
{
Loading,
Loaded,
NotAbleToLoad
}
public async Task LoadViewModel()
{
try
{
// Change to loading state to indicate to user something is happening
OnStateChanged(LoadingStates.Loading);
// Do some loading tasks, which may take a few seconds to complete
await Task.Factory.StartNew(() =>
{
var waiter = new ManualResetEvent(false);
waiter.WaitOne(5000);
});
// Change to loaded state, which should show the data loaded
OnStateChanged(LoadingStates.Loaded);
}
catch (Exception ex)
{
// Error in loading data, so display notice about being unable to load data
Debug.WriteLine(ex.Message);
OnStateChanged(LoadingStates.NotAbleToLoad);
}
}
public void NavigateToDifferentPage()
{
Navigator.Navigate<SecondViewModel>();
}
}
When each page is created, it's necessary for the page to wire up an event handler to the StateChanged event handler on its corresponding view model. Within the event handler is a call to the VisualStateManager's GoToState method. This will invoke the appropriate state change, with or without transitions, as shown in Listing 5.
Listing 5. Invoking a state change.
public MainPage()
{
InitializeComponent();
ViewModel.StateChanged += ViewModelStateChanged;
}
private void ViewModelStateChanged(object sender, StateChangedEventArgs e)
{
VisualStateManager.GoToState(this, e.StateName, e.UseTransitions);
}
private BaseViewModel ViewModel
{
get
{
return DataContext as BaseViewModel;
}
}
So far you've seen how you can resolve issues around navigation and state changes for the view models that reside in the PCL. This hasn't really addressed the issue of why you want to separate your UI from your view models (which encapsulate the logic of your application). One reason is to aid in making your code testable. There are a number of ways to test an application. Visual Studio offers Windows Phone users the ability to create and run test cases. The testing project is based on the Windows Phone Unit Test App project template, and as such includes both tests and test harness in the same project. In this case I simply want to illustrate that you can write test cases that demonstrate the navigation and state changes invoked by the MainViewModel, as shown in Listing 6.
Listing 6. Test cases demonstrating navigation and state changes.
[TestClass]
public class MainViewModelTests
{
[TestMethod]
public async Task TestLoadViewModel()
{
var vm = new MainViewModel();
var states = new List<string>();
vm.StateChanged+=(s,e) => states.Add(e.StateName);
await vm.LoadViewModel();
Assert.AreEqual(2,states.Count);
Assert.AreEqual("Loading",states[0]);
Assert.AreEqual("Loaded", states[1]);
}
[TestMethod]
public void TestNavigateToDifferentPage()
{
var vm = new MainViewModel();
var navigated = false;
vm.Navigator = new TestNavigator
{
Callback = (type) =>
{
navigated = true;
Assert.AreEqual(type,typeof (SecondViewModel));
}
};
vm.NavigateToDifferentPage();
Assert.IsTrue(navigated);
}
}
public class TestNavigator : INavigation
{
public Action<Type> Callback { get; set; }
public void Navigate<T>()
{
Callback(typeof (T));
}
}
In this article you've seen how you can invoke navigation and visual state changes from view models located within a PCL. A clear separation of the logic of your application allows it to be easily reused across target platforms by simply referencing the PCL. It also makes it easier to test your application logic, as there's no direct dependency on the layout of the application.