View Model First Navigation Part 2 - The Devil's In The Details
The previous post on view model first navigation within Xamarin.Forms showed how to create a navigation service that associated a view to view model, kept track of those associations, provided functions to navigate between view models, and had the ability to instantiate a view based on which view model was being navigated to.
In other words, it allowed navigation to a Xamarin.Forms.Page
just by having a view model say it wanted to show another view model.
That’s some pretty sweet stuff!
The only problem with the navigation service, as implemented in the previous post, is that it didn’t work with master detail layouts. Not so sweet after all. Not to worry though – we’re going to right the ship and update the service so we can handle master detail pages in addition to tab and plain ‘ol navigation pages!
Along the way we’re going to add some convenience functions to the navigation service and answer some common questions I have been getting about it. Before diving in too deep, if you haven’t already, please read the previous post on VM First navigation to get up to speed on what we’ll be talking about here.
You can find the full implementation of the view model first navigation library on my GitHub here.
Adding Master/Detail Navigation
I have to say, adding the functionality to navigate between different detail pages from a master page proved to be more tricky than I had originally thought it would be. I thought I’d only have to find the MasterDetailPage
as the Application.Current.MainPage
, load up the view corresponding to the view model I wanted to show, then set the DetailPage
property. Easy. Well, it is – kind of. The problem is there are a lot of little details that need to be accounted for along the way – the devil’s in the details, so to speak.
Detail Devil #1 – New Navigation Function
Since swapping out a detail page is not the same as PushAsync
or PopAsync
, we need to add a new function to the service’s interface with the definition being:
void SwitchDetailPage(BaseViewModel viewModel);
Easy enough – the implementation of that with our existing NavigationService
class then looks like this:
public void SwitchDetailPage(BaseViewModel viewModel)
{
var view = InstantiateView(viewModel);
Page newDetailPage;
// Tab pages shouldn't go into navigation pages
if (view is TabbedPage)
newDetailPage = (Page)view;
else
newDetailPage = new NavigationPage((Page)view);
DetailPage = newDetailPage;
}
There are a couple of interesting things going on in that function. The first is that we’re checking if the page we want to swap out is derived from a TabbedPage
. If it is not, then we’re going to wrap the page within a NavigationPage
– so we can get a nav bar on top for a heading and the familiar stack navigation. I can’t imagine a scenario where we’d want to have a TabbedPage
as a root of a navigation stack.
The second interesting thing is the DetailPage
property, whose implementation is:
// Because we're going to do a hard switch of the page, either return
// the detail page, or if that's null, then the current main page
Page DetailPage
{
get
{
var masterController = Application.Current.MainPage as MasterDetailPage;
return masterController?.Detail ?? Application.Current.MainPage;
}
set
{
var masterController = Application.Current.MainPage as MasterDetailPage;
if (masterController != null)
{
masterController.Detail = value;
masterController.IsPresented = false;
}
else
{
Application.Current.MainPage = value;
}
}
}
Now we’re starting to see some of the little details pop out that made implementing this a bit more involved than I originally thought. I did not want to have SwitchDetailPage
be tightly coupled to having a MasterDetailPage
in the UI. So when getting the DetailPage
, it checks to see if the app’s current main page is a MasterDetailPage
, and if so, returns its Detail
page. If not, it returns what ever is set as the app’s main page. The same thing, but in reverse, for set – if there is a MasterDetailPage
set in the UI – its Detail
page gets set. If there is not – the application’s root page gets replaced.
In other words – the SwitchDetailPage
needs to take into account what happens when a MasterDetailPage
is both present, and when it is not.
Detail Devil #2 – Obtaining The Proper Xamarin.Forms.INavigation Reference
Every single Page
object within Xamarin.Forms
has a reference to an INavigation
property. However, if you try to perform a navigation on one that isn’t valid (like doing a push onto a TabbedPage
that’s not part of a navigation stack) – an exception will occur. Since we don’t want our view models to know whether they’re hosted within a TabbedPage
, the Detail
of a MasterDetailPage
or just a regular navigation stack – our navigation service will need to abstract that away from them.
Within the navigation service, we need to come up with a hierarchy of checking the type of our app’s main page and then returning the appropriate INavigation
object before we do any navigation operations. That’s being done in the FormsNavigation
property, and it looks like this:
INavigation FormsNavigation
{
get
{
var tabController = Application.Current.MainPage as TabbedPage;
var masterController = Application.Current.MainPage as MasterDetailPage;
// First check to see if we're on a tabbed page, then master detail, finally go to overall fallback
return tabController?.CurrentPage?.Navigation ??
(masterController?.Detail as TabbedPage)?.CurrentPage?.Navigation ?? // special consideration for a tabbed page inside master/detail
masterController?.Detail?.Navigation ??
Application.Current.MainPage.Navigation;
}
}
It’s not as elegant as I’d like it – but it does the job. First it tries to see if the main page is a TabbedPage
or MasterDetailPage
. If it’s a TabbedPage
, it will return the INavigation
for the current page shown in one of the tabs.
However, things get weird if it’s a MasterDetailPage
… first thing we do is check whether or not the current displayed page is a TabbedPage
– then if so follow the routine as above. Otherwise return the INavigation
from the displayed Detail
page.
Finally, if it’s neither – just return the main’s navigation.
Alright – we’re making progress … we’re able to set the Detail property of the MasterDetailPage
and also get at the proper INavigation
reference. But how do we keep track of all the view models that make up the various detail pages?
Detail Devil #3 – Keeping Track
On to how we’re going to keep track of the various view models that can be navigated to from the view model that backs the MasterDetailPage.Master
. This one is actually pretty easy right? Create a custom model class that holds a description of the view model for potential display and then the type of the view model. Put that into a collection of some sort for binding – away we go.
But…
Remember the signature of the method which performs the detail navigation for us looks like:
void SwitchDetailPage(BaseViewModel viewModel);
Hmm… we’re sending it a fully instantiated view model, so if we’re to have a regular Type
property in this new model class, that leaves the door open to the type not being of a BaseViewModel
.
OK… so we’re going to need to constrain the view model type to be of BaseViewModel
within our new model class… and we’ll do that through a new interface and class. The interface will look like:
public interface IMasterListItem<out T> where T : BaseViewModel
{
}
And the implementation of that interface will look like:
public class MasterListItem<T> : IMasterListItem<T> where T : BaseViewModel
{
public string DisplayName { get; set; }
public MasterListItem(string displayName)
{
DisplayName = displayName;
}
}
By using a little covariance here we’re constraining our model class to always hold types derived from BaseViewModel
. So populating a list (that we’ll eventually bind to a ListView
) will look a bit like this:
AvailablePages = new List<IMasterListItem<BaseViewModel>>();
AvailablePages.Add(new MasterListItem<NormalOneViewModel>("Normal Nav"));
AvailablePages.Add(new MasterListItem<RootTabViewModel>("Tab Pages"));
Then, when we’re ready to perform some navigation, the code to do so will look like this:
// Get the view model type
var viewModelType = itemToNavigate.GetType().GenericTypeArguments[0];
// Get a view model instance
var viewModel = Activator.CreateInstance(viewModelType) as BaseViewModel;
// Perform the switch
_navService.SwitchDetailPage(viewModel);
itemToNavigate
above is a IMasterItem<BaseViewModel>
object. Generally that variable will come from a SelectedItem
property on a ListView
.
That’s It!
All in all, not too bad, right? There were quite a few little details that needed to be ironed out before we could implement the master/detail page view model first navigation – such as making sure TabbedPage
‘s don’t end up nested in a NavigationPage
, finding the correct INavigation
object to perform the actual navigation, and figuring out how to store view model types to provide easy access to them when navigating – but once worked out – the service works like a charm!
To view the service along with a working model – view the GitHub project here.
Bonus! Some Convenience Features
As long as I was in the navigation service, I added some minor convenience features to make it easier to use. The largest being the ability to send an action along with a PushAsync
or PushModalAsync
function. This way one could invoke a function within the target view model to perform some initialization. The implementation looks like the following:
public async Task PushAsync<T>(Action<T> initialize = null) where T : BaseViewModel
{
T viewModel;
// Instantiate the view model & invoke the initialize method, if any
viewModel = Activator.CreateInstance<T>();
initialize?.Invoke(viewModel);
await PushAsync(viewModel);
}
And invoking that function would look like:
await _navService.PushAsync<TabOneChildViewModel>(
(vm) => vm.InitializeDisplay("Title from initialization routine"));
Questions
Finally I wanted to answer some questions I have received since posting the original article on view model first navigation.
-
Why use Splat to locate the navigation service?
- The reason I’m using Splat here is that it provides an easy means to locate the navigation service – and it will keep the nav service in memory too. The built-in Xamarin.Forms dependency service would create it new each time it resolves it, which means we’d have to run through the initialization routine where it associates views to view models each time. Ideally a proper IoC container would be used and the nav service would be injected into the constructor of the view models instead of using a service locator.
-
Why do you “hard code” in views as starting points in the app’s main XAML?
- While one could work around this, I find it easier to be more explicit as to what is being displayed in the start by declaratively saying which views go where. Makes the code easier to understand for somebody else picking it up for the first time.