As this is the first MvxTutorial, I'm going to create a MvvmCross project from scratch.

I'll show you: how to setup the project, what NuGet packages to add, key parts of the code and to finish I will bind some simple controls. The app I'm going to make is a bottling calculator for home brew beers - I like beer 🍻. This will include an input to enter the amount of beer you have and a couple of labels to show how many bottles you can fill.

The app will be a cross-platform iOS and Android app built with native Xamarin. I will do a follow-up post using Xamarin.Forms, but for now the project will consist of 3 projects:

  • Core - contains the shared code, most of the business logic and viewmodels.
  • iOS - contains the iOS views and bindings from the views to the viewmodels in the "Core"
  • Android - contains the Android views and bindings from the views to the viewmodels in the "Core"

I am going to be building this on a Mac, but the steps are similar if you are using Visual Studio on Windows. I will do a separate post on UWP, MacOS and others but for now, I'm going to focus on iOS and Android.


Core

Project Setup

To start, we need to create a .NetStandard Project, this will be known as the "Core". The core's main function is to share code between the platforms. As we are using an MVVM pattern, we will create our viewmodels here and put business logic in them. This will allow our logic to be shared across the platform projects and keep only the view code in those projects.

From opening Visual Studio select:
New Project... then under .Net Core > Library > .Net Standard Library > Next.

The next section is where we will select the .Net Standard version. Generally speaking, you want your "Core" to have a higher version so it can consume more libraries and use more .Net APIs. However, if you are making a library you will want to use the lowest version possible so that more projects are compatible with your library. In short, follow these rules:

  • App project version should be higher to support more libraries and APIs.
  • Library project version should be lower, so its compatible with more projects.

I'm going to choose .Net Standard 2.0 in the core, as this will allow for greater backwards compatibility with frameworks and libraries that target lower .Net Standard versions. The next stage is to configure the project, this is my config:

Typically the core project follows this naming standard <PROJECT_NAME>.Core to show it is the core.

MvvmCross Setup

The next step is to add the MvvmCross NuGet package to the project. Right click on the Dependencies folder, go to Add Packages..., search for "MvvmCross", look for the package below and click Add Package:

At the time of writing this post version 6.1.2 was the latest, so that is the version I am going. After the package has installed, the readme.txt will be displayed. We are going to follow the steps for the "Core project" in this readme file:

Core project

  1. Add an App class to the root folder (See Core/App.cs.pp in sample files).
  2. Add a ViewModels folder to the root of the project and add at least one ViewModel class to this folder (See Core/HomeViewModel.cs.pp in sample files).

Add an MvxApplication

First, delete the Class1.cs file and add a new file App.cs. Add this code to the newly created App.cs:

using MvvmCross.ViewModels;
using BottlingCalculator.Core.ViewModels;

namespace BottlingCalculator.Core
{
    public class App : MvxApplication
    {
        public override void Initialize()
        {
            RegisterAppStart<HomeViewModel>();
        }
    }
}

The using BottlingCalculator.Core.ViewModels; and the reference to HomeViewModel will show as errors right now as we haven't created them yet. We will fix this in the "Add a ViewModel" step later.

As you can see, we are overriding the Initalize() method on MvxApplication. This method runs when the app starts (on a background thread) after the IoC has been initalised and a few other services. The order in which the services get created can be seen in the MvxSetup class. Inside our Initalize() method, we register the app's starting viewmodel as HomeViewModel with:

RegisterAppStart<HomeViewModel>();

This is a nice convenience method that creates a MvxAppStart object and registers it with the MvvmCross IoC using its interface IMvxAppStart. It also sets HomeViewModel to be the first view model to navigate to in the method NavigateToFirstViewModel(). We will see the Initalize() method being called in our platform setup classes later on.

Add a ViewModel

We now need to fix those errors in App.cs. First, add a "ViewModels" folder and inside this folder add a HomeViewModel.cs file. Now add the following code in HomeViewModel.cs:

using System;
using MvvmCross.ViewModels;

namespace BottlingCalculator.Core.ViewModels
{
    public class HomeViewModel : MvxViewModel
    {
        private double _beerInLitres;
        public double BeerInLitres
        {
            get => _beerInLitres;
            set => SetProperty(ref _beerInLitres, value);
        }
    }
}

Here we have created one property in a MvxViewModel. We are using SetProperty() to set the value of this property. This method is the more efficient at checking wether or not the property needs to be changed. It does this by first checking if the property is different, if so it will raise the RaisePropertyChanging event, change the property, then raise the RaisePropertyChanged event. This method also returns a boolean to indicate wether or not the change has happened:

  • true, when the values are different and the assignation has happened
  • false, when the values are the same and the assignation hasn't happened.

When it returns false, the events RaisePropertyChanging and RaisePropertyChanged don't even get triggered. Sending events with the same change is redundant and can lead to performance issues. When RaisePropertyChanging and RaisePropertyChanged are triggered the bound UI control updates

As property change events are listened to by the bound UI control and when triggered they update the property on that control.

Now we are going to put this into practise, replace the code in HomeViewModel with this:

using System;
using MvvmCross.ViewModels;

namespace BottlingCalculator.Core.ViewModels
{
    public class HomeViewModel : MvxViewModel
    {
        private double _beerInLitres;
        public double BeerInLitres
        {
            get => _beerInLitres;
            set
            {
                if (SetProperty(ref _beerInLitres, value))
                {
                    RaisePropertyChanged(nameof(BeerBottles));
                    RaisePropertyChanged(nameof(Pints));
                    RaisePropertyChanged(nameof(WineBottles));
                    RaisePropertyChanged(nameof(MiniKegs));
                }
            }
        }

        public string BeerBottles  => $"{Math.Round(BeerInLitres / 330, 2)} - beer bottles";

        public string Pints => $"{Math.Round(BeerInLitres / 568, 2)} -  pints";

        public string WineBottles => $"{Math.Round(BeerInLitres / 750, 2)} - wine bottles";

        public string MiniKegs => $"{Math.Round(BeerInLitres / 5000, 2)} - mini kegs";
    }
}

Now we have 4 read-only string properties and a double property. In the setter of BeerInLitres property we are checking the result of SetProperty() to see if we need to alert the other properties of the change. This way you avoid raising properties change events when nothing has actually changed. "Kinda interesting" I hear you say 😏.

At this point, build the project just to check everything is setup correctly. You should have no build errors. Everything working? good! 👍🏻 we can move on to the iOS project.


iOS

Project Setup

Right-click on your solution and go to Add > Add New Project... then under iOS select App > Single View App > Next.

The next section is where we configure the iOS project. I'm going to name the app "BottlingCalc", but name the project "BottlingCalculator.iOS" and use the following settings shown below:

To clean up the new project:

  • Delete the ViewController.cs, ViewController.designer.cs and Main.storyboard - I don't like to use storyboards in my projects as we are not using them as intended. I like to keep with .xib files, it's just personal preference.
  • Open Info.plist. Under Deployment Info, change the Main Interface to LaunchScreen.

Next, add a reference to the core project so we can share that juicy logic code.
To do this, right-click on the "References" folder in the iOS project, select Edit References..., click the "Projects" tab at the top, tick BottlingCalculator.Core and click OK.

MvvmCross Setup

Now that we have the project added, we can add the MvvmCross NuGet package just like in the core. Right click on the Packages folder, go to Add Packages..., search for "MvvmCross",  look for the package below and click Add Package:

Make sure to add the same version as the core (6.1.2). Again the readme.txt should open, but this time we are interested in the iOS section:

iOS projects (ignore if not building for iOS)

  1. Inside AppDelegate.cs, change the AppDelegate class to inherit from MvxApplicationDelegate<MvxIosSetup<Core.App>, Core.App> instead of ApplicationDelegate (See iOS/AppDelegate.cs.pp in sample files).
  2. Still inside AppDelegate.cs, delete all the pre-populated methods to leave a blank AppDelegate class.
  3. Add a Views folder to the root of the project and add at least one View class to this folder to correspond to the ViewModel class in the Core project (See iOS/HomeView.cs.pp in sample files).
  4. Add a new iOS Interface Builder layout (XIB) or StoryBoard file to the Views folder to correspond to the View created in the previous step. (See iOS/HomeView.xib.pp in sample files).

So, open AppDelegate.cs and replace the code with this:

using Foundation;
using MvvmCross.Platforms.Ios.Core;

namespace BottlingCalculator.iOS
{
    [Register("AppDelegate")]
    public class AppDelegate : MvxApplicationDelegate<MvxIosSetup<App>, App>
    {
    }
}

The main difference here compared to the default AppDelegate is the class we are inheriting from, which is MvxApplicationDelegate.

This class registers the setup, MvxIosSetup, with the IoC and sets the presenter to the default MvvmCross iOS presenter.  The presenter controls how the views are going to be displayed. We need to delete all the pre-populated methods in our AppDelegate so that MvvmCross can start up and use them to fire lifetime events.

Create a View

To finish the last steps from the readme.txt. Create a "Views" folder in the iOS project. Once created, right-click on the folder, select Add then New File..., under the iOS category scroll to the bottom, select View Controller and name this file HomeView. This will create 3 files:

  • HomeView.cs - Essentially the "code behind" the interface file.
  • HomeView.designer.cs - An auto-generated designer file used to communicate between the .xib file and the code behind.
  • HomeView.xib - The interface file used to create the UI.

Replace the code in HomeView.cs with this:

using BottlingCalculator.Core.ViewModels;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Platforms.Ios.Presenters.Attributes;
using MvvmCross.Platforms.Ios.Views;

namespace BottlingCalculator.iOS.Views
{
    [MvxRootPresentation(WrapInNavigationController = true)]
    public partial class HomeView : MvxViewController
    {
        public HomeView() : base("HomeView", null)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            var set = this.CreateBindingSet<HomeView, HomeViewModel>();
            set.Bind(BeerInputTextField).To(vm => vm.BeerInLitres);
            set.Bind(BeerBottlesLabel).To(vm => vm.BeerBottles);
            set.Bind(PintsLabel).To(vm => vm.Pints);
            set.Bind(WineBottlesLabel).To(vm => vm.WineBottles);
            set.Bind(KegsLabel).To(vm => vm.MiniKegs);
            set.Apply();
        }
    }
}

There will be errors on InputBeerTextField, BeerBottleLabel, PintsLabel, WineBottleLabel and KegLabel, but we will fix that soon.

As this is our first iOS view there are 3 parts of code I'd like to go over. Firstly:

[MvxRootPresentation(WrapInNavigationController = true)]

This attribute lets the MvvmCross navigation service know that this is the root view and that we want a UINavigationController to wrap this view. You will see this when we run the project, it will have a navigation bar at the top. Next, we are inheriting from MvxViewController which essentially adds data binding to the UIViewController class and passes life cycle events up to the bound viewmodel, e.g. ViewWillAppear calls the viewmodel's ViewAppearing method, etc. I will explain the viewmodel life cycle in another tutorial, but for now we just need to know that the view's life cycle event will also be called in the viewmodel. Finally the binding code, this is where the magic happens ✨. CreateBindingSet will create a binding set between the target, HomeView, and the source, HomeViewModel. The binding set has a list of binding descriptions, MvxBindingDescription, which we can add to by calling Bind() and passing in the target (view). We can then set different properties on the binding description using the following:

  • From() - Sets the target (view) property to bind to. MvvmCross has default properties for most controls, you can see a list of them here for iOS. So, for our labels we don't need to explicitly state that we want to bind to the Text property, e.g.,  For(v => v.Text), as this is already the default.
  • To() - Sets the source (viewmodel) property to bind to.
  • Mode() - Sets what type of binding is set.

To set the binding mode, you can also use these convenience methods:  

  • OneWay() - source (viewmodel) updates sent to the target (view) only.
  • OneWayToSource() - target (view) updates sent to source (viewmodel) only.
  • OneTime() - source (viewmodel) updates sent to the target (view) only once. Can be useful for read-only properties.
  • TwoWay() - Updates sent both ways. Never miss an update 😉, also usually the default.  

Finally calling Apply() on the set prevents it from being garbage collect and finishes the binding code.

Create the iOS UI

First a little bit about how I edit .xib files. I like to edit my .xib files in Xcode, you can try to follow along with the iOS Designer, but I find it doesn't have the same feature parity as Xcode's Interface Builder.

So open HomeView.xib in Xcode either by right-clicking and selecting Open with... > Xcode Interface Builder or by using the amazing Visual Studio for Mac extension by Colby Williams, Default Designer which will default xibs to open in Xcode 😲.

I am going to create a UITextFeild and four UILabels, for simplicity I will put them all in a UIStackView. In a later tutorial I'll go over autolayout, but for now they will be in a UIStackView:

We need to set the text field's keyboard to decimal. Select the text field and press ⌘⎇4, this will open the attributes inspector scroll down to KeyboardType and set it to Decimal Pad.

The next thing to do is to create outlets from the UI controls to the code behind. To do this, with Xcode still open, press ⌘⎇⏎, this will open the Assistant editor. At the top of the new window that has opened, click on "HomeView.m" and select "HomeView.h". Next, hold ctrl and click on the text field and drag this into the HomeView.h file. A pop up should appear, call this outlet BeerInputTextField. Do this for the remaining labels and name them BeerBottlesLabel, PintsLabel, WineBottlesLabel and KegsLabel from top to bottom. Here is an example of how to create the outlets:

If you go back to Visual Studio and check the HomeView.designer.cs you will see it has updated with our outlets. Also if you check HomeView.cs you will notice that the errors are now gone.

At this point run the iOS app, you should get no build errors and once the simulator has started you should see this:


Android

Project Setup

So if you right-click on your solution, go to Add > Add New Project... then under Android select App > Blank Android App > Next.

The next section is where we set the: app name, package name, target Android API version and the theme. I'm going to name the app "BottlingCalc", but name the project "BottlingCalculator.Droid" and select "Modern Development", this sets the minimum Android API version to 16 (Jelly Bean, 4.1). Use the following settings shown below:

To clean up the freshly created project:

  • Delete AboutAssets.txt in the "Assets" folder.
  • Delete AboutResources.txt in the "Resources" folder.

Next, add a reference to the core project so we can share that logic code to the droid too 🤖. To do this, right-click on the "References" folder in the Android project, select Edit References..., click the "Projects" tab at the top, tick BottlingCalculator.Core and click OK.

MvvmCross Setup

Again, add the MvvmCross NuGet package just like in the core project. Right click on the Packages folder, go to Add Packages..., search for "MvvmCross",  look for the package below and click Add Package:

Make sure to add the same version as the core (6.1.2). The readme.txt doesn't open on Android (or at least it did for me 😧), but this is what it says under Android:

Android projects (ignore if not building for Android)

  1. Add a reference to the Mono.Android.Export assembly.
  2. Delete MainActivity.cs, and add a new activity class called SplashScreen to the root folder.
  3. Change the SplashScreen activity class to inherit from MvxSplashScreenActivity<MvxAndroidSetup<Core.App>, Core.App> instead of Activity (See Android/SplashScreen.cs.pp in sample files).
  4. Add a new Android layout to the Resources/layout folder to correspond to the SplashScreen created in the previous step. (See Android/SplashScreen.axml.pp in sample files).
  5. Add a Views folder to the root of the project and add at least one View class to this folder to correspond to the ViewModel class in the Core project (See Android/HomeView.cs.pp in sample files).
  6. Add a new Android layout to the Resources/layout folder to correspond to the View class created in the previous step. (See Android/HomeView.axml.pp in sample files).

Note: If you wish to use the AppCompat versions of Android classes, you can follow the above instructions with the following modifications

  1. Add the MvvmCross v7 Android AppCompat Support Libraries NuGet package (MvvmCross.Droid.Support.v7.AppCompat) to to your Android platform specific project.
  2. When changing the the inheritance for the SplashScreen activity class, use MvxSplashScreenAppCompatActivity instead of MvxSplashScreenActivity.
  3. Add a new XML file that defines an AppCompat theme to the Resources/values folder called styles.xml. (See Android/styles.xml.pp in sample files)
  4. Ensure the theme created in the previous step is referenced in the Attribute for the MainActivity class (Theme = "@style/MainTheme").

So, because I selected "Modern Development" I am also going to run through the AppCompat section too, steps 7-10 in the readme.txt.

To start, right-click on the "References" folder in the Android project, select Edit References..., click on the search bar at the top, type Mono.Android.Export, there should be one result Mono.Android.Export.dll, tick this and click OK. This is needed as some of the packages/libraries we are going to add use the ExportAttribute and ExportFieldAttribute. This allows those packages/libraries to using parts of the native Android libraries that are not exposed in Xamarin.Android, you can read more about this here.

Next, add the MvvmCross.Droid.Support.v7.AppCompat to the project. Right click on the Packages folder, go to Add Packages..., search for "MvvmCross.Droid.Support.v7.AppCompat",  look for the package below and click Add Package. Make sure it's the same version as the core and iOS, version 6.1.2.

In iOS there is already a splash screen created automatically but for Android we need to manually add one. So, to add a splash screen to the app, rename MainActivity.cs to SplashScreen.cs and add the following code:

using Android.App;
using Android.Content.PM;
using BottlingCalculator.Core;
using MvvmCross.Droid.Support.V7.AppCompat;

namespace BottlingCalculator.Droid
{
    [Activity(
        Label = "BottlingCalculator",
        Theme = "@style/MainTheme",
        MainLauncher = true,
        NoHistory = true,
        ScreenOrientation = ScreenOrientation.Portrait)]
    public class SplashScreen : MvxSplashScreenAppCompatActivity<MvxAppCompatSetup<App>, App>
    {
        public SplashScreen()
            : base(Resource.Layout.SplashScreen)
        {
        }
    }
}

As you can see, we are inheriting from MvxSplashScreenAppCompatActivity. This registers the default Android setup, MvxAppCompatSetup, with the IoC and sets the presenter to the default MvvmCross Android presenter.

If you take a look at the method OnCreate() in  MvxSplashScreenAppCompatActivity you can see that it calls  InitializeAndMonitor() on our Android setup this starts the setup and initialises all the MvvmCross components.

We will have an error on the SplashScreen layout as the file has not been created yet. So to fix this, rename "Resources/layout/Main.axml" to SplashScreen.axml and change its contents to the code below: (Don't try to delete this file as it seems to crash Visual Studio for Mac 🤷🏻‍♂️):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Loading...." />
</LinearLayout>

All we have defined in this layout, is a screen that has a label which says "Loading..." This is good enough for my app, but you may need to change this to suit your needs.

The next step is to add a AppCompat theme. So add a new XML file called styles.xml in "Resources/values", XML files are under the XML category in the create new file popup. Add this code to the file:

<?xml version="1.0" encoding="utf-8" ?>
<resources>

  <style name="MainTheme" parent="MainTheme.Base">
  </style>
  <!-- Base theme applied no matter what API -->
  <style name="MainTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
    <!--If you are using revision 22.1 please use just windowNoTitle. Without android:-->
    <item name="windowNoTitle">true</item>
    <!--We will be using the toolbar so no need to show ActionBar-->
    <item name="windowActionBar">false</item>
    <!-- Set theme colors from http://www.google.com/design/spec/style/color.html#color-color-palette -->
    <!-- colorPrimary is used for the default action bar background -->
    <item name="colorPrimary">#2196F3</item>
    <!-- colorPrimaryDark is used for the status bar -->
    <item name="colorPrimaryDark">#1976D2</item>
    <!-- colorAccent is used as the default value for colorControlActivated
         which is used to tint widgets -->
    <item name="colorAccent">#FF4081</item>
    <!-- You can also set colorControlNormal, colorControlActivated
         colorControlHighlight and colorSwitchThumbNormal. -->
    <item name="windowActionModeOverlay">true</item>

    <item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
  </style>

  <style name="AppCompatDialogStyle" parent="Theme.AppCompat.Light.Dialog">
    <item name="colorAccent">#FF4081</item>
  </style>
</resources>

This is the default style file, each property has a description detailing what it does. I would change the colors to your app's color palette or use the nice tool Google has made.

Create a View

Now we will create our first view, HomeView and bind the controls to the viewmodel. Add a "Views" folder to the root of the Android project. Then Add a new class called "HomeView.cs". Add this code in to the HomeView:

using Android.App;
using Android.OS;
using MvvmCross.Platforms.Android.Presenters.Attributes;
using MvvmCross.Platforms.Android.Views;

namespace BottlingCalculator.Droid.Views
{
    [MvxActivityPresentation]
    [Activity(Label = "View for HomeViewModel", 
              Theme = "@style/MainTheme")]
    public class HomeView : MvxActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.HomeView);
        }
    }
}

The first thing to point out in this code is the Attribute [MvxActivityPresentation] this allows the Android presenter to detect that you want this view to be presented as an activity. Next you can see that we are inheriting from MvxActivity, this class is essentially an activity, but with extra methods to help with the Mvvm pattern. It includes creating data contexts to allow for binding and reporting life cycle events to the views view model, e.g., ViewAppeared, ViewCreated, etc.

The line with Resource.Layout.HomeView will show as an error until we add the layout file in the next section. So lets fix this.

Create the Android UI

Right-click on the "Resources/layout" folder, click Add > New File... then under Android, click Layout and call this file "HomeView". This will add a HomeView.axml file. Now add this code to this file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="40dp"
        local:MvxBind="Text BeerInLitres" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="40dp"
        local:MvxBind="Text BeerBottles" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="40dp"
        local:MvxBind="Text Pints" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="40dp"
        local:MvxBind="Text WineBottles" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="40dp"
        local:MvxBind="Text MiniKegs" />
</LinearLayout>

I'll run through the components we have added and where we have bound properties from the view model. Firstly, we have add a EditText control, which is bound to the BeerInLitres property on our view model. The default binding code is local:MvxBind="Text BeerInLitres", this binds the Text property of the control to the BeerInLitres property on our view model, by default this binding is two way so it will receive property changed updates in both View and ViewModel properties. So if the user types in the TextView the viewmodel will get updated and if the value gets changed by the viewmodel code the UI will update.

Next we have added four TextViews which are bound in a similar fashion to the following properties: BeerBottles, Pints, WineBottles and MiniKegs.

Now that you have everything setup for the Android project go ahead a run the project on a emulator or device. You should see something like this:


Summary

So in summary, we have created a .Net Standard Core project which contains the logic that can be shared with the platform projects. We also created two platform projects, iOS and Android, that use the shared logic to bind to UI components which display the required data. Also, you now have an understanding as to how MvvmCross starts up. As a final note, I would like to add some recommendations to two templates. These can help you get started with MvvmCross quickly, but I like to set it up from scratch just so I know what is added.

Some template projects that I personally like are:

If you are going to use these templates, check they are using the version of MVVMCross that you want to use and the dependencies they add are ones you want for your project.

The code for this tutorial can be seen on my GitHub here. Well I hope you enjoyed the first MvxTutorial and see you at the next one!