A very simple application demonstrating how to use FunctionZero.MvvmZero
and Xamarin.Forms
to build a cross-platform application using
the MVVM pattern, dependency injection and an IoC container
After a recent change, you may need to update your Xamarin.Forms
packages before you install MvvmZero
.
This document has been updated accordingly.
Launch Visual Studio, grab a coffee and before it's gone cold you'll have an app!
This tutorial will guide you through the creation of a 4-page cross platform application as shown here:
There are many great resources online that can teach you MVVM. This is what you need to know to follow this tutorial:
- Each
Page
of the application consists of aContentPage
and a correspondingViewModel
ViewModels
expose data theContentPage
can display andCommands
theContentPage
can invoke- There is no business logic in a
ContentPage
- There is no UI in a
ViewModel
- MvvmZero delegates the creation of
ContentPages
andViewModels
to a 'simple factory' method. In this tutorial we'll be using an IoC container calledSimpleInjector
as our factory
If any of this is new to you, all will become clear!
As this is cross-platform, we need to specify the platform we want to run. Here we're testing the UWP build
And here it is ...
Your Visual Studio Solution will contain one platform-specific project for each target platform you have choosen to support. We will not be making any changes to those
All of our work will be in the cross-platform project MvvmZeroTutorialApp
Right-click->Manage NuGet packages...
Please note: You may need to update your Xamarin.Forms
package before MvvmZero
will install. To do so,
Right-click on the Solution->Manage NuGet Packages for Solution, ensure 'Include prerelease' is not checked, check 'Select all packages', click 'Update'
Note that installing MvvmZero also installs FunctionZero.CommandZero automatically. You can read about CommandZero here
Everything we do in this tutorial is in the MvvmZeroTutorialApp
project. The platform-specific projects are not touched
Add these folders to the project (Right-click->Add->New Folder ...)
- CabbagesPage.xaml
- OnionsPage.xaml
- ResultsPage.xaml
The Solution tree should now look like this:
One ViewModel
for each ContentPage
- Mvvm/PageViewModels/CabbagesPageVm.cs
- Mvvm/PageViewModels/OnionsPageVm.cs
- Mvvm/PageViewModels/ResultsPageVm.cs
- Mvvm/ViewModels/TutorialBaseVm.cs
- Mvvm/PageBoilerplate/Locator.cs
MvvmZero uses a PageService object to navigate from one page to the next. Locator.cs
is where we create a PageService
and provide it with a suitable factory method for creating Pages
and ViewModels
. Since SimpleInjector is my new favourite toy
that's what we'll be using as our factory, though it's straightforward enough to write your own factory if that's what you prefer
Tip: If your ContentPage
constructor requires the ViewModel to be injected as a dependency,
you can let an IoC container resolve your ViewModels directly and use the single-generic overload PushPageAsync<TPage>(Action<object> setStateAction, ...)
using FunctionZero.MvvmZero;
using MvvmZeroTutorialApp.Mvvm.Pages;
using MvvmZeroTutorialApp.Mvvm.PageViewModels;
using SimpleInjector;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace MvvmZeroTutorialApp.Mvvm.Boilerplate
{
public class Locator
{
private Container _IoCC;
internal Locator(Application currentApplication)
{
// Create the IoC container that will contain all our configurable classes ...
_IoCC = new Container();
// Tell the IoC container what to do if asked for an IPageService
_IoCC.Register<IPageServiceZero>(
() =>
{
// This is how we create an instance of PageServiceZero.
// The PageService needs to know how to get the current NavigationPage it is to interact with.
// (If you have a FlyoutPage at the root, the navigationGetter should return the current Detail item)
// It also needs to know how to get Page and ViewModel instances so we provide it with a factory
// that uses the IoC container. We could easily provide any sort of factory, we don't need to use an IoC container.
var pageService = new PageServiceZero(() => App.Current.MainPage.Navigation, (theType) => _IoCC.GetInstance(theType));
return pageService;
},
// One only ever will be created.
Lifestyle.Singleton
);
// Tell the IoC container about our Pages.
_IoCC.Register<HomePage>(Lifestyle.Singleton);
_IoCC.Register<CabbagesPage>(Lifestyle.Singleton);
_IoCC.Register<OnionsPage>(Lifestyle.Singleton);
_IoCC.Register<ResultsPage>(Lifestyle.Singleton);
// Tell the IoC container about our ViewModels.
_IoCC.Register<HomePageVm>(Lifestyle.Singleton);
_IoCC.Register<CabbagesPageVm>(Lifestyle.Singleton);
_IoCC.Register<OnionsPageVm>(Lifestyle.Singleton);
_IoCC.Register<ResultsPageVm>(Lifestyle.Singleton);
// Optionally add more to the IoC conatainer, e.g. loggers, Http comms objects etc. E.g.
// IoCC.Register<ILogger, MyLovelyLogger>(Lifestyle.Singleton);
}
/// <summary>
/// This is called once during application startup
/// </summary>
internal async Task SetFirstPage()
{
// Create and assign a top-level NavigationPage.
// If you use a FlyoutPage instead then its Detail item will need to be a NavigationPage
// and you will need to modify the 'navigationGetter' provided to the PageServiceZero instance to
// something like this:
// () => ((FlyoutPage)App.Current.MainPage).Detail.Navigation
App.Current.MainPage = new NavigationPage();
// Ask the PageService to assemble and present our HomePage ...
await _IoCC.GetInstance<IPageServiceZero>().PushPageAsync<HomePage, HomePageVm>((vm) => {/* Optionally interact with the vm, e.g. to inject seed-data */ });
}
/// <summary>
/// For debug purposes to let us know when a Page is assembled by the PageService
/// </summary>
/// <param name="thePage">A reference to the page that has been presented</param>
private void PageCreated(Page thePage)
{
Debug.WriteLine(thePage);
}
}
}
Go to App.xaml.cs, add the Locator
backing variable, add the using
statement and replace the constructor as shown below:
using MvvmZeroTutorialApp.Mvvm.Boilerplate;
using Xamarin.Forms;
namespace MvvmZeroTutorialApp
{
public partial class App : Application
{
// Backing property for the Locator instance
public Locator Locator { get; private set; }
public App()
{
InitializeComponent();
// Create our Locator instance and tell it about the Application instance ...
Locator = new Locator(this);
// Ask the Locator to get us going ...
_ = Locator.SetFirstPage();
}
protected override void OnStart()
{
}
protected override void OnSleep()
{
}
protected override void OnResume()
{
}
}
}
Paste this into TutorialBaseVm.cs
using FunctionZero.MvvmZero;
namespace MvvmZeroTutorialApp.Mvvm.ViewModels
{
/// <summary>
/// This is a base class for your viewmodels
/// Note: It is not required, your viewmodels can be POCO or derive directly from MvvmZeroBaseVm
/// </summary>
public abstract class TutorialBaseVm : MvvmZeroBaseVm
{
// TODO: Put any base class specialization for your app in here
}
}
The HomePage
will have two Buttons
so we'll need two Commands
for the Buttons
to bind to
The Commands
navigate to new Pages
, so we'll need the instance of PageServiceZero
described in Locator.cs
- As we're using an IoC container, it is simply a case of adding
IPageServiceZero
as a constructor parameter and the container will do the rest
Installing MvvmZero
automatically installed FunctionZero.CommandZero
and that contains the ICommand
implementation
we're going to use, documented here
If you have a preferred implementation feel free to use that instead
Now paste the following into HomePageVm.cs:
using FunctionZero.CommandZero;
using FunctionZero.MvvmZero;
using MvvmZeroTutorialApp.Mvvm.Pages;
using MvvmZeroTutorialApp.Mvvm.ViewModels;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MvvmZeroTutorialApp.Mvvm.PageViewModels
{
public class HomePageVm : TutorialBaseVm
{
private readonly IPageServiceZero _pageService;
/// <summary>
/// The UI can bind to this command and use it to get to the CabbagesPage
/// </summary>
public ICommand CabbagesPageCommand { get; }
/// <summary>
/// The UI can bind to this command and use it to get to the OnionsPage
/// </summary>
public ICommand OnionsPageCommand { get; }
/// <summary>
/// Here we inject an IPageService, so we can use it to get to another page when we're ready
/// </summary>
/// <param name="pageService"></param>
public HomePageVm(IPageServiceZero pageService)
{
_pageService = pageService;
// Set up our commands for the UI to bind to ...
CabbagesPageCommand = new CommandBuilder().SetExecuteAsync(CabbagesPageCommandExecuteAsync).SetName("Cabbages").Build();
OnionsPageCommand = new CommandBuilder().SetExecuteAsync(OnionsPageCommandExecuteAsync).SetName("Onions").Build();
}
private async Task CabbagesPageCommandExecuteAsync(/* Optional : object arg */)
{
// Take us to the CabbagesPage page ...
await _pageService.PushPageAsync<CabbagesPage, CabbagesPageVm>((vm) => { /* Initialize the vm in here if necessary */ });
}
private async Task OnionsPageCommandExecuteAsync(/* Optional : object arg */)
{
// Take us to the OnionsPage page ...
await _pageService.PushPageAsync<OnionsPage, OnionsPageVm>((vm) => { /* Initialize the vm in here if necessary */ });
}
}
}
Replace the Page content with the following. MvvmZero will automatically set the BindingContext
of the
ContentPage
to an instance of HomePageVm
so we simply create two Buttons
and 'bind' them to the CabbagesPageCommand
and the OnionsCommand
in our ViewModel
<ContentPage.Content>
<StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand">
<Label Text="Make your choice"/>
<Button Command="{Binding CabbagesPageCommand}" Text="{Binding CabbagesPageCommand.Text}"/>
<Button Command="{Binding OnionsPageCommand}" Text="{Binding OnionsPageCommand.Text}"/>
</StackLayout>
</ContentPage.Content>
The buttons will take us to a new page and the back-button will bring us back to the home page.
This ViewModel
exposes a string
and a ICommand
for the UI to bind to
If the string
passes validation, NextCommand
is enabled and the user can proceed
Note that the PageService
injects data into the ResultsPage
via an Init
method
on ResultsPageVm
. We'll add the Init
method next
using FunctionZero.CommandZero;
using FunctionZero.MvvmZero;
using MvvmZeroTutorialApp.Mvvm.Pages;
using MvvmZeroTutorialApp.Mvvm.ViewModels;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MvvmZeroTutorialApp.Mvvm.PageViewModels
{
public class CabbagesPageVm : TutorialBaseVm
{
private string _name;
private IPageServiceZero _pageService;
/// <summary>
/// The UI can bind to this and display or modify it
/// </summary>
public string Name
{
get => _name;
set
{
if (value != _name)
{
_name = value;
// NOTE: TutorialBaseVm implements INotifyPropertyChanged.
// If it did not, we must add the interface to our class definition
// otherwise the Bindings in XAML would not be able to track changes to any properies in the class.
OnPropertyChanged();
}
}
}
/// <summary>
/// The UI can bind to this command and use it to get to the ResultsPage
/// </summary>
public ICommand NextCommand { get; }
public CabbagesPageVm(IPageServiceZero pageService)
{
_pageService = pageService;
// Initialise Name so we don't have to null-check it later
Name = string.Empty;
// Set up our Command for the UI to bind to ...
NextCommand = new CommandBuilder()
.SetExecuteAsync(NextCommandExecuteAsync)
.SetCanExecute(NextCommandCanExecute)
.SetName(GetCurrentName)
// This command can enable or disable itself or change its Text if the 'Name' property changes
.AddObservedProperty(this, nameof(Name))
.Build();
}
// When the NextCommand is invoked (by the UI) this method is called to take us to the results page
private async Task NextCommandExecuteAsync(object arg)
{
string payload = $"The Cabbages Page has been visited by {Name}";
await _pageService.PushPageAsync<ResultsPage, ResultsPageVm>((vm) => vm.Init(payload));
}
// Returns true if the Name property is considered valid
private bool NextCommandCanExecute()
{
return Name.Length >= 4;
}
// This is reevaluated (by the UI) every time the 'Name' property changes
private string GetCurrentName()
{
if (NextCommandCanExecute() == false)
return "Keep typing ...";
return "Next";
}
}
}
Now add this to ResultsPageVm so we can build what we have so-far
public void Init(string payload)
{
throw new NotImplementedException();
}
Replace the Page content in CabbagesPage.xaml
with the following
<ContentPage.Content>
<StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand">
<Label Text="Cabbages Page!"/>
<Label Text="Please tell me your name ..."/>
<Editor Text="{Binding Name}" Placeholder="Minimum 4 characters please"/>
<Button Command="{Binding NextCommand}" Text="{Binding NextCommand.Text}"/>
</StackLayout>
</ContentPage.Content>
Replace the contents of ResultsPageVm.cs
with the following
using FunctionZero.CommandZero;
using FunctionZero.MvvmZero;
using System.Windows.Input;
namespace MvvmZeroTutorialApp.Mvvm.PageViewModels
{
public class ResultsPageVm : TutorialBaseVm
{
private string _displayText;
/// <summary>
/// The UI can bind to this to display its content
/// We're using the SetProperty helper in the base class to raise
/// INotifyPropertyChanged (INPC) for us, so the UI will know if DisplayText changes.
/// Compare this with the 'Name' property in CabbagesPageVm where INPC is raised manually.
/// </summary>
public string DisplayText
{
get => _displayText;
set => SetProperty(ref _displayText, value);
}
/// <summary>
/// The UI can bind to this command and use it to start again
/// </summary>
public ICommand StartAgainCommand { get; }
/// <summary>
///
/// </summary>
/// <param name="pageService"></param>
public ResultsPageVm(IPageServiceZero pageService)
{
StartAgainCommand = new CommandBuilder().SetExecuteAsync(async () => await pageService.PopToRootAsync()).SetName("Restart").Build();
}
public void Init(string payload)
{
DisplayText = payload;
}
}
}
<ContentPage.Content>
<StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand">
<Label Text="Results Page!"/>
<Label Text="{Binding DisplayText}"/>
<Button Command="{Binding StartAgainCommand}" Text="{Binding StartAgainCommand.Text}"/>
</StackLayout>
</ContentPage.Content>
You should now be able to run the application and visit the CabbagePage and the ResultsPage. That leaves the OnionsPage
Here's the code for OnionsPage.cs ...
using FunctionZero.CommandZero;
using FunctionZero.MvvmZero;
using MvvmZeroTutorialApp.Mvvm.Pages;
using MvvmZeroTutorialApp.Mvvm.ViewModels;
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Input;
namespace MvvmZeroTutorialApp.Mvvm.PageViewModels
{
public class OnionsPageVm : TutorialBaseVm
{
/// <summary>
/// The UI can bind to this command and use it to get to the ResultsPage
/// </summary>
public ICommand NextCommand { get; }
/// <summary>
/// A very basic ViewModel
/// </summary>
public OnionsPageVm(IPageServiceZero pageService)
{
NextCommand = new CommandBuilder()
.SetExecuteAsync(() => pageService.PushPageAsync<ResultsPage, ResultsPageVm>((vm) => vm.Init("Hello from the Onions Page!")))
.SetName("Next")
.Build();
}
}
}
... and the UI for OnionsPage.xaml
<ContentPage.Content>
<StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" >
<Label Text="Welcome to the Onions Page" />
<Button Command="{Binding NextCommand}" Text="{Binding NextCommand.Text}"/>
</StackLayout>
</ContentPage.Content>
And there you have it. A cross-platform app for iOS, Android and UWP using the MVVM pattern, dependency injection and an IoC container!
To run this on an Android device
- Enable developer mode on the device (often by tapping on the build number in settings 5 times)
- Plug it into your PC
- Select 'Android' as your build target and build for 'Any CPU'
- Build and run!
It's a little more involved deploying to iOS. Good luck! :)