Navigation

This part of documentation covers some of the techniques available within MugenMvvm for navigating between viewmodels.

One of the most important feature of MugenMvvm navigation is that if the application is tombstoned and restored, it will be executed as usual.

Navigation in MugenMvvm is serviced by IViewModelPresenter interface. Presented below is declaration of this interface:

public interface IViewModelPresenter
    {
        [NotNull]
        ICollection<IDynamicViewModelPresenter> DynamicPresenters { get; }

        [NotNull]
        Task WaitCurrentNavigationsAsync(IDataContext context = null);

        [NotNull]
        IAsyncOperation ShowAsync([NotNull] IDataContext context);

        Task<bool> CloseAsync([NotNull] IDataContext context);

        void Restore([NotNull] IDataContext context);
    }
  • DynamicPresenters - the collection of IDynamicViewModelPresenter items. IDynamicViewModelPresenter has a Priority and can show itself by using TryShowAsync method. You can check interface definition here.
  • ShowAsync - this method make view model instance show. It is iterating over DynamicPresenters ordered by priority and invoking its TryShowAsync method in order to show view model.
  • Restore - wherever is needed to restore the state of view model (iOS, Android) this method will be invoked. As ShowAsync method this one is iterating over DynamicPresenters ordered by priority and trying to find appropriate IRestorableDynamicViewModelPresenter implementation.
  • WaitCurrentNavigationsAsync method that allows to wait current navigation if it active.

Regular Navigation

In order to show other view please follow the next algorithm:

  1. Declare RelayCommand in main view model.
  2. Bind it on Click event of control.
  3. Implement command method as follows:
using (var viewModel = GetViewModel<OtherViewModel>())
{
  await viewModel.ShowAsync();
}

Tabbed Navigation

Using of tabs is a little tricky, but just a little.
There are two ways to work with tabs in MugenMvvm.

The First Approach

In order to allow view model shows multiple viewmodel one have to follow the next algorithm:

  1. Add some tab ItemsSource property in ViewModel.
  2. Bind this collection to ItemsSource of tab control.
  3. Then add new tabs and they will appear on tab control.

Let's look at example. As platform we going to use Android, all actions are similar on all platforms.

Suppose you have MugenMvvm Android application tuned in right way. Please follow Getting Started] section in order to get info how to start development with MugenMvvm.

Let's add ActionBar on main screen and add some tabs into it. First of all, let's create tab viewmodel with Number property in order to easily distinguish the tabs.

public class ChildViewModel : ViewModelBase
{
  public int Number { get; private set; }

  public void Init(int number)
  {
    Number = number;
  }
}

Then inherit your view model on MultiViewModel. Then override OnInitialized method and add the next code:

public class MainViewModel : MultiViewModel
{
  #region Overrides of ViewModelBase

  protected override void OnInitialized()
  {
    base.OnInitialized();
    for (var i = 0; i < 5; i++)
    {
      var vm = GetViewModel<ChildViewModel>();
      vm.Init(i + 1);
      AddViewModel(vm, false);
    }

    // set selected vm
    SelectedItem = ItemsSource[0];
  }

  #endregion
}

We are adding five tabs in OnInitialized method and initialize them by appropriate number. Then we are setting SelectedItem as first viewmodel in ItemsSource.

Let's write our views. Main.axml have to be equal to follow:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:pkg="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
  <ActionBar android:layout_height="wrap_content"
          android:layout_width="wrap_content"
          pkg:ActionBarTemplate="@layout/_actionbarmain"
          pkg:Bind="ItemsSource ItemsSource; SelectedItem SelectedItem, Mode=TwoWay" />
</LinearLayout>

Now add _ActionBarMain.xml.

<?xml version="1.0" encoding="utf-8"?>

<ActionBar NavigationMode="Tabs" 
           ItemsSource="{ItemsSource}" SelectedItem="{SelectedItem, Mode=TwoWay}">
  <TabTemplate Content="{}" Text="{'Tab ' +  $context.Number}" />
</ActionBar>

It is ready. Whenever you run application you have to see something similar to next screenshot:

715715

Binded tabs

In order to get better understanding refer on the link and watch the YouTube demonstration of this example.
Also you can get the sources of this example from GitHub.

The Second Approach

In order to allow view model shows multiple viewmodel you have to follow the next algorithm:

  1. Get reference on current IViewModelPresenter (easy to implement with usage of DI).
  2. Register your viewmodel as DynamicMultiViewModelPresenter by using IViewModelPresenter.DynamicPresenters.
  3. On disposing remove it.

Let's look at example. As platform we going to use Android, all actions are similar on all platforms.

Suppose you have MugenMvvm Android application tuned in right way. Please follow Getting Started] section in order to get info how start development with MugenMvvm.

Let's add ActionBar on main screen and add some tabs into it. First of all, let's create tab viewmodel with Number property in order to easily distinguish the tabs.

internal class ChildViewModel : ViewModelBase
{
  public string Number { get; private set; }

  public void Init(string number)
  {
    Number = number;
  }
}

After that, inherit your view model on MultiViewModel. Then override OnInitialized method and add the next code:

public class MainViewModel : MultiViewModel
 {
   private readonly DynamicMultiViewModelPresenter _presenter;
   private readonly IViewModelPresenter _viewModelPresenter;
   private int _number;

   public MainViewModel(IViewModelPresenter viewModelPresenter)
   {
     _viewModelPresenter = viewModelPresenter;
     _presenter = new DynamicMultiViewModelPresenter(this);
     _viewModelPresenter.DynamicPresenters.Add(_presenter);
     AddNewTabCommand = RelayCommandBase.FromAsyncHandler(AddNewTab);
   }

   public ICommand AddNewTabCommand { get; private set; }

   private async Task AddNewTab()
   {
     using (var vm = GetViewModel<ChildViewModel>())
     {
       vm.Init("#" + ++_number);
       var operation = vm.ShowAsync();

       //wait until the view model is displayed.
       await operation.NavigationCompletedTask;

       //wait until the view model is closed.
       await operation;
     }
   }

   #region Overrides of ViewModelBase

   protected override void OnDispose(bool disposing)
   {
     if (disposing)
       _viewModelPresenter.DynamicPresenters.Remove(_presenter);
     base.OnDispose(disposing);
   }

   #endregion
 }

In the constructor we are getting IViewModelPresenter and register our viewmodel presenter for multiple viewmodels. Also, we are overriding OnDispose in order to remove our presenter when activity will be disposed.

Let's write our views. Main.axml have to be equal to follow markup:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:pkg="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
  <OptionsMenu
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    pkg:MenuTemplate="@menu/mainmenu" />
  <ActionBar android:layout_height="wrap_content"
          android:layout_width="wrap_content"
          pkg:ActionBarTemplate="@layout/_actionbarmain"
          pkg:Bind="ItemsSource ItemsSource; SelectedItem SelectedItem, Mode=TwoWay" />
</LinearLayout>

Then add ActionBar and Menu templates:

<?xml version="1.0" encoding="utf-8"?>

<ActionBar NavigationMode="Tabs" 
           ItemsSource="{ItemsSource}" SelectedItem="{SelectedItem, Mode=TwoWay}">
  <TabTemplate Content="{}" Text="{'Tab ' +  $context.Number}" />
</ActionBar>
<?xml version="1.0" encoding="utf-8"?>

<Menu>
  <MenuItem Title="Add new tab" Click="{AddNewTabCommand}" ShowAsAction="never" />
</Menu>

It is ready. Whenever you run application you have to see something similar to next gif:

651651

Add tabs dynamically.

In order to get better understnading refer on the link and watch the YouTube demonstration of this example.
Also you can get the sources of this example from GitHub.

Master/Detail Navigation (Custom Presenter)

You can easily add any kind of navigation using the IDynamicViewModelPresenter interface. For example we need to make Master/Detail navigation for our app.
First we need to create main view model that will show our content:

public class MainViewModel : CloseableViewModel
    {
        #region Fields

        private readonly IToastPresenter _toastPresenter;
        private IViewModel _selectedItem;

        #endregion

        #region Constructors

        public MainViewModel(IToastPresenter toastPresenter)
        {
            Should.NotBeNull(toastPresenter, nameof(toastPresenter));
            _toastPresenter = toastPresenter;
            var items = new List<MenuItemModel>();
            for (var i = 0; i < 10; i++)
            {
                var item = new MenuItemModel
                {
                    Id = i,
                    Name = "Item " + i
                };
                items.Add(item);
            }
            Items = items;
            OpenItemCommand = new RelayCommand<MenuItemModel>(OpenItem);
        }

        #endregion

        #region Properties

        public IViewModel SelectedItem
        {
            get { return _selectedItem; }
            set
            {
                if (Equals(value, _selectedItem)) return;
                _selectedItem = value;
                OnPropertyChanged();
            }
        }

        public IList<MenuItemModel> Items { get; }

        #endregion

        #region Commands

        public ICommand OpenItemCommand { get; }

        private async void OpenItem(MenuItemModel item)
        {
            if (item == null)
                return;
            using (var vm = GetViewModel<ItemViewModel>())
            {
                vm.DisplayName = item.Name;
                vm.Id = item.Id;
                await vm.ShowAsync();
                _toastPresenter.ShowAsync(vm.DisplayName + " was closed", ToastDuration.Short);
            }
        }

        #endregion
    }

we defined the SelectedItem property for the content and the Items property that will show list of available to navigate content.
Second we need to create presenter that will use the SelectedItem property to show content and register navigation callback:

public class SplitViewViewModelPresenter : IDynamicViewModelPresenter
    {
        #region Fields

        private static readonly DataConstant<IDataContext> SelectedItemState = DataConstant.Create(() => SelectedItemState, true);

        private readonly IOperationCallbackManager _callbackManager;
        private readonly INavigationDispatcher _navigationDispatcher;
        private readonly IViewModelProvider _viewModelProvider;
        private static readonly NavigationType SplitViewNavigationType = new NavigationType(nameof(SplitViewNavigationType), new OperationType(nameof(SplitViewNavigationType)));

        #endregion

        #region Constructors

        public SplitViewViewModelPresenter(IViewModelProvider viewModelProvider, INavigationDispatcher navigationDispatcher, IOperationCallbackManager callbackManager)
        {
            _viewModelProvider = viewModelProvider;
            _navigationDispatcher = navigationDispatcher;
            _callbackManager = callbackManager;
            viewModelProvider.Preserved += OnViewModelPreserved;
            viewModelProvider.Restored += OnViewModelRestored;
        }

        #endregion

        #region Properties

        public int Priority => int.MinValue;

        #endregion

        #region Methods

        private static void OnViewModelPreserved(IViewModelProvider sender, ViewModelPreservedEventArgs args)
        {
            var mainViewModel = args.ViewModel as MainViewModel;
            if (mainViewModel?.SelectedItem != null)
                args.State.AddOrUpdate(SelectedItemState, sender.PreserveViewModel(mainViewModel.SelectedItem, DataContext.Empty));
        }

        private static void OnViewModelRestored(IViewModelProvider sender, ViewModelRestoredEventArgs args)
        {
            var mainViewModel = args.ViewModel as MainViewModel;
            if (mainViewModel != null)
            {
                var context = args.State.GetData(SelectedItemState);
                if (context != null)
                    mainViewModel.SelectedItem = sender.RestoreViewModel(context, DataContext.Empty, false);
            }
        }

        private void OnCurrentNavigationsCompleted(IViewModel viewModel)
        {
            var mainViewModel = _navigationDispatcher.GetTopViewModels().OfType<MainViewModel>().FirstOrDefault();
            if (mainViewModel == null)
            {
                mainViewModel = _viewModelProvider.GetViewModel<MainViewModel>();
                mainViewModel.ShowAsync((model, result) => model.Dispose(), context: new DataContext { { NavigationConstants.ClearBackStack, true } });
            }

            var oldValue = mainViewModel.SelectedItem;
            if (oldValue == viewModel)
                return;
            mainViewModel.SelectedItem = viewModel;
            _navigationDispatcher.OnNavigated(new NavigationContext(SplitViewNavigationType, NavigationMode.New, oldValue, viewModel, this));
            if (oldValue != null)
                _navigationDispatcher.OnNavigated(new NavigationContext(SplitViewNavigationType, NavigationMode.Remove, oldValue, null, this));
        }

        #endregion

        #region Implementation of interfaces

        public IAsyncOperation TryShowAsync(IDataContext context, IViewModelPresenter parentPresenter)
        {
            var viewModel = context.GetData(NavigationConstants.ViewModel);
            if (viewModel == null)
                return null;

            var navigationOperation = new AsyncOperation<object>();
            _callbackManager.Register(SplitViewNavigationType.Operation, viewModel, navigationOperation.ToOperationCallback(), context);

            //waiting when all navigations will be completed (Page, Window) to be sure that the MainViewModel is the top view model.
            parentPresenter.WaitCurrentNavigationsAsync(context)
                .ContinueWith(task => OnCurrentNavigationsCompleted(viewModel), TaskScheduler.FromCurrentSynchronizationContext());
            return navigationOperation;
        }

        public Task<bool> TryCloseAsync(IDataContext context, IViewModelPresenter parentPresenter)
        {
//we can add code that will change current SelectedItem to default for example
            return null;
        }

        #endregion
    }

The TryShowAsync method gets view model that we should show:

var viewModel = context.GetData(NavigationConstants.ViewModel);
if (viewModel == null)
    return null;

then presenter should register navigation callback using the IOperationCallbackManager interface this callback should be called when view model will be closed.

var navigationOperation = new AsyncOperation<object>();
_callbackManager.Register(SplitViewNavigationType.Operation, viewModel, navigationOperation.ToOperationCallback(), context);

then wait when all current navigation will be completed to be sure that the MainViewModel is the top view model:

//waiting when all navigations will be completed (Page, Window) to be sure that the MainViewModel is the top view model.
 parentPresenter.WaitCurrentNavigationsAsync(context)
                .ContinueWith(task => OnCurrentNavigationsCompleted(viewModel), TaskScheduler.FromCurrentSynchronizationContext());

then using the INavigationDispatcher interface gets the MainViewModel from opened view models or creates it if there no opened view models:

var mainViewModel = _navigationDispatcher.GetTopViewModels().OfType<MainViewModel>().FirstOrDefault();
if (mainViewModel == null)
{
    mainViewModel = _viewModelProvider.GetViewModel<MainViewModel>();
    mainViewModel.ShowAsync((model, result) => model.Dispose(), context: new DataContext { { NavigationConstants.ClearBackStack, true } });
}

then it sets the SelectedItem property and notifies the INavigationDispatcher about new navigation:

var oldValue = mainViewModel.SelectedItem;
if (oldValue == viewModel)
  return;
mainViewModel.SelectedItem = viewModel;
_navigationDispatcher.OnNavigated(new NavigationContext(SplitViewNavigationType, NavigationMode.New, oldValue, viewModel, this));

then it notifies the INavigationDispatcher about closed view model if it's not null, the INavigationDispatcher automatically completes the associated callbacks with closed view model.

if (oldValue != null)
  _navigationDispatcher.OnNavigated(new NavigationContext(SplitViewNavigationType, NavigationMode.Remove, oldValue, null, this));

This is a base sample of presenter, you can for example keep back stack if you want or add any other features that you want.
Full sources of example is available on GitHub.

Passing Parameters between ViewModels

Parameters between viewmodels can be passed by using overloads of ShowAsync method.

Let's look at an example. Suppose we want to disable animation of target view. In order to to that, we will pass NavigationConstants.UseAnimation equal to false.

private async Task Navigate()
{
  using (var vm = GetViewModel<TargetViewModel>())
  {
    await vm.ShowAsync(NavigationConstants.UseAnimations.ToValue(false));
  }
}

Here we passed NavigationConstants.UseAnimations as false and on any platform supported animation during showing it will be disabled.

📘

Most Useful Navigation Constants

Presented below are the list of most useful NavigationConstants constants:

  • SuppressTabNavigation,SuppressWindowNavigation, SuppressPageNavigation- if one viewmodel can be showed as tab, page or window one will pass these constants in order to indicate which view must be suppressed.
  • ViewName - one can set the name of view that must be bound to viewmodel.
  • ClearBackStack - if this parameter is passed as true back stack will be cleared (on mobile platforms only (iOS, Android, Xamarin.Forms)).
  • UseAnimations - one can set this parameter in order to control show animation of target view.

One can check all navigation constants here.

📘

ViewModel Extension Methods

There are exist several useful extension methods for a viewmodel. Presented below are methods for viewmodel showing (first this parameter and return type are omitted for convenience):

  • ShowAsync(..., IDataContext context = null) - method allows to skip IDataContext parameter. DataContext.Empty will be passed instead.
  • ShowAsync(..., string viewName, IDataContext context = null) - method allows to set the name of bound view.
  • ShowAsync(..., params DataConstantValue[] parameters) - method allows pass a collection of parameters to target viewmodel.

One can check all viewmodel extension methods here.

Navigation on Previously Created ViewModel

One of the most powerful features of MugenMvvm navigation is navigation on any previously created
viewmodel. In order to show previously created viewmodel just call ShowAsync again:

private async Task NavigateOnParent()
{
  this.ParentViewModel.ShowAsync();
}

Clear Back Stack

Back stack on mobile platforms can be easily cleared. Just pass NavigationConstants.ClearBackStack constant equal to true. Let's look at an example:

private async Task NavigateClearBackStack()
{
  using (var vm = GetViewModel<MainViewModel>())
    await vm.ShowAsync(NavigationConstants.ClearBackStack.ToValue(true));
}

List of Interface Markers

On some platforms (iOS, Xamarin.Forms) there are exists several interfaces that used as markers. They provided some hits in what way MugenMvvm have to show a view.
Presented below is the list of these interfaces with some short description:
iOS

  • IModalView - view should be shown as modal window.
  • IModalNavSupportView - view will be wrapped in NavigationController.
  • ISupportActivationModalView - view should be shown as modal window as long as Activate method will be invoked whenever view is activated.
  • ITabView - view should be shown as a tab.

Xamarin.Forms

  • IModalView - view should be shown as modal window.
  • ISupportActivationModalView - iew should be shown as modal window as long as Activate method will be invoked whenever view is activated.
  • ITabView - view should be shown as a tab.

More Subtle Way to Control Navigation

INavigationDispatcher

The INavigationDispatcher interface allows to control all navigation, all custom presenters should use it to notify about navigation.
Presented below is declaration of this interface:

public interface INavigationDispatcher
    {
        [NotNull]
        IDictionary<NavigationType, IList<IOpenedViewModelInfo>> GetOpenedViewModels(IDataContext context = null);

        [NotNull]
        IList<IOpenedViewModelInfo> GetOpenedViewModels([NotNull]NavigationType type, IDataContext context = null);

        Task<bool> OnNavigatingAsync([NotNull] INavigationContext context);

        void OnNavigated([NotNull]INavigationContext context);

        void OnNavigationFailed([NotNull]INavigationContext context, [NotNull] Exception exception);

        void OnNavigationCanceled([NotNull]INavigationContext context);

        event EventHandler<INavigationDispatcher, NavigatedEventArgs> Navigated;
    }
  • GetOpenedViewModels methods that allow to get all opened view models can be filtered by navigation type.
  • OnNavigatingAsync, OnNavigated, OnNavigationFailed, OnNavigationCanceled methods that should be used when you implement custom navigation to notify about navigation see the custom presenter sample.
    Navigated event that allows to handle all navigation events.

An example of using methods GetOpenedViewModels, for a scenario of a PopToViewModel feature. Meaning that when you have vm1 -> vm2 -> vm3 -> vm4 that in vm4 you can do CloseToAsync and that vm4, vm3 are closed automatically (the user does not see the transitions) but that viewModel logic is respected. But in the end you come back to vm2 in it's current state. (so not vm4 -> vm2 flow but vm4 -> vm3 -> vm2 back stack).

public static async Task CloseToAsync<TViewModel>(this IViewModel viewModel)
            where TViewModel : IViewModel
        {
            //getting all opened pages
            var openedViewModels = viewModel.GetIocContainer(true).Get<INavigationDispatcher>().GetOpenedViewModels(NavigationType.Page);
            bool close = false;
            foreach (var viewModelInfo in openedViewModels)
            {
                if (!close && viewModelInfo.ViewModel is TViewModel)
                    close = true;
                if (close)
                {
                    await viewModelInfo.ViewModel.CloseAsync(new DataContext
                    {
                        {NavigationConstants.UseAnimations, false }
                    });
                }
            }
        }

INavigableViewModel

One can control in what way view will be shown by using INavigableViewModel:

public interface INavigableViewModel : IViewModel
    {
        void OnNavigatedTo([NotNull] INavigationContext context);

        Task<bool> OnNavigatingFromAsync([NotNull] INavigationContext context);

        void OnNavigatedFrom([NotNull] INavigationContext context);
    }

OnNavigatedTo - called when a view becomes the active view.
OnNavigatingFromAsync, OnNavigatedFrom - called when a view is no longer the active view.