Enterprise App Navigation
Note
This eBook was published in the spring of 2017, and has not been updated since then. There is much in the book that remains valuable, but some of the material is outdated.
Xamarin.Forms includes support for page navigation, which typically results from the user's interaction with the UI or from the app itself as a result of internal logic-driven state changes. However, navigation can be complex to implement in apps that use the Model-View-ViewModel (MVVM) pattern, as the following challenges must be met:
- How to identify the view to be navigated to, using an approach that does not introduce tight coupling and dependencies between views.
- How to coordinate the process by which the view to be navigated to is instantiated and initialized. When using MVVM, the view and view model need to be instantiated and associated with each other via the view's binding context. When an app is using a dependency injection container, the instantiation of views and view models might require a specific construction mechanism.
- Whether to perform view-first navigation, or view model-first navigation. With view-first navigation, the page to navigate to refers to the name of the view type. During navigation, the specified view is instantiated, along with its corresponding view model and other dependent services. An alternative approach is to use view model-first navigation, where the page to navigate to refers to the name of the view model type.
- How to cleanly separate the navigational behavior of the app across the views and view models. The MVVM pattern provides a separation between the app's UI and its presentation and business logic. However, the navigation behavior of an app will often span the UI and presentations parts of the app. The user will often initiate navigation from a view, and the view will be replaced as a result of the navigation. However, navigation might often also need to be initiated or coordinated from within the view model.
- How to pass parameters during navigation for initialization purposes. For example, if the user navigates to a view to update order details, the order data will have to be passed to the view so that it can display the correct data.
- How to co-ordinate navigation, to ensure that certain business rules are obeyed. For example, users might be prompted before navigating away from a view so that they can correct any invalid data or be prompted to submit or discard any data changes that were made within the view.
This chapter addresses these challenges by presenting a NavigationService
class that's used to perform view model-first page navigation.
Note
The NavigationService
used by the app is designed only to perform hierarchical navigation between ContentPage instances. Using the service to navigate between other page types might result in unexpected behavior.
Navigating Between Pages
Navigation logic can reside in a view's code-behind, or in a data bound view model. While placing navigation logic in a view might be the simplest approach, it is not easily testable through unit tests. Placing navigation logic in view model classes means that the logic can be exercised through unit tests. In addition, the view model can then implement logic to control navigation to ensure that certain business rules are enforced. For example, an app might not allow the user to navigate away from a page without first ensuring that the entered data is valid.
A NavigationService
class is typically invoked from view models, to promote testability. However, navigating to views from view models would require the view models to reference views, and particularly views that the active view model isn't associated with, which is not recommended. Therefore, the NavigationService
presented here specifies the view model type as the target to navigate to.
The eShopOnContainers mobile app uses the NavigationService
class to provide view model-first navigation. This class implements the INavigationService
interface, which is shown in the following code example:
public interface INavigationService
{
ViewModelBase PreviousPageViewModel { get; }
Task InitializeAsync();
Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase;
Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;
Task RemoveLastFromBackStackAsync();
Task RemoveBackStackAsync();
}
This interface specifies that an implementing class must provide the following methods:
Method | Purpose |
---|---|
InitializeAsync |
Performs navigation to one of two pages when the app is launched. |
NavigateToAsync |
Performs hierarchical navigation to a specified page. |
NavigateToAsync(parameter) |
Performs hierarchical navigation to a specified page, passing a parameter. |
RemoveLastFromBackStackAsync |
Removes the previous page from the navigation stack. |
RemoveBackStackAsync |
Removes all the previous pages from the navigation stack. |
In addition, the INavigationService
interface specifies that an implementing class must provide a PreviousPageViewModel
property. This property returns the view model type associated with the previous page in the navigation stack.
Note
An INavigationService
interface would usually also specify a GoBackAsync
method, which is used to programmatically return to the previous page in the navigation stack. However, this method is missing from the eShopOnContainers mobile app because it's not required.
Creating the NavigationService Instance
The NavigationService
class, which implements the INavigationService
interface, is registered as a singleton with the Autofac dependency injection container, as demonstrated in the following code example:
builder.RegisterType<NavigationService>().As<INavigationService>().SingleInstance();
The INavigationService
interface is resolved in the ViewModelBase
class constructor, as demonstrated in the following code example:
NavigationService = ViewModelLocator.Resolve<INavigationService>();
This returns a reference to the NavigationService
object that's stored in the Autofac dependency injection container, which is created by the InitNavigation
method in the App
class. For more information, see Navigating When the App is Launched.
The ViewModelBase
class stores the NavigationService
instance in a NavigationService
property, of type INavigationService
. Therefore, all view model classes, which derive from the ViewModelBase
class, can use the NavigationService
property to access the methods specified by the INavigationService
interface. This avoids the overhead of injecting the NavigationService
object from the Autofac dependency injection container into each view model class.
Handling Navigation Requests
Xamarin.Forms provides the NavigationPage
class, which implements a hierarchical navigation experience in which the user is able to navigate through pages, forwards and backwards, as desired. For more information about hierarchical navigation, see Hierarchical Navigation.
Rather than use the NavigationPage
class directly, the eShopOnContainers app wraps the NavigationPage
class in the CustomNavigationView
class, as shown in the following code example:
public partial class CustomNavigationView : NavigationPage
{
public CustomNavigationView() : base()
{
InitializeComponent();
}
public CustomNavigationView(Page root) : base(root)
{
InitializeComponent();
}
}
The purpose of this wrapping is for ease of styling the NavigationPage
instance inside the XAML file for the class.
Navigation is performed inside view model classes by invoking one of the NavigateToAsync
methods, specifying the view model type for the page being navigated to, as demonstrated in the following code example:
await NavigationService.NavigateToAsync<MainViewModel>();
The following code example shows the NavigateToAsync
methods provided by the NavigationService
class:
public Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase
{
return InternalNavigateToAsync(typeof(TViewModel), null);
}
public Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase
{
return InternalNavigateToAsync(typeof(TViewModel), parameter);
}
Each method allows any view model class that derives from the ViewModelBase
class to perform hierarchical navigation by invoking the InternalNavigateToAsync
method. In addition, the second NavigateToAsync
method enables navigation data to be specified as an argument that's passed to the view model being navigated to, where it's typically used to perform initialization. For more information, see Passing Parameters During Navigation.
The InternalNavigateToAsync
method executes the navigation request, and is shown in the following code example:
private async Task InternalNavigateToAsync(Type viewModelType, object parameter)
{
Page page = CreatePage(viewModelType, parameter);
if (page is LoginView)
{
Application.Current.MainPage = new CustomNavigationView(page);
}
else
{
var navigationPage = Application.Current.MainPage as CustomNavigationView;
if (navigationPage != null)
{
await navigationPage.PushAsync(page);
}
else
{
Application.Current.MainPage = new CustomNavigationView(page);
}
}
await (page.BindingContext as ViewModelBase).InitializeAsync(parameter);
}
private Type GetPageTypeForViewModel(Type viewModelType)
{
var viewName = viewModelType.FullName.Replace("Model", string.Empty);
var viewModelAssemblyName = viewModelType.GetTypeInfo().Assembly.FullName;
var viewAssemblyName = string.Format(
CultureInfo.InvariantCulture, "{0}, {1}", viewName, viewModelAssemblyName);
var viewType = Type.GetType(viewAssemblyName);
return viewType;
}
private Page CreatePage(Type viewModelType, object parameter)
{
Type pageType = GetPageTypeForViewModel(viewModelType);
if (pageType == null)
{
throw new Exception($"Cannot locate page type for {viewModelType}");
}
Page page = Activator.CreateInstance(pageType) as Page;
return page;
}
The InternalNavigateToAsync
method performs navigation to a view model by first calling the CreatePage
method. This method locates the view that corresponds to the specified view model type, and creates and returns an instance of this view type. Locating the view that corresponds to the view model type uses a convention-based approach, which assumes that:
- Views are in the same assembly as view model types.
- Views are in a .Views child namespace.
- View models are in a .ViewModels child namespace.
- View names correspond to view model names, with "Model" removed.
When a view is instantiated, it's associated with its corresponding view model. For more information about how this occurs, see Automatically Creating a View Model with a View Model Locator.
If the view being created is a LoginView
, it's wrapped inside a new instance of the CustomNavigationView
class and assigned to the Application.Current.MainPage
property. Otherwise, the CustomNavigationView
instance is retrieved, and provided that it isn't null, the PushAsync
method is invoked to push the view being created onto the navigation stack. However, If the retrieved CustomNavigationView
instance is null
, the view being created is wrapped inside a new instance of the CustomNavigationView
class and assigned to the Application.Current.MainPage
property. This mechanism ensures that during navigation, pages are added correctly to the navigation stack both when it's empty, and when it contains data.
Tip
Consider caching pages. Page caching results in memory consumption for views that are not currently displayed. However, without page caching it does mean that XAML parsing and construction of the page and its view model will occur every time a new page is navigated to, which can have a performance impact for a complex page. For a well-designed page that does not use an excessive number of controls, the performance should be sufficient. However, page caching might help if slow page loading times are encountered.
After the view is created and navigated to, the InitializeAsync
method of the view's associated view model is executed. For more information, see Passing Parameters During Navigation.
Navigating When the App is Launched
When the app is launched, the InitNavigation
method in the App
class is invoked. The following code example shows this method:
private Task InitNavigation()
{
var navigationService = ViewModelLocator.Resolve<INavigationService>();
return navigationService.InitializeAsync();
}
The method creates a new NavigationService
object in the Autofac dependency injection container, and returns a reference to it, before invoking its InitializeAsync
method.
Note
When the INavigationService
interface is resolved by the ViewModelBase
class, the container returns a reference to the NavigationService
object that was created when the InitNavigation method is invoked.
The following code example shows the NavigationService
InitializeAsync
method:
public Task InitializeAsync()
{
if (string.IsNullOrEmpty(Settings.AuthAccessToken))
return NavigateToAsync<LoginViewModel>();
else
return NavigateToAsync<MainViewModel>();
}
The MainView
is navigated to if the app has a cached access token, which is used for authentication. Otherwise, the LoginView
is navigated to.
For more information about the Autofac dependency injection container, see Introduction to Dependency Injection.
Passing Parameters During Navigation
One of the NavigateToAsync
methods, specified by the INavigationService
interface, enables navigation data to be specified as an argument that's passed to the view model being navigated to, where it's typically used to perform initialization.
For example, the ProfileViewModel
class contains an OrderDetailCommand
that's executed when the user selects an order on the ProfileView
page. In turn, this executes the OrderDetailAsync
method, which is shown in the following code example:
private async Task OrderDetailAsync(Order order)
{
await NavigationService.NavigateToAsync<OrderDetailViewModel>(order);
}
This method invokes navigation to the OrderDetailViewModel
, passing an Order
instance that represents the order that the user selected on the ProfileView
page. When the NavigationService
class creates the OrderDetailView
, the OrderDetailViewModel
class is instantiated and assigned to the view's BindingContext
. After navigating to the OrderDetailView
, the InternalNavigateToAsync
method executes the InitializeAsync
method of the view's associated view model.
The InitializeAsync
method is defined in the ViewModelBase
class as a method that can be overridden. This method specifies an object
argument that represents the data to be passed to a view model during a navigation operation. Therefore, view model classes that want to receive data from a navigation operation provide their own implementation of the InitializeAsync
method to perform the required initialization. The following code example shows the InitializeAsync
method from the OrderDetailViewModel
class:
public override async Task InitializeAsync(object navigationData)
{
if (navigationData is Order)
{
...
Order = await _ordersService.GetOrderAsync(
Convert.ToInt32(order.OrderNumber), authToken);
...
}
}
This method retrieves the Order
instance that was passed into the view model during the navigation operation, and uses it to retrieve the full order details from the OrderService
instance.
Invoking Navigation using Behaviors
Navigation is usually triggered from a view by a user interaction. For example, the LoginView
performs navigation following successful authentication. The following code example shows how the navigation is invoked by a behavior:
<WebView ...>
<WebView.Behaviors>
<behaviors:EventToCommandBehavior
EventName="Navigating"
EventArgsConverter="{StaticResource WebNavigatingEventArgsConverter}"
Command="{Binding NavigateCommand}" />
</WebView.Behaviors>
</WebView>
At runtime, the EventToCommandBehavior
will respond to interaction with the WebView
. When the WebView
navigates to a web page, the Navigating
event will fire, which will execute the NavigateCommand
in the LoginViewModel
. By default, the event arguments for the event are passed to the command. This data is converted as it's passed between source and target by the converter specified in the EventArgsConverter
property, which returns the Url
from the WebNavigatingEventArgs
. Therefore, when the NavigationCommand
is executed, the Url of the web page is passed as a parameter to the registered Action
.
In turn, the NavigationCommand
executes the NavigateAsync
method, which is shown in the following code example:
private async Task NavigateAsync(string url)
{
...
await NavigationService.NavigateToAsync<MainViewModel>();
await NavigationService.RemoveLastFromBackStackAsync();
...
}
This method invokes navigation to the MainViewModel
, and following navigation, removes the LoginView
page from the navigation stack.
Confirming or Cancelling Navigation
An app might need to interact with the user during a navigation operation, so that the user can confirm or cancel navigation. This might be necessary, for example, when the user attempts to navigate before having fully completed a data entry page. In this situation, an app should provide a notification that allows the user to navigate away from the page, or to cancel the navigation operation before it occurs. This can be achieved in a view model class by using the response from a notification to control whether or not navigation is invoked.
Summary
Xamarin.Forms includes support for page navigation, which typically results from the user's interaction with the UI, or from the app itself, as a result of internal logic-driven state changes. However, navigation can be complex to implement in apps that use the MVVM pattern.
This chapter presented a NavigationService
class, which is used to perform view model-first navigation from view models. Placing navigation logic in view model classes means that the logic can be exercised through automated tests. In addition, the view model can then implement logic to control navigation to ensure that certain business rules are enforced.