View Model First Navigation Part 2 - The Devil's In The Details

Hi! I released a NuGet package that does VM First Navigation! This article here still contains valuable info - but this other article has the necessary info to start using the VM First NuGet package.

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.