The MVVM Light framework for Xamarin.Android provides a NavigationService
allowing for navigation to views to be invoked from code in a View Model command handler for example.
The NavigationService
has the limitation that an Activity
class must derive from an ActivityBase
class provided by MVVM Light. This itself derives from Activity
so the type of views which can be used are limited.
I recently ran into this problem whereby I needed to use a view with a TabLayout
which requires the Activity
to derive from FragmentActivity
. The purpose of this post is to provide an example which is based upon the MVVM Light NavigationService
but will work with any Activity
type and doesn't require deriving from ActivityBase
.
The source code can be found on GitHub.
Overview
The code presented is the NavigationService
with very minimal changes - The class works great apart from the ActivityBase
inheritance limitation.
I found a great post by Jim Bennett MVVMLight navigation and AppCompatActivity which presents a solution to the problem in the context of using AppCompatActivity
. If you are in this scenario take a read because Jim provides code on GitHub and a Nuget package so this would be your quickest solution.
At first I tried to just use the Activity
deriving from FragmentActivity
with the NavigationService
and it worked to navigate to the view, but blew up with an exception when navigating back so be aware of this.
I have used the Plugin.CurrentActivty Nuget package written by James Montemagno. See his post Access the Current Android Activty from Anywhere! for details of how to use this elsewhere in your Xamarin.Android app. James also provides a Nuget package Plugin.currentActivty.
In additon the DialogService
and DispatcherHelper
classes depend upon ActivityBase
so I have provided examples using the Plugin.CurrentActivty rather than the ActivityBase
.
The example app provides a Home View and a Child View. There are buttons on each page to navigate to the other page. The Home View has an EditText
into which an integer parameter can be entered. This simulates passing an Id as a navigation parameter to the Child View, the value is displayed on the Child View.
The Child View has a second button titled Show Message which demonstrates the AlternateDispatcherHelper
and AlternateDialogService
to show a Message Box.
The images below show the example app:-
The Home View
The Child View
The Message Box
Resources
The MVVM Light documentation, The MVVM Light NavigationService part 1: The INavigationService interface and The MVVM Light NavigationService part 4: Xamarin.Android.
James Montemagno's post Access the Current Android Activty from Anywhere!. The source code is on GitHub and a Nuget package Plugin.CurrentActivty is available.
Jim Bennett's post MVVMLight navigation and AppCompatActivity provides an example geared towards the use of MVVM Light navigation with
AppCompatActivity
classes. He provides code on GitHub and a Nuget package. If you are in this scenario this maybe the quickest way around this particular problem.
Key Points
The Nuget package Plugin.CurrentActivty should be added to the Xamarin.Android app project.
Use the
AlternateNavigationService
implementation of theINavigationService
interface in conjunction with theNavigationHelper
. Your view can derive from any Xamarin.AndroidActivity
class but must implement the interfaceINavigationView
.Instantiate an instance of
NavigationHelper
and provide a get property to satisfy theINavigationService
interface.Override
OnResume
and call theOnResume
method on theNavigationHelper
and the base implementation.Register the
AlternateNavigationService
andAlternateDialogService
with the IOC container (SimpleIOC in this example)An example view excerpt is shown below showing the required implementation of these points:-
[Activity(Label = "Child View")]
public class ChildView : Activity, INavigationView
{
private readonly NavigationHelper navigationHelper = new NavigationHelper();
public NavigationHelper Helper
{
get
{
return navigationHelper;
}
}
protected override void OnResume()
{
Helper.OnResume(this);
base.OnResume();
}
}
Adding the Nuget Packages
To start with we need to add the Nuget package Plugin.CurrentActivty:-
When installed this will add a MainApplication.cs file and instructions will be displayed in a ReadMe.txt file:-
Example Navigation Service
The AlternateNavigationService
is basically the MVVM Light NavigationService
class copied and modifed to use the Plugin.CurrentActivity and the NavigationHelper
via the INavigationView
interface.
Essentially the properties and methods provided by the ActivityBase
class are instead provided by the NavigationHelper
class. I have removed the XML documentation comments from the code listings below but they are present in the source code on GitHub.
The INavigationView
interface (in the Navigation folder) can be seen below:-
namespace NavigationExample.Navigation
{
public interface INavigationView
{
NavigationHelper Helper { get; }
}
}
The NavigationHelper
class (in the Navigation folder) can be seen below:-
public class NavigationHelper
{
public Activity CurrentActivity { get; set; }
public string ActivityKey { get; set; }
public string NextPageKey { get; set; }
public void GoBack()
{
if (CurrentActivity != null)
{
CurrentActivity.OnBackPressed();
}
}
public void OnResume(Activity view)
{
CurrentActivity = view;
if (string.IsNullOrEmpty(ActivityKey))
{
ActivityKey = NextPageKey;
NextPageKey = null;
}
}
}
The AlternateNavigationService
class (in the Navigation folder) is modifed to call the methods on the NavigationHelper
class rather than the ActivityBase
and the CurrentView property is provided by the Plugin.CurrentActivity :-
public INavigationView CurrentView
{
get { return CrossCurrentActivity.Current.Activity as INavigationView; }
}
The class listing can be seen below:-
public class AlternateNavigationService : INavigationService
{
public INavigationView CurrentView
{
get { return CrossCurrentActivity.Current.Activity as INavigationView; }
}
public const string RootPageKey = "-- ROOT --";
private const string ParameterKeyName = "ParameterKey";
private readonly Dictionary<string, Type> _pagesByKey = new Dictionary<string, Type>();
private readonly Dictionary<string, object> _parametersByKey = new Dictionary<string, object>();
public string CurrentPageKey
{
get
{
return CurrentView.Helper.ActivityKey ?? RootPageKey;
}
}
public void Configure(string key, Type activityType)
{
lock (_pagesByKey)
{
if (_pagesByKey.ContainsKey(key))
{
_pagesByKey[key] = activityType;
}
else
{
_pagesByKey.Add(key, activityType);
}
}
}
public object GetAndRemoveParameter(Intent intent)
{
if (intent == null)
{
throw new ArgumentNullException("intent",
"This method must be called with a valid Activity intent");
}
var key = intent.GetStringExtra(ParameterKeyName);
intent.RemoveExtra(ParameterKeyName);
if (string.IsNullOrEmpty(key))
{
return null;
}
lock (_parametersByKey)
{
if (_parametersByKey.ContainsKey(key))
{
var param = _parametersByKey[key];
_parametersByKey.Remove(key);
return param;
}
return null;
}
}
public T GetAndRemoveParameter<T>(Intent intent)
{
return (T)GetAndRemoveParameter(intent);
}
public void GoBack()
{
CurrentView.Helper.GoBack();
}
public void NavigateTo(string pageKey)
{
NavigateTo(pageKey, null);
}
public void NavigateTo(string pageKey, object parameter)
{
if (CurrentView.Helper.CurrentActivity == null)
{
throw new InvalidOperationException("No CurrentActivity found");
}
lock (_pagesByKey)
{
if (!_pagesByKey.ContainsKey(pageKey))
{
throw new ArgumentException(
string.Format(
"No such page: {0}. Did you forget to call NavigationService.Configure?",
pageKey),
"pageKey");
}
var intent = new Intent(CurrentView.Helper.CurrentActivity, _pagesByKey[pageKey]);
if (parameter != null)
{
lock (_parametersByKey)
{
var guid = Guid.NewGuid().ToString();
_parametersByKey.Add(guid, parameter);
intent.PutExtra(ParameterKeyName, guid);
}
}
CurrentView.Helper.CurrentActivity.StartActivity(intent);
CurrentView.Helper.NextPageKey = pageKey;
}
}
}
The AlternateDialogService
class (in the Navigation folder) is changed by a single line only in the CreateDialog method:-
from:-
var builder = new AlertDialog.Builder(ActivityBase.CurrentActivity);
to:-
var builder = new AlertDialog.Builder(CrossCurrentActivity.Current.Activity);
The listing for the CreateDialog method can be seen below:-
public class AlternateDialogService : IDialogService
{
....
private static AlertDialogInfo CreateDialog(
string content,
string title,
string okText = null,
string cancelText = null,
Action<bool> afterHideCallbackWithResponse = null)
{
var tcs = new TaskCompletionSource<bool>();
var builder = new AlertDialog.Builder(CrossCurrentActivity.Current.Activity);
builder.SetMessage(content);
builder.SetTitle(title);
AlertDialog dialog = null;
builder.SetPositiveButton(
okText ?? "OK",
(d, index) =>
{
tcs.TrySetResult(true);
// ReSharper disable AccessToModifiedClosure
if (dialog != null)
{
dialog.Dismiss();
dialog.Dispose();
}
if (afterHideCallbackWithResponse != null)
{
afterHideCallbackWithResponse(true);
}
// ReSharper restore AccessToModifiedClosure
});
if (cancelText != null)
{
builder.SetNegativeButton(
cancelText,
(d, index) =>
{
tcs.TrySetResult(false);
// ReSharper disable AccessToModifiedClosure
if (dialog != null)
{
dialog.Dismiss();
dialog.Dispose();
}
if (afterHideCallbackWithResponse != null)
{
afterHideCallbackWithResponse(false);
}
// ReSharper restore AccessToModifiedClosure
});
}
builder.SetOnDismissListener(
new OnDismissListener(
() =>
{
tcs.TrySetResult(false);
if (afterHideCallbackWithResponse != null)
{
afterHideCallbackWithResponse(false);
}
}));
dialog = builder.Create();
return new AlertDialogInfo
{
Dialog = dialog,
Tcs = tcs
};
}
....
}
The AlternateDispatcherHelper
class (in the Threading folder) is modified by a single line change in the CheckBeginInvokeOnUI method:-
from:-
ActivityBase.CurrentActivity.RunOnUiThread(action);
to:-
CrossCurrentActivity.Current.Activity.RunOnUiThread(action);
In the CheckDispatcher method the if statement is changed
from:-
if (ActivityBase.CurrentActivity == null)
to:-
if (CrossCurrentActivity.Current.Activity == null)
The error message is also changed to remove the comment about ActivityBase
.
The class listing can be seen below:-
public static class AlternateDispatcherHelper
{
public static void CheckBeginInvokeOnUI(Action action)
{
if (action == null)
{
return;
}
CheckDispatcher();
CrossCurrentActivity.Current.Activity.RunOnUiThread(action);
}
public static void Initialize()
{
}
public static void Reset()
{
}
private static void CheckDispatcher()
{
if (CrossCurrentActivity.Current.Activity == null)
{
var error = new StringBuilder("The AlternateDispatcherHelper cannot be called.");
throw new InvalidOperationException(error.ToString());
}
}
}
Using the Alternate classes
To use the AlternateNavigationService
and AlternateDialogService
classes they must be registered with the IOC container. In the example this is done in the Bootstrapper
class, which provides the static property to access the ViewModelLocator
and also configures the pages with the AlternateNavigationService
:-
public static class Bootstrapper
{
private static ViewModelLocator locator;
public static ViewModelLocator Locator
{
get
{
if (locator == null)
{
DispatcherHelper.Initialize();
var nav = new AlternateNavigationService();
SimpleIoc.Default.Register<INavigationService>(() => nav);
//register the pages with the navigation service
nav.Configure(ViewModelLocator.MainPageKey, typeof(MainView));
nav.Configure(ViewModelLocator.ChildPageKey, typeof(ChildView));
SimpleIoc.Default.Register<IDialogService, AlternateDialogService>();
locator = new ViewModelLocator();
}
return locator;
}
}
}
The ViewModelLocator
class registers the View Models with the IOC container and provides properties so the View classes (Xamarin.Android Activity classes) can obtain the ViewModels. It also provides the string definitions for the page keys used with the AlternateNavigationService
. The class listing is:-
public class ViewModelLocator
{
public const string MainPageKey = "MainPage";
public const string ChildPageKey = "ChildPage";
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
SimpleIoc.Default.Register<ChildViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public ChildViewModel Child
{
get
{
return ServiceLocator.Current.GetInstance<ChildViewModel>();
}
}
public static void Cleanup()
{
// TODO Clear the ViewModels
}
}
The View's (Xamarin.Android Activity
classes) must implement the INavigationView
interface and in doing so provide a property on type NavigationHelper
and an override on OnResume
which calls the NavigationHelper
s OnResume. Make sure to call the OnResume method on both the NavigationHelper
and the base class.
The Home View screen is provided by the MainView class:-
[Activity(Label = "Home View", MainLauncher = true, Icon = "@mipmap/icon")]
public class MainView : Activity, INavigationView
{
private readonly List<Binding> bindings = new List<Binding>();
private readonly NavigationHelper navigationHelper = new NavigationHelper();
public NavigationHelper Helper
{
get
{
return navigationHelper;
}
}
protected override void OnResume()
{
Helper.OnResume(this);
base.OnResume();
}
private MainViewModel Vm
{
get
{
return Bootstrapper.Locator.Main;
}
}
private Button navigateChildButton;
public Button NavigateChildButton
{
get
{
return navigateChildButton ??
(navigateChildButton = FindViewById<Button>(
Resource.Id.navChildButton));
}
}
private EditText numberEditText;
public EditText NumberEditText
{
get
{
return numberEditText ??
(numberEditText = FindViewById<EditText>(
Resource.Id.paramEditText));
}
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
NavigateChildButton.SetCommand("Click", Vm.NavigateChildCommand);
//prevent agressive linker removing 'Click' event
NavigateChildButton.Click += (sender, e) => { };
bindings.Add(this.SetBinding(
() => Vm.Parameter,
() => NumberEditText.Text,
BindingMode.TwoWay));
}
}
The MainViewModel
uses the AlternateNavigationService
to show the Child View:-
public class MainViewModel : ViewModelBase
{
protected readonly INavigationService navigationService;
private int parameter;
private RelayCommand navigateChildCommand;
public INavigationService NavigationService
{
get { return navigationService; }
}
public int Parameter
{
get
{
return parameter;
}
set
{
Set(ref parameter, value);}
}
}
public RelayCommand NavigateChildCommand
{
get
{
return navigateChildCommand ??
(navigateChildCommand = new RelayCommand(
() =>
{
navigationService.NavigateTo(ViewModelLocator.ChildPageKey,
new ChildNavigationParameter()
{ Id = Parameter });
}));
}
}
public MainViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
}
The ChildViewModel
demonstrates the use of the AlternateDialogService
and AlternateDispatcherHelper
when the Show Message button is pressed:-
public RelayCommand ShowMessageCommand
{
get
{
return showMessageCommand ??
(showMessageCommand = new RelayCommand(
() =>
{
Task.Run(() =>
{
AlternateDispatcherHelper.CheckBeginInvokeOnUI(() =>
dialogService.ShowMessageBox(
"Test the Alternate Dialog Service works.",
"Test AlternateDialogService"));
});
}));
}
}
I have used the AlternateNavigationService
with Activity
and FragmentActivity
derived views so far and it seems to work fine.