Introduction
This article is built on topics and solutions introduced in ViewFactory for Xamarin application and Xamarin Master-Detail Page articles. It shows a method for easier navigation and use of services via IoC in Xamarin application.
Knowledge from previous examples is not necessary, but makes this article a bit easier to understand.
For IoC container, it will use TinyIoC, but not vanilla version from author GitHub repository (or from Nuget). I will use my personal fork, because this version can be used in PCL projects, which allows TinyIoC to be placed inside shared project, which makes it much easier. Otherwise, with the original version, which is incompatible with PCL version of .NET, we would be force to place the same TinyIoC code in every platform project. It does complicate things unnecessary. With PCL enabled version, this is no longer a problem.
Idea is to use IoC container to:
- Create seamless navigation between views
- Resolve application services in view models
Easy Navigation
It is important to note that new navigation mechanism should work for usual Xamarin views that are descendants from Xamarin Page class and also for pages deriving from DetailPage
class. Detail views are the ones that have to be placed inside master page like it was explained in Master-Detail article. With navigation mechanism like that, we can easily navigate to another from view model (which should hold that kind of logic anyway) to another page using its view model, completely oblivious to the details of view implementation.
Navigation to view model type by resolving that type from IoC container, allows to inject this model dependencies in a very easy and elegant way via constructor. You need show to the user GPS location? You just need to add new constructor parameter with IGSPService
and use obtained reference.
How to do it? First, we need to create Xamarin application with shared project - IoCSample
. In this article, I will use Android platform. Rest of the platform projects are not necessary. After creating projects, we need to add few references to Android project: ViewFactory
, ViewFactory.Android
and CustomMasterDetailControl
(all are available in XamarinSamples, where this article code will be added as well). Those projects are necessary to implement better navigation between views. CustomMasterDetailControl
assembly contains MasterDetailControl
, which will allow us to create detail views and put them inside defined master control. Detail views along with ordinary Xamarin views will allow to show how navigation is independent from views definitions. ViewFactory
projects will be used internally by navigation for views creation. After that, we can add TinyIoC file with IoC container definition. This class do not need any initialization. Use of static
value TinyIoCContainer.Current
is enough for our needs.
Now we can change default application. Here is content of App.xaml. App.xaml.cs and MainActivity.cs files, if we do things without IoC.
="1.0"="utf-8"
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IoCSample.App">
</Application>
public partial class App
{
public App ()
{
InitializeComponent();
MainPage = new MainPageView { BindingContext = new MainPageViewModel() };
}
}
[Activity(Label = "IoCSample", Icon = "@drawable/icon",
MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
global::Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new IoCSample.App());
}
}
We are creating two instances: view and view model to create main page for application. The goal is to use ViewFactory
to create page from view model type. To do that, we need to first register ViewFactory
service in IoC, but because ViewFactory
is defined in Android platform project (because few Reflection features are not that easily accessible from PCL and it is much easier from full .NET in platform project; however this is possible in PCL but exceeds the scope of this article), we also need to register this service in platform class - MainActivity
.
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
RegisterServices(TinyIoCContainer.Current);
global::Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new IoCSample.App());
}
private void RegisterServices(TinyIoCContainer container)
{
container.Register<IViewFactory, VFactory>();
}
IViewFactory
is new interface for ViewFactory
class, which will make use of it more convenient with IoC.
public interface IViewFactory
{
UIPage CreateView<TViewModel>() where TViewModel : BaseViewModel;
UIPage CreateView<TViewModel, TView>() where TViewModel : BaseViewModel
where TView : UIPage;
UIPage CreateView<TViewModel, TView>(TViewModel viewModel) where TViewModel : BaseViewModel
where TView : UIPage;
UIPage CreateView(Type viewModelType);
void Init(Assembly appAssembly);
}
Then, we can initialize MainPage
of application, with ViewFactory
.
public IViewFactory viewFactory { get; set; }
public App()
{
var container = TinyIoCContainer.Current;
container.BuildUp(this);
viewFactory.Init(Assembly.GetExecutingAssembly());
InitializeComponent();
MainPage = viewFactory.CreateView<MainPageViewModel>();
}
After starting application, we will see MainPage
, which proves that ViewFactory
, IoC combo work.
Ok, but still it is just creation of new value for MainPage
property of application. How to implement easier navigation? If you are not planning single page application, we need Xamarin INavigation
instance. The easiest way to obtain one is to create NavigationPage
instance as value for MainPage
instead.
MainPage = new NavigationPage(viewFactory.CreateView<MainPageViewModel>());
Now let's create another page to be able to test if new navigation works.
="1.0"="UTF-8"
<customMasterDetailControl:UIPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:customMasterDetailControl=
"clr-namespace:CustomMasterDetailControl;
assembly=CustomMasterDetailControl"
x:Class="IoCSample.PageView">
<Label Text="{Binding Label}" HorizontalOptions="Center" VerticalOptions="Center"/>
</customMasterDetailControl:UIPage>
The next step is to create navigation between view models. We can create static
class NavigationHelper
for this.
public class NavigationHelper
{
private static readonly INavigation _navigation;
private static readonly IViewFactory _viewFactory;
private static TinyIoCContainer _container;
static NavigationHelper()
{
var container = TinyIoCContainer.Current;
_container = container;
_viewFactory = container.Resolve<IViewFactory>();
_navigation = container.Resolve<INavigation>();
}
public static void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
{
_navigation.PushAsync(_viewFactory.CreateView<TViewModel>());
}
public static void NavigateTo<TViewModel>(Action<TViewModel> init) where TViewModel : BaseViewModel
{
var viewModel = _container.Resolve<TViewModel>();
init(viewModel);
_navigation.PushAsync(_viewFactory.CreateView(viewModel));
}
}
It uses new CreateView
method inside ViewFactory
class.
public UIPage CreateView<TViewModel>(TViewModel viewModel)
{
var viewModelType = viewModel.GetType();
if (Views.ContainsKey(viewModelType))
{
var viewData = Views[viewModelType];
return CreateView(viewModel, viewData.Creator);
}
return null;
}
The above method allows us to create page with given view model. But it would have made things problematic if view model needs to be initialized somehow before navigation to its page. For example, product details new instance or identificator of a product. We can create new button inside MainPage
which will cause navigation to new PageView
with initialization action to show how this works.
New view model looks like below:
public class PageViewModel : BaseViewModel
{
public void SetLabelText(string value)
{
Label = value;
}
public string Label { get; set; }
}
Initialization
method and property that is binded to view label. Navigation to this page is implemented with new NavigationHelper
class called from MainPageView
button.
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Row="0" Text="{Binding MainText}"
VerticalOptions="Center" HorizontalOptions="Center" />
<Button Grid.Row="1" Text="To Page" Command="{Binding ToPage}" />
</Grid>
public ICommand ToPage
{
get
{
return _toPage ?? (_toPage = new Command(() =>
{
NavigationHelper.NavigateTo<PageViewModel>(
vm => vm.SetLabelText("Value from MainPageViewModel"));
}));
}
}
After build and run, the application correctly navigates from MainPageView
to PageView
.
The same command but without initialization of view model is less complicated and looks like this:
public ICommand ToPage
{
get
{
return _toPage ?? (_toPage = new Command(NavigationHelper.NavigateTo<PageViewModel>));
}
}
There is also the possibility of creating, i.e., IViewModelInit
interface with Init()
method, but it creates another problem. How to define parameters in this method? If this will be single object type value, it forces cast to correct type to set necessary data in view model. If we do not want casting, we need to create generic BaseViewModel<>
class with generic method for initialization. But still, it forces us to put types in two places (type definition and initialization method), which complicates code that usually changes very often and still exists problem with number of parameters (though I never saw such method with more than two parameters). This is why I decided to leave it like this.
Nevertheless, we can simplify this further by creating method for navigating inside base view model class. Ideally, it would be to put inside BaseViewModel
but it is referenced assembly without knowledge of NavigationHelper
class. Because of that, we need to create new NavigationViewModel
type with those methods and then change application view models base types.
public class NavigationBaseViewModel : BaseViewModel
{
public static void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
{
NavigationHelper.NavigateTo<TViewModel>();
}
public static void NavigateTo<TViewModel>
(Action<TViewModel> init) where TViewModel : BaseViewModel
{
NavigationHelper.NavigateTo(init);
}
}
After changing MainPageViewModel
base class to type above, we can rewrite ToPage
command once again.
public ICommand ToPage
{
get
{
return _toPage ?? (_toPage = new Command(() =>
{
NavigateTo<PageViewModel>
(vm => vm.SetLabelText("Value from MainPageViewModel"));
}));
}
}
In one line, we can now order our application to:
- Create view model instance
- Initialize view model instance with appropriate data from previous view
- Create new view
- Set view model in new view
- Show new view to the user
Pretty elegant solution comparing to what is required in Xamarin by default.
public ICommand ToPageXamarin
{
get
{
return _toPage ?? (_toPage = new Command(() =>
{
var newView = new PageView();
var newViewModel = new PageViewModel();
newViewModel.SetLabelText("Value from MainPageViewModel");
newView.BindingContext = newViewModel;
var navigation = App.Current.MainPage.Navigation;
navigation.PushAsync(newView);
}));
}
}
The above code gets even more complicated if PageViewModel
would have some dependencies with dependencies, etc.
How About Navigation to Detail Pages?
The next step is to implement navigation to detail pages too in the same way - by single call to NavigateTo<>
method. This requires us to change NavigationHelper
class. The problem with static
class is that we can't enforce in any way necessity of pointing out which Master page should be used. Of course, you can have one single file for all of your applications, but I highly doubt it. Most of the time, they will differ a little. Because of that, even class MasterPageControl
as in the previous article might not be enough since it i.e. lacks ability to put menu on the left side of application (but can be easily achieved by modification of MasterPageControl.xaml file). Of course, it is possible to create a static
property, let's say called MasterPageViewModelType
, which would point out, which master view should be used for detail pages, but this is more error prone. I prefer to make code as clear to use as possible and then throw errors in run-time. Most of the time, I am the end user of my own code and I do not want to think how I should implement some mechanism to 'get things done' . It should be easy and obvious as much as possible. Because of that, a better idea is to do service for navigation as abstract
base class, with abstract
property, with type of master page view model.
So we need base implementation of NavigationService
. A good idea is to create a generic class.
public class NavigationService<TMasterViewModel> : INavigationService<TMasterViewModel>
where TMasterViewModel : MasterDetailControlViewModel
{
}
Most of the code is copied from NavigationHelper
, but to implement detail views support, we need add few new things.
public TMasterViewModel MasterViewModel
{
get
{
var page = _navigation.NavigationStack.LastOrDefault();
return page?.BindingContext as TMasterViewModel;
}
}
The above property returns instance of master view model as type defined in class definition. Instance is not null
only if there is already master page on top of a stack. Why? Because Xamarin INavigation do not allow the same page to be twice in the views stack (which makes sense) and this is why we need to create new master page every time detail page is added after ordinary, fullscreen page. Think of this as:
Master(Detail1) -> Full screen page -> Master(Detail2)
If you want to push new detail page on stack like above, you can reuse last master page. But if you want to push new detail page on top, after 'full screen page' is on top, you cannot reuse the previous one because it is already in other position. Therefore, new instance has to be created.
With this knowledge, we can implement new NavigateTo<>
methods.
public void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
{
PushPage<TViewModel>(_viewFactory.CreateView<TViewModel>());
}
public void NavigateTo<TViewModel>(Action<TViewModel> init) where TViewModel : BaseViewModel
{
var viewModel = _container.Resolve<TViewModel>();
init(viewModel);
PushPage<TViewModel>(_viewFactory.CreateView(viewModel));
}
public void PushPage<TViewModel>(Page page) where TViewModel : BaseViewModel
{
if (!_viewFactory.IsDetailView<TViewModel>())
{
_navigation.PushAsync(page);
}
else
{
var masterViewModel = MasterViewModel;
UIPage masterPage = null;
if (masterViewModel == null)
{
masterPage = _viewFactory.CreateView<TMasterViewModel>();
masterViewModel = (TMasterViewModel)masterPage.BindingContext;
}
masterViewModel.Detail = page;
if (MasterViewModel == null)
{
_navigation.PushAsync(masterPage);
}
}
}
New PushPage
method takes care of previous call to INavigation.PushAsync
for ordinary (full screen) pages.
It uses new IsDetailView
method of IViewFactory
interface to found if page is detail view.
public bool IsDetailView(Type viewModelType)
{
return Views[viewModelType].IsDetail;
}
public bool IsDetailView<TViewModel>() where TViewModel : BaseViewModel
{
return IsDetailView(typeof(TViewModel));
}
ViewFactory
already contains information if view is detail view or not. We just needed a new method to extract this information from service.
If the above method returns true
for some view model, it means that it has to be handled differently and cannot be directly pushed into INavigation
stack. This different logic is handled by else
clause in INavigationService.PushPage
method. If there is no master page on top - it is created (as it was explained above) along with appropriate view by ViewFactory
. After that, new detail page is set in master and finally master page is pushed into navigation stack of Xamarin (if it is not there yet). That explained, here the whole class will look like:
public class NavigationService<TMasterViewModel> : INavigationService<TMasterViewModel>
where TMasterViewModel : MasterDetailControlViewModel
{
private readonly TinyIoCContainer _container;
private readonly INavigation _navigation;
private readonly ViewSwitcher.IViewFactory _viewFactory;
public NavigationService()
{
var container = TinyIoCContainer.Current;
_container = container;
_viewFactory = container.Resolve<ViewSwitcher.IViewFactory>();
_navigation = container.Resolve<INavigation>();
}
public TMasterViewModel MasterViewModel
{
get
{
var firstOrDefault = _navigation.NavigationStack.FirstOrDefault();
return firstOrDefault?.BindingContext as TMasterViewModel;
}
}
public void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
{
PushPage<TViewModel>(_viewFactory.CreateView<TViewModel>());
}
public void NavigateTo<TViewModel>(Action<TViewModel> init) where TViewModel : BaseViewModel
{
var viewModel = _container.Resolve<TViewModel>();
init(viewModel);
PushPage<TViewModel>(_viewFactory.CreateView(viewModel));
}
public void PushPage<TViewModel>(Page page) where TViewModel : BaseViewModel
{
if (!_viewFactory.IsDetailView<TViewModel>())
{
_navigation.PushAsync(page);
}
else
{
var masterViewModel = MasterViewModel;
UIPage masterPage = null;
if (masterViewModel == null)
{
masterPage = _viewFactory.CreateView<TMasterViewModel>();
masterViewModel = (TMasterViewModel)masterPage.BindingContext;
}
masterViewModel.Detail = page;
if (MasterViewModel == null)
{
_navigation.PushAsync(masterPage);
}
}
}
}
Now we can finally put it to use. For example, new button added to PageView
in a similar way like in MainPageView
, will navigate to new DetailView
.
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Row="0"
Text="{Binding Label}" HorizontalOptions="Center"
VerticalOptions="Center"/>
<Button Grid.Row="1" Text="To detail"
Command="{Binding ToDetailPage}" />
</Grid>
With view model like below:
public class PageViewModel : NavigationBaseViewModel
{
private Command _toDetailPage;
public void SetLabelText(string value)
{
Label = value;
}
public string Label { get; set; }
public ICommand ToDetailPage
{
get
{
return _toDetailPage ?? (_toDetailPage = new Command(OnToDetailPage));
}
}
private void OnToDetailPage()
{
NavigateTo<DetailViewModel>();
}
}
But we need to define master page first - MasterDetailControl
.
<masterDetail:MasterDetailControl
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:masterDetail="clr-namespace:CustomMasterDetailControl;assembly=CustomMasterDetailControl"
x:Class="IoCSample.Views.MasterDetailView">
<masterDetail:MasterDetailControl.SideContent>
<StackLayout>
<Button Text="To detail page" Command="{Binding ToDetail}" />
</StackLayout>
</masterDetail:MasterDetailControl.SideContent>
</masterDetail:MasterDetailControl>
View model for the above view looks like this:
public class MasterDetailViewModel : MasterDetailControlViewModel
{
private ICommand _toDetai;
public ICommand ToDetail
{
get { return _toDetai ?? (_toDetai = new Command(OnToDetail)); }
}
private void OnToDetail()
{
NavigationHelper.NavigateTo<Detail1ViewModel>();
}
}
Now, we need just to register new service in container, in App.xaml.cs file and we can try it out.
public void RegisterServices(TinyIoCContainer container)
{
container.Register<INavigationService, NavigationService<MasterDetailViewModel>>();
}
After running this code, we can see that this works fine and should give result similar to the image below:
Now, when we have Navigation implemented, we can jump to the second topic.
ViewModels Dependency Injection
Views are supposed to have no constructor dependencies. Models are usually just data or data containers, which data logic (add order for customer, etc.). The only thing UI should have is application logic in view models. Most of the time, they will obtain new data or show existing data to the user. But sometimes, there is a need for some specific logic, i.e., get GPS location, connect with Bluetooth device, generate PDF, show modal dialog, etc. All those functions are more general and it is a good idea to implement them as services that can be shared between different views and applications. Sharing can be implement by IoC container and Dependency Injection (via constructors) in view models.
What do we need to do in our project to achieve that? Two things:
ViewFactory
should create view models from IoC container, not Activator
class. - Register services during application initialization.
With those two, we can add parameters to constructors with services interfaces.
Let's do first point first. It is quite easy and the only problem is that we have to create a virtual
method in BaseViewFactory
class and overwrite it in new type defined in the same assembly as TinyIoC.
private UIPage CreateView(Type viewModelType, Func<UIPage> creator)
{
var viewModel = CreateViewModelInstance(viewModelType);
return CreateView(viewModel, creator);
}
protected virtual object CreateViewModelInstance(Type viewModelType)
{
var viewModel = Activator.CreateInstance(viewModelType);
return viewModel;
}
The above methods are part of changed BaseViewFactory
. Logic is the same - create instance of type via Activator
. Now, new type in IoCSample
project.
public class IoCViewFactory : ViewFactory.ViewFactory.ViewFactory
{
private readonly TinyIoCContainer _container;
public IoCViewFactory(TinyIoCContainer container)
{
_container = container;
}
protected override object CreateViewModelInstance(Type viewModelType)
{
return _container.Resolve(viewModelType);
}
}
As you can see, we just had to overwrite virtual
method to use IoC container instead of Activator
class. This is all we had to do!
Now, we need to create and register some service to use it inside view models. Let's imagine that we have some mechanism to store cache data in database or in file. We can change and/or read it from any view model. For case of simplicity following class, interface is enough for our sample.
public class DataCacheService : IDataCacheService
{
public Dictionary<string, object> DataCache { get; } = new Dictionary<string, object>();
}
public interface IDataCacheService
{
Dictionary<string, object> DataCache { get; }
}
Really simple mechanism to save some information during runtime. We have to register this service in container. We can do this inside App.xaml.cs file.
public void RegisterServices(TinyIoCContainer container)
{
container.Register<INavigationService, NavigationService<MasterDetailViewModel>>();
container.Register<IDataCacheService, DataCacheService>().AsSingleton();
}
We can now share cached data by accessing service. For example, we can save data in one view model - DetailViewModel
and read it in another one - Detail1ViewModel
. Saving is done by simple Entry
control binded to view model property. Every time user changes the value of Entry
, data is saved. Then after navigation to another view, it will show saved data in its label. Below is the code of mentioned view models.
public class DetailViewModel : BaseViewModel
{
private readonly IDataCacheService _dataCacheService;
private string _text;
public DetailViewModel(IDataCacheService dataCacheService)
{
_dataCacheService = dataCacheService;
if (_dataCacheService.DataCache.ContainsKey(CacheKey))
{
_text = (string)_dataCacheService.DataCache[CacheKey];
}
}
public const string CacheKey = "CacheKey";
public string Text
{
get { return _text; }
set
{
_text = value;
_dataCacheService.DataCache[CacheKey] = value;
}
}
}
public class Detail1ViewModel : BaseViewModel
{
public Detail1ViewModel(IDataCacheService dataCacheService)
{
if (dataCacheService.DataCache.ContainsKey(DetailViewModel.CacheKey))
{
Text = (string)dataCacheService.DataCache[DetailViewModel.CacheKey];
}
}
public string Text { get; private set; }
}
Really simple example. Appropriate views for view models are even simpler and there is no point listings their code.
After running new application, we can test if this actually works.
As you can see. the value entered in one view is accessible in the second one. Dependency injection in view models works fine with automatic navigation to views by view model types, without consideration of specifics of view definition.
You can download the cleaned and refactored code (everything in one project) from here (77.98 kB) or from github (with other Xamarin articles in the series).