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 ofIDynamicViewModelPresenter
items.IDynamicViewModelPresenter
has aPriority
and can show itself by usingTryShowAsync
method. You can check interface definition here.ShowAsync
- this method make view model instance show. It is iterating overDynamicPresenters
ordered by priority and invoking itsTryShowAsync
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. AsShowAsync
method this one is iterating overDynamicPresenters
ordered by priority and trying to find appropriateIRestorableDynamicViewModelPresenter
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:
- Declare
RelayCommand
in main view model. - Bind it on
Click
event of control. - 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:
- Add some tab
ItemsSource
property in ViewModel. - Bind this collection to
ItemsSource
of tab control. - 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:
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:
- Get reference on current
IViewModelPresenter
(easy to implement with usage of DI). - Register your viewmodel as
DynamicMultiViewModelPresenter
by usingIViewModelPresenter.DynamicPresenters
. - 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:
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 astrue
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 inNavigationController
.ISupportActivationModalView
- view should be shown as modal window as long asActivate
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 asActivate
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.
Updated over 7 years ago