Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

Getting Started: MvvmCross with Xamarin Forms (Part 2)

In my previous post I covered the first part of this post on Getting Started with MvvmCross and Xamarin Forms where I covered the initial steps in getting a new Xamarin Forms project started. In this post I’m going to continue on and show how you can configure a Xamarin Forms solution to make use of MvvmCross.

Before I get started with MvvmCross I’m going to add a new project which will contain all my ViewModels. Whilst not entirely necessary, particularly with Xamarin Forms where the views/pages are in a .NET Standard library, it’s good practice to completely separate your ViewModels away from the views/pages to avoid any accidental interdependencies forming. I’ll add a new project based on the .NET Standard class library template.

image_thumb11[1]

For this library I’m going to adjust the .NET Standard version back to 1.0 – I prefer to start with a low .NET Standard version and increase it only when I need to take advantage of features in the higher versions. This ensures that the library can be referenced by the widest set of targets platforms.

image_thumb11

I’ll add a reference to the new project to each of the other projects in the solution.

image_thumb31

The next step is to add a reference to the MvvmCross NuGet package. Currently MvvmCross is still distributed as a set of Portable Class Libraries and if we attempt to add the NuGet package to either our MvvmcrossGettingStarted or MvvmcrossGettingStarted.Core projects, we’ll get an error as they’re both .NET Standard library. What’s annoying about this is that the MvvmCross PCLs are fully compatible with .NET Standard, meaning that it should be possible to add a reference to them. Unfortunately Visual Studio isn’t clever enough to be able to resolve this, and as such we need to adjust the csproj files for both projects before attempting to add a reference to MvvmCross.

Add the following line into the first PropertyGroup of the csproj files for both MvvmcrossGettingStarted or MvvmcrossGettingStarted.Core projects. One saving grace is that it’s now easy in Visual Studio to edit a csproj by right-clicking on the project and selecting “Edit <project name>.csproj”.

<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wpa81</PackageTargetFallback>

Eg.

image_thumb23

Next we can go ahead and add a reference to MvvmCross to all our projects. Right-click on the solution node in Solution Explorer and select Manage NuGet Packages for Solution, and then search for mvvmcross. Select the most recent stable version of MvvmCross (this image is a little old as the version is at 5.6.3 at time of writing this)

image_thumb9

In addition to the main MvvmCross package, we also want to add in the Xamarin Forms support library, MvvmCross.Forms. Note that we do not add this to the MvvmcrossGettingStarted.Core project – this is the separation of concerns we setup at the beginning of this post to ensure there is no dependencies on the viewing technology within our ViewModels.

image_thumb21

Now that we have added the references to MvvmCross there are a bunch of small changes we need to apply to our application in order to get it all up and running. We’ll start in the MvvmcrossGettingStarted.Core project where we need to create two classes.

The first class we’ll create inherits from MvxApplication and is used to setup the application within the ViewModel world. MvvmCross has an opinionated navigation model whereby navigation is defined at a ViewModel level, and simply implemented at a View level. As such the MvxApplication class, in this case GettingStartedApplication, defines the first ViewModel for the application.

public class GettingStartedApplication : MvxApplication
{
     public override void Initialize()
     {
         RegisterNavigationServiceAppStart<MainViewModel>();
     }
}

The second class is the ViewModel that matches the first view or page of the application. MainPage was created back when we created the Xamarin Forms application, so we’ll create a class called MainViewModel. Whilst you can override the default view to viewmodel mapping in MvvmCross, it’s preconfigured to align views and viewmodels based on a naming convention. I typically stick with XXXViewModel and XXXPage but XXXView is also supported out of the box.

In this case MainViewModel exposes a simple property that we’ll data bind to later to show that the Page and ViewModel have been glued together correctly.

public class MainViewModel: MvxViewModel
{
     public string WelcomeText => "Welcome to my data bound Xamarin Forms + MvvmCross application!";
}

Now we’ll switch over to the MvvmcrossGettingStarted project and make some changes to both the App and MainPage classes.

In the App.xaml, we need to change the root element to reference MvxFormsApplication

<?xml version="1.0" encoding="utf-8" ?>
<mvx:MvxFormsApplication xmlns="
http://xamarin.com/schemas/2014/forms"
                     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                     xmlns:mvx="clr-namespace:MvvmCross.Forms.Platform;assembly=MvvmCross.Forms"
                    
x:Class="MvvmcrossGettingStarted.App">
     <Application.Resources>
     </Application.Resources>
</mvx:MvxFormsApplication>

And in App.xaml.cs, remove the inheritance – the Microsoft templates insist on including the inheritance in both the xaml and xaml.cs files which is quite unnecessary and should be removed.

public partial class App
{
     public App()
     {
         InitializeComponent();
     }
}

We need to make a similar change to MainPage.xaml, changing the root element to MvxContentPage. We’ll also change the Label to use data binding to return the WelcomeText property from the MainViewModel.

<?xml version="1.0" encoding="utf-8" ?>
<mvx:MvxContentPage xmlns="
http://xamarin.com/schemas/2014/forms"
                     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                     xmlns:local="clr-namespace:MvvmcrossGettingStarted"
                     xmlns:mvx="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
                    
x:Class="MvvmcrossGettingStarted.MainPage">
     <Label Text="{Binding WelcomeText}"
            VerticalOptions="Center"
            HorizontalOptions="Center" />
</mvx:MvxContentPage>

Again, remove the inheritance specified in MainPage.xaml.cs

public partial class MainPage
{
     public MainPage()
     {
         InitializeComponent();
     }
}

The next step involves adding a Setup class to each of the head projects, and then creating an instance of the Setup class to invoke MvvmCross when the application starts up.

UWP

The UWP Setup inherits from MvxFormsWindowsSetup and unlike the Android and iOS Setup classes, the UWP Setup needs to override the default log behaviour by setting the log provider type to None and then creating an instance of the EmptyVoidLogProvider  (the implementation of this is coming up soon) – this should be fixed in a future MvvmCross version.

public class Setup : MvxFormsWindowsSetup
{
     public Setup(Frame rootFrame, LaunchActivatedEventArgs e) : base(rootFrame, e)
     {
     }

    protected override MvxLogProviderType GetDefaultLogProviderType() => MvxLogProviderType.None;

    protected override IMvxLogProvider CreateLogProvider() => new EmptyVoidLogProvider();

    protected override IEnumerable<Assembly> GetViewAssemblies()
     {
         return new List<Assembly>(base.GetViewAssemblies().Union(new[] { typeof(MvvmcrossGettingStarted.App).GetTypeInfo().Assembly }));
     }

    protected override MvxFormsApplication CreateFormsApplication() => new MvvmcrossGettingStarted.App();

    protected override IMvxApplication CreateApp() => new Core.GettingStartedApplication();
}

Now in App.xaml.cs we need to replace

Xamarin.Forms.Forms.Init(e);

with

var setup = new Setup(rootFrame, e);
setup.Initialize();

And in Main.xaml.cs replace

LoadApplication(new MvvmcrossGettingStarted.App());

with

var start = Mvx.Resolve<IMvxAppStart>();
start.Start();

var presenter = Mvx.Resolve<IMvxFormsViewPresenter>() as MvxFormsUwpViewPresenter;
LoadApplication(presenter.FormsApplication);

Finally, we need to add the EmptyVoidLogProvider

public class EmptyVoidLogProvider : IMvxLogProvider
{
     private readonly EmptyVoidLog voidLog;

    public EmptyVoidLogProvider()
     {
         voidLog = new EmptyVoidLog();
     }

    public IMvxLog GetLogFor<T>()
     {
         return voidLog;
     }

    public IMvxLog GetLogFor(string name)
     {
         return voidLog;
     }

    public IDisposable OpenNestedContext(string message)
     {
         throw new NotImplementedException();
     }

    public IDisposable OpenMappedContext(string key, string value)
     {
         throw new NotImplementedException();
     }

    public class EmptyVoidLog : IMvxLog
     {
         public bool Log(MvxLogLevel logLevel, Func<string> messageFunc, Exception exception = null, params object[] formatParameters)
         {
             return true;
         }
     }
}

Now when we build and run the UWP project we can see that the MainPage is shown and is data bound to the MainViewModel.

image

iOS

The iOS Setup is the simplest out of the three platforms.

public class Setup : MvxFormsIosSetup
{
     public Setup(IMvxApplicationDelegate applicationDelegate, UIWindow window)
         : base(applicationDelegate, window)
     {
     }

    protected override IEnumerable<Assembly> GetViewAssemblies()
     {
         return new List<Assembly>(base.GetViewAssemblies().Union(new[] { typeof(MvvmcrossGettingStarted.App).GetTypeInfo().Assembly }));
     }

    protected override MvxFormsApplication CreateFormsApplication() => new MvvmcrossGettingStarted.App();

    protected override IMvxApplication CreateApp() => new Core.GettingStartedApplication();
}

In AppDelegate we need to change the inheritance from  global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate to MvxFormsApplicationDelegate and change the FinishedLaunching method as follows:

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
     Window = new UIWindow(UIScreen.MainScreen.Bounds);

    var setup = new Setup(this, Window);
     setup.Initialize();

    var startup = Mvx.Resolve<IMvxAppStart>();
     startup.Start();

    LoadApplication(setup.FormsApplication);

    Window.MakeKeyAndVisible();

    return base.FinishedLaunching(app, options);
}

Now we’re good to build and run the iOS project

image

Android

Lastly, add Setup to the Android project. Note this is slightly different from the iOS and UWP projects in that the GetViewAssemblies method excludes the assembly for the Android head project. This is to avoid the MainActivity being added as a view, that based on our naming convention would match with MainViewModel giving a duplicate when attempting to resolve the View that should be rendered.

public class Setup : MvxFormsAndroidSetup
{
     public Setup(Context applicationContext) : base(applicationContext)
     {
     }

    protected override IEnumerable<Assembly> GetViewAssemblies()
     {
         return new List<Assembly>(base.GetViewAssemblies()
             .Union(new[] { typeof(MvvmcrossGettingStarted.App).GetTypeInfo().Assembly })
             .Except(new[] {this.GetType().Assembly})
             );
     }

    protected override MvxFormsApplication CreateFormsApplication() => new MvvmcrossGettingStarted.App();

    protected override IMvxApplication CreateApp() => new Core.GettingStartedApplication();
}

The MainActivity needs to be updated to change its inheritance from global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity to MvxFormsAppCompatActivity<MainViewModel>. And the OnCreate needs to be updated to

[Activity(Label = "MvvmcrossGettingStarted", Icon = "@drawable/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : MvxFormsAppCompatActivity<MainViewModel>
//global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
     protected override void OnCreate(Bundle bundle)
     {
         TabLayoutResource = Resource.Layout.Tabbar;
         ToolbarResource = Resource.Layout.Toolbar;

        base.OnCreate(bundle);

        var startup = Mvx.Resolve<IMvxAppStart>();
         startup.Start();
         InitializeForms(bundle);

     }
}

Finally the last platform, Android, is good to build and run. Note however that Android has a tendency to be a pain and that after setting everything up correctly you may still run into issues building, deploying and running. Before you waste hours looking at your code to see what you’ve done wrong, make the assumption that the tools are crap – delete both bin and obj folders from the Android head project, and uninstall the application from the device/emulator (assuming it has already been installed). Try building and running again – if this still fails, then you may indeed have something wrong with your code!

image

Comments are closed