/xamarin-forms-mvvm-adaptor

ViewModel-first mvvm framework for Xamarin.Forms. Lightweight, it adapts Xamarin's existing mvvm engine.

Primary LanguageC#OtherNOASSERTION

Logo Xamarin Forms Mvvm Adaptor

ViewModel-First Mvvm framework for Xamarin.Forms. Lightweight, it adapts Xamarin's existing Mvvm engine.

NuGet Coverage Build Status Dev. Feed

Why?

This library was inspired by the Enterprise Application Patterns Using Xamarin.Forms eBook.

Xamarin has fantastic Mvvm functionality, however the pattern is geared towards View-First navigation. I prefer to keep the View "dumb", and put all my logic in the ViewModel. This library allows you to use the Xamarin engine with a ViewModel-First pattern, keeping a strong separation of concerns and more readable code. Using this library, you will find that its rare that you need to put anything in your .xaml.cs code-behind files.

Features:

  • Familiar syntax, e.g. PushAsync<TViewModel>() is similar to Xamarin's PushAsync(Page page)
  • Easily pass data to the pushed view-model with PushAsync<TViewModel>(object navigationData)
  • OnAppearingAsync() method within the pushed view-model is triggered automatically when its associated page appears at the top of the stack.
  • Supports full dependency injection with the DI engine of your choice.
  • Supports multiple navigation-stacks. Useful if you want each tab of a TabbedPage to have its own separate navigation tree.
  • Lightweight because its adapts Xamarin's existing engine.

Getting Started

MvvmAdaptor can be consumed in two flavours:

  • Vanilla implementation
  • With DI implementation (using Dependency Injection / IoC Engine of your choice)

Skip to the flavour that suits you or check out the the Sample App.

Vanilla implementation

Instantiate the controller as a single instance. One way to do this is with a static property in your app.xaml.cs file:

public partial class App : Application
{
  public static NavController NavController { get; } = new NavController();

You would access the NavController by calling App.NavController anywhwere in your application.

Another way would be to use a dependency service. If you prefer this approach, you will probably be consuming the Dependency Injection (DI) flavour.

Keep to the following naming conventions. The MvvmAdaptor works by assuming that you name your View and ViewModel classes consistently. The default expectation is that:

  • Views end with the suffix 'Page', and view-models with the suffix 'ViewModel'. For example MainPage and MainViewModel.
  • All views are in the .Views sub-namespace, and view-models in the .ViewModels sub-namespace. For example: MainPage will be in the MyApp.Views namespace, and MainViewModel will be in the MyApp.ViewModels namespace.

You can change the expected naming convention to your personal style with the SetNamingConventions() method.

ViewModels must implement the IAdaptorViewModel interface. The easiest way to achieve this is to extend the AdaptorViewModel. This has the added benefit of including all the mvvm boilerplate code and more because it uses James Montemagno's MvvmHelpers as a dependency.

public class MainViewModel : XamarinFormsMvvmAdaptor.AdaptorViewModel
	{

Alternatively, you can roll your own BaseViewModel. Note that the two interface methods:

  • Are marked virtual so that you can override them in your derived view-model classes.
  • Return Task.FromResult(false); as the default implementation.
public abstract class BaseViewModel : IAdaptorViewModel
{
    public virtual Task InitializeAsync(object navigationData)
    {
        return Task.FromResult(false);
    }

    public virtual Task OnAppearingAsync()
    {
        return Task.FromResult(false);
    }
  
  // Add your own implementations of INotifyPropertyChanged, and custom code
  // ...
}

In your view-model instance, if your override implementation of InitializeAsync() or OnAppearingAsync() is synchronous, you can just return the task base.InitializeAsync(null):

public override Task InitializeAsync(object navigationData)
{
  DoSomeSynchronousWork(navigationData as MyDocument);
    
  return base.InitializeAsync(null);
}

Bind to your view-model as you normally would with Xamarin. I prefer to link in the xaml file so that intellisense picks up your bindings.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             ... 
             xmlns:vm="clr-namespace:MyApp.ViewModels"/>
  <ContentPage.BindingContext>
      <vm:MainViewModel/>
  </ContentPage.BindingContext>
	
	<ContentPage.Content>
    ...

Initialize the RootViewModel by running InitAsync() in the OnStart() override.

And set you app's MainPage to your NavController's NavigationRoot.

public partial class App : Application
{
	...	
  protected override async void OnStart()
  {
    //Important to initialize before setting the MainPage
    await NavController.InitAsync(new MainPage());
    MainPage = NavController.NavigationRoot;
  }

Start Navigating!

Navigate forwards from your view-models as follows:

//To Push
await App.NavController.PushAsync<DetailViewModel>();
//or, if you want to pass data
await App.NavController.PushAsync<DetailViewModel>(listItem);

//MODAL NAVIGATION
await App.NavController.PushModalAsync<DetailViewModel>();

Navigate backwards with pop:

//To Pop
await App.NavController.PopAsync();

//MODAL NAVIGATION
await App.NavController.PopModalAsync();

Checkout the Navigation section for a menu of the additional navigation properties and methods.

Checkout the WordJumble Sample App for a simple example that uses this framework.


DI implementation

If you look at the WordJumble Sample App, you will notice that the DI-flavour looks like more work than the Vanilla option. This is the price you pay for a more testable, and more maintainable app.

I could have trimmed some of the code requirement if I bundled a DI-engine into the MvvmAdaptor. Instead, I believe its more important to let you work with your DI-engine of choice. Here, I use AutoFac as an example.

Keep to the following naming conventions. The MvvmAdaptor works by assuming that you name your View and ViewModel classes consistently. The default expectation is that:

  • Views end with the suffix 'Page', and view-models with the suffix 'ViewModel'. For example MainPage and MainViewModel.
  • All views are in the .Views sub-namespace, and view-models in the .ViewModels sub-namespace. For example: MainPage will be in the MyApp.Views namespace, and MainViewModel will be in the MyApp.ViewModels namespace.

You can change the expected naming convention to your personal style with the SetNamingConventions() method.

ViewModels must implement the IAdaptorViewModel interface. The easiest way to achieve this is to extend the AdaptorViewModel. This has the added benefit of including all the mvvm boilerplate code (and more) because it uses James Montemagno's MvvmHelpers as a dependency.

public class MainViewModel : XamarinFormsMvvmAdaptor.AdaptorViewModel

Alternatively, you can roll your own BaseViewModel. Note in the example below that the two interface methods:

  • Are marked virtual so that you can override them in your derived view-model classes.
  • Return Task.FromResult(false); as the default implementation.
public abstract class BaseViewModel : IAdaptorViewModel
{
    public virtual Task InitializeAsync(object navigationData)
    {
        return Task.FromResult(false);
    }

    public virtual Task OnAppearingAsync()
    {
        return Task.FromResult(false);
    }
  
  // Add your own implementations of INotifyPropertyChanged, and custom code
  // ...
}

In your view-model instance, if your override implementation of InitializeAsync() or OnAppearingAsync() is synchronous, you can just return the task base.InitializeAsync(null):

public override Task InitializeAsync(object navigationData)
{
  DoSomeSynchronousWork(navigationData as MyDocument);
    
  return base.InitializeAsync(null);
}

Use DI in your ViewModel constructors. Here is an example from the WordJumble Sample App. Note that the NavController itself is injected as a dependency, allowing you to easily stub this service in your tests.

public class JumbleViewModel : XamarinFormsMvvmAdaptor.AdaptorViewModel
{
  readonly IFlexiCharGeneratorService flexiCharGenerator;
  readonly INavController navController;

  //ViewModel Constructor with Dependency Injection
  public JumbleViewModel(INavController navController, 
                         IFlexiCharGeneratorService flexiCharGeneratorService)
    {
        flexiCharGenerator = flexiCharGeneratorService;
        this.navController = navController;
    ...

No need to set the binding-context in your Views. MvvmAdaptor will do this for you.

Tip: If you want intellisense to pick up bindings in your xaml, you need to give it a view-model. You can use design time data to do this. Notice the :d namespace in the snippet below. If you go this route, you will need two constructors in your view-models. An empty one to satisfy the xaml, and your primary one with all the dependencies. Autofac works on the principle of "greediest" constructor, so it will pick the right one automatically. Most DI engines follow this default.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:vm="clr-namespace:MyApp.ViewModels"/>
<!-- This is optional -->
  <d:ContentPage.BindingContext>
      <vm:MainViewModel/>
  </d:ContentPage.BindingContext>
	
	<ContentPage.Content>
    ...

Set up the DI container. Make it accessible throughout your project as a single instance. One way to do this is with a static property in your app.xaml.cs file:

public partial class App : Application
{
  //This will be accessible throughout the app
  public static IContainer DiContainer { get; private set; }

public App()
{
  InitializeComponent();

  // Set up the container for Dependency Injection
  var builder = new ContainerBuilder();
  // Add services
  builder.RegisterType<FlexiCharGeneratorService>()
    .As<IFlexiCharGeneratorService>().SingleInstance();
  // Add view-models
  builder.RegisterType<MainViewModel>().AsSelf();
  builder.RegisterType<JumbleViewModel>().AsSelf();
  builder.RegisterType<FlexiCharDetailViewModel>().AsSelf();
  // Add the NavController
  builder.RegisterType<NavController>()
    .As<INavController>().SingleInstance();
  DiContainer = builder.Build();
}

You can now access the DiContainer by calling App.DiContainer anywhwere in your application.

Initialize the RootViewModel by running DiInitAsync() in the OnStart() override.

And set you app's MainPage to your NavController's NavigationRoot.

public partial class App : Application
{
	...	
  protected override async void OnStart()
  {
    var navController = DiContainer.Resolve<INavController>();
    //Important to initialize before setting MainPage
    await navController.DiInitAsync(DiContainer.Resolve<MainViewModel>());
    MainPage = navController.NavigationRoot;
  }

Start Navigating!

Navigate forwards from your view-models as follows:

public class JumbleViewModel : XamarinFormsMvvmAdaptor.AdaptorViewModel
{
  ...
  readonly INavController navController;

  //Constructor
  public JumbleViewModel(INavController navController)
  {
    this.navController = navController;
  }

  async Task Navigate()
  {
    ...
    //To Push
    await navController.DiPushAsync(
      App.DiContainer.Resolve<FlexiCharDetailViewModel>());

    //or, if you want to pass data
    await navController.DiPushAsync(
      App.DiContainer.Resolve<FlexiCharDetailViewModel>(),navigationData);

    //MODAL NAVIGATION
    await navController.DiPushModalAsync(
      App.DiContainer.Resolve<FlexiCharDetailViewModel>());

Navigate backwards with pop:

//To Pop
await navController.PopAsync();

//Pop Modal
await navController.PopModalAsync();

Checkout the Navigation section for a menu of the additional navigation properties and methods.

Checkout the WordJumble Sample App for a simple example that uses this framework.


Navigation

The Navigation framework simply follows the Xamarin.Forms approach. If you are unfamiliar or rusty, please refer to Microsoft's docs on Performing Navigation (10min read). In addition to the familar methods, I have added some additional stack manipulation helpers.

Available in Xamarin.Forms:

  • Properties
    • NavigationStack (called MainStack here)
    • ModalStack
    • RootPage
    • CurrentPage
  • Methods:
    • PushAsync & PushModalAsync
    • PopAsync & PopModalAsync
    • InsertPageBefore
    • RemovePage

Additional helpers:

  • Properties :
    • TopPage
    • HiddenPage (see detail below for explaination) in addition to RootPage and TopPage
    • RootViewModel, TopViewModel, and HiddenViewModel corresponding to the pages above
  • Extension methods on the MainStack and ModalStack
    • GetCurrentPage() and GetCurrentViewModel()
    • GetPreviousPage() and GetPreviousViewModel()
  • Methods:
    • CollapseMainStack()
    • RemovePreviousPageFromMainStack()
  • Static methods that don't need a NavController instance:
    • DiCreatePageForAsync(IAdaptorViewModel viewmodel)

Shortcuts for accessing Pages and ViewModels in the stack

The image below represents a stack of four pages, with labels corresponding to properties that allow you to access these pages or their corresponding view-models. The Top, and Root pages are self-explanatory. The Hidden page is always beneath the Top page. It is 'hidden' by the top page, and will always be the first to appear when the Top page is popped off the stack.

Stack

This last point is important to consider if you have pages in a modal stack. Remember that the modal-stack always hides the navigation-stack. If there is one page in the modal-stack, the hidden page is at the top of the navigation-stack (see below), as it will appear next when the modal stack is popped.

Stack

If the modal stack has two pages, the 'hidden' page is beneath the top page of the modal-stack (figure below) because it will appear next when the top modal page is popped.

Stack

For any other pages you can always specify the index of the stack:

// To access a page
Page page = NavController.MainStack[3];

// To access it's corresponding viewmodel
var viewModel = page.BindingContext as IAdaptorViewModel;

Sample App

WordJumble

On the MainPage the user types a four letter word into an entry dialogue. A new page appears with the word's letters jumbled randomly on the page. The user can tap a letter to open a dialogue which lets them rotate the letter differently.

iOS Droid
ScreenShot iOS ScreenShot Droid

WordJumble demonstrates both flavours of the XamarinFormsMvvmAdaptor. When browsing the code, look for the conditional compilation symbol #if WITH_DI for the the DI flavour of XamarinFormsMvvmAdaptor. If building the app, be aware of what configuration you are building.

This app demonstrates:

  • Initialising the Navigation Controller
  • Push a page onto the stack from the view-model
    • Passing data to that page (the user-entered word),
    • Triggering the WordJumbleViewModel's InitializeAsync() method
  • Push a modal page onto the modal stack
    • Again passing data to the modal page
  • OnAppearing() triggered in the WordJumbleViewModel when the user has finished rotating the letter in the modal dialogue
  • Popping of pages

In addition to XamarinFormsMvvmAdaptor, the sample uses the following features which you may or may-not be familiar with: