Vanilla MVVM for Xamarin Forms
Want to do MVVM without the frameworks? Xamarin Forms provides everything we need to implement the pattern and make our ViewModels testable.
Note that some naming, patterns and code have been borrowed from the excellent FreshMVVM framework. This is a great lightweight framework to get started with if you don't want to roll your own.
Simply put, MVVM helps us to abstract any UI logic out of the View and into a ViewModel to make it testable, while the data and business logic remains in the Model.
To avoid boilerplate code we're going to create a couple of base classes, one for our Views, and one for our ViewModels.
ViewModelBase
The basic implementation of ViewModelBase
needs to implement INotifyPropertyChanged
to enable bindings, needs to enable navigation and ideally provides hooks into the page lifecycle. Here's a simple implementation:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Title { get; set; }
public INavigation Navigation { get; }
protected ViewModelBase(INavigation navigation)
{
Navigation = navigation;
}
public virtual void Init(object initData)
{
}
public void WireEvents(Page page)
{
page.Appearing += ViewIsAppearing;
page.Disappearing += ViewIsDisappearing;
}
protected virtual void ViewIsDisappearing(object sender, EventArgs e)
{
}
protected virtual void ViewIsAppearing(object sender, EventArgs e)
{
}
protected virtual async Task PushPage(ContentPage page)
{
await Navigation.PushAsync(page);
}
protected virtual async Task PopPage()
{
await Navigation.PopAsync();
}
}
Seeing ContentPage
here may ring some alarm bells. This is a code smell, one of the rules of MVVM is that the ViewModel should know nothing about the View in order to be easily testable, and ContentPage is a View. In my experience, this is an acceptable trade-off for reducing complexity, and is still easy to test. If you don't like it, use an MVVM Framework to abstract away the navigation.
For navigation, we accept a Xamarin.Forms.INavigation object. This is easily mockable in our tests and is future proof. If Xamarin extends that interface in the future we won't need any changes to use it.
Borrowing from FreshMVVM, our Init()
method allows us to pass custom data to the ViewModel. You're going to have to cast it from an object, but if that makes you uneasy then it wouldn't be too hard to extend this example to support a generic interface for the initialisation data.
I've thrown in a Title
property here, it's not really needed but demonstrates that you should be adding any common properties to this class that your ViewModels will need. Note that while Title is bindable, we're not emitting a PropertyChanged event, this is because I use Fody.PropertyChanged to automatically add that boilerplate code when compiling.
Lastly, and again borrowing from FreshMVVM, we're going to allow the ViewModel access to some of the page lifecycle events through the WireEvents
method.
ContentPageBase
Our base class for the View will allow us to wire up all the page events and the bindings and pass the navigation object.
public class ContentPageBase<T> : ContentPage where T : ViewModelBase
{
public T ViewModel { get; set; }
protected ContentPageBase(object initData)
{
// The BindingContext = ViewModel call is left to the derived Page to do,
// Xamarin best practice says to bind it after InitializeComponent() so we can't do it here.
using (var scope = App.Container.BeginLifetimeScope(builder =>
builder.RegisterInstance(Navigation).As<INavigation>()))
{
ViewModel = scope.Resolve<T>();
ViewModel.WireEvents(this);
ViewModel.Init(initData);
}
}
protected ContentPageBase() : this(null)
{
}
}
Xamarin Forms provides a BindingContext for it's views, allowing us to delegate bindings to a separate class. We're going to take advantage of this and provide a custom base ContentPage class that will setup our ViewModel for every page.
Note that the ContentPageBase is generic, it accepts the associated ViewModel as a type. We then use dependency injection to resolve the ViewModel, here I'm using Autofac and I register the current Navigation instance for this scope. To do this however we need access to IoC container, again it's not pretty but I expose the container as a property on the App object (App.Container
).
To use this as our base class for Views, change the ContentPage
in XAML to ContentPageBase
and provide the ViewModel as the generic type argument. E.g.
<?xml version="1.0" encoding="utf-8" ?>
<mvvm:ContentPageBase xmlns="http://xamarin.com/schemas/2014/forms"
...
xmlns:vm="clr-namespace:MyApp.ViewModels;assembly=MyApp"
xmlns:mvvm="clr-namespace:MyApp.Mvvm;assembly=MyApp"
x:Class="MyApp.Views.AboutPage"
x:TypeArguments="vm:AboutViewModel">
...
</mvvm:ContentPageBase>
Finally, we need to wire up the BindingContext in the code behind. Ideally I'd do this in the ContentPageBase but if you set BindingContext before InitializeComponent is called, weird things can happen. Note that specifying the base class is optional in the code behind (e.g. : ContentPageBase<AboutViewModel>
).
public partial class AboutPage
{
public AboutPage()
{
InitializeComponent();
BindingContext = ViewModel;
}
}
ViewModel Resolution
All MVVM frameworks rely on dependency injection to resolve the ViewModel for a particular view. In ContentPageBase you saw us retrieve the ViewModel from App.Container
, so we need to register all the ViewModels into the container.
public partial class App
{
public static IContainer Container { get; set; }
/// <summary>
/// This default parameterless constructor is needed for Xamarin previewer
/// </summary>
public App() : this(new Bootstrapper())
{
}
public App(Bootstrapper bootstrapper)
{
InitializeComponent();
Container = bootstrapper.CreateContainer();
MainPage = new NavigationPage(new AboutPage());
}
}
Here I'm passing the IoC builder, Bootstrapper
, into the app, which allows the platform specific projects (Android / iOS etc...) to extend the Bootstrapper and register any platform specific dependencies.
public class Bootstrapper
{
public IContainer CreateContainer()
{
var builder = new ContainerBuilder();
RegisterDepenencies(builder);
return builder.Build();
}
protected virtual void RegisterDepenencies(ContainerBuilder builder)
{
builder.RegisterType<SomeRepository>().As<ISomeRepository>().InstancePerLifetimeScope();
RegisterViewModels(builder);
}
private void RegisterViewModels(ContainerBuilder builder)
{
var assembly = typeof(ViewModelBase).GetTypeInfo().Assembly;
var viewModelTypes = assembly.GetLoadableTypes()
.Where(x => x.IsAssignableTo<ViewModelBase>() && x != typeof(ViewModelBase));
foreach (var type in viewModelTypes)
{
builder.RegisterType(type).AsSelf();
}
}
}
The Bootstrapper uses reflection to register all our ViewModels, conveniently just looking up any class that implements ViewModelBase.
Project Structure
Your project structure may look something like this:
MyApp/
│
├─── Models/
| └─── About.cs
├─── Mvvm/
│ ├─── ContentPageBase.cs
│ └─── ViewModelBase.cs
├─── Pages/
│ └─── AboutPage.xaml
├─── ViewModels/
│ └─── AboutViewModel.cs
│
├─── App.xaml
└─── Bootstrapper.cs
Testing the ViewModels
Testing a ViewModel is as simple as newing it up. If you want to test the navigation, simply mock out INavigation. Using Xamarin.Forms.Mocks you can even use the Device library, e.g. Device.BeginInvokeOnMainThread
.
[SetUp]
public void SetUp()
{
Xamarin.Forms.Mocks.MockForms.Init();
}
[Test]
public void HomePage_ShouldNavigateToAboutPage_WhenAboutButtonIsPressed()
{
//Arrange
var navigation = Substitute.For<INavigation>();
var page = Substitute.For<Page>();
var viewModel = new HomeViewModel(navigation);
viewModel.WireEvents(page);
viewModel.Init();
//Act
viewModel.ShowAboutPage();
//Assert
navigation.Received().PushPage(Arg.Any<AboutPage>());
}