Xamarin, Xamarin.Forms

What’s Missing in Xamarin.Forms MVVM Support

One thing that has always bothered me about Xamarin.Forms development is that it has an opinionated encouragement for MVVM, but its supported implementation for MVVM has always felt incomplete. This has caused a number of supporting MVVM frameworks to sprout up.

I will not argue against using MVVM for Xamarin.Forms applications as I do believe it is the most efficient and productive way of developing with it. This is largely due to that “opinionated encouragement for MVVM” I mentioned initially, which is manifested largely by its support and encouragement to use Data Binding: the magic-sauce for MVVM support.


Aside:
For those interested in developing with Xamarin.Forms without MVVM, Fabulous is an officially supported framework to build Xamarin.Forms applications with F# and MVU (Model-View-Update).
https://fsprojects.github.io/Fabulous/


After developing with a few different MVVM frameworks over the years, I came to desire to use something simpler and much closer to the base API surface and functionality that Xamarin.Forms provides. This was driven by a few different factors:

  • Bringing on new developers and passing off applications to support teams can be costly. Additional frameworks lead to the overhead of training people on these frameworks and how to use them properly.
  • Any additional MVVM framework introduces additional risk for issues and bugs, some of which manifest in fundamental changes in how Xamarin.Forms is supposed to operate.
  • I found the main benefit(s) of using these frameworks were actually unnecessary in the vast majority of cases.

In essence, what I realized was that there was little benefit provided by these frameworks. Any benefit that did occur was generally outweighed by the points above.

My focus here is not to call out any specific issues with any specific frameworks, but to provide a viable and productive alternative by filling in the gaps yourself. If you have interest in additional posts about the various MVVM frameworks, please reply in the comments below or hit me up on Twitter.

In order to solve this issue, we must first define what these “gaps” are. Generally, I found they revolved around the missing support for an official Xamarin.Forms ViewModel. For anyone doing MVVM long enough, it’s always been expected of you to write your own ViewModelBase for INotifyPropertyChanged support.


Aside:
I’m hoping the introduction of default interface implementations with C# 8 will cause Microsoft to add a basic implementation to System.ComponentModel.INotifyPropertyChanged in the future.


Here is a common implementation of INotifyPropertyChanged and the one I am using.

public abstract class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(storage, value))
        {
            return false;
        }

        storage = value;
        OnPropertyChanged(propertyName);

        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

}

What I largely found to be lacking is support for Navigation and DisplayAlert outside of the Page as well as passing arguments to a ViewModel during navigation. Honestly, this is primarily what I used other MVVM frameworks for. My challenge was to solve this in the simplest way possible without changing how these features of Xamarin.Forms function.

A heavy-handed approach would be to allow all navigation and alert displays to still occur in the Page class, but to provide additional Data Binding and support between the ViewModel and the Page. This seemed like a terrible way to go…

In the end and for simplicity, I settled on a necessary evil: to provide a cyclic reference. This means that the Page has a reference to the ViewModel as it is the BindingContext for the Page while the ViewModelBase has a reference to the Page in order to access the API surface of the Page (albeit in a controlled manner). The “evilness” was offset by referencing the Page as a WeakReference<T>.

public abstract class ViewModelBase : NotifyPropertyChanged
{     
    private WeakReference<ContentPage> _page;
    protected ContentPage Page => _page.TryGetTarget(out ContentPage target) ? target : null;
    public void SetWeakPage(ContentPage page) => _page = new WeakReference<ContentPageBase>(page);
}

Now, we have a ViewModelBase that has an API surface that allows us to specify a Page for the ViewModel. This gives us the gift of being able to access and aggregate the API surface of the Page within the ViewModel. You’ll notice that I have the page marked as protected, so the full API surface of it is accessible, but this can easily be made private (and arguably should be).

The boiler plate for creating a page in this way may look like this:

var page = new MyPage();
var vm = new MyViewModel();
vm.SetWeakPage(page);
page.BindingContext = vm;

In a future post, well update to add in dependency injection and complex-type parameter passing.

Since we now have the Page accessible within the ViewModel, we can provide controlled access to the Navigation instance of the Page and the API surface for displaying alerts, all without having to create separate service abstractions. Starting with alerts, we’ll add the following to our ViewModelBase:

public Task DisplayAlert(string title, string message, string cancel)
    => Page?.DisplayAlert(title, message, cancel);

public Task<bool> DisplayAlert(string title, string message, string accept, string cancel)
     => Page?.DisplayAlert(title, message, accept, cancel);

public Task<string> DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons)
    => Page?.DisplayActionSheet(title, cancel, destruction, buttons);

For Navigation, things become more complex. The Navigation APIs expect an instance of a Page. We shouldn’t expect an implementing ViewModel to use our boiler plate over-and-over again to construct a Page to pass through to the Navigation’s API call. Therefore, we’ll create a method that will do the construction for us. However, we’ll also need access to this from the Application class when we bootstrap the Forms application. Therefore, I put the implementation of this in an ApplicationBase class, which I also used for other reasons that I’ll get into in future posts.

public abstract class ApplicationBase : Application
{
    public Page CreatePage<TPage, TViewModel>()
        where TPage : ContentPage
        where TViewModel : ViewModelBase
    {
        var vm = default(TPage);
        var page = default(TViewModel);

        vm.SetWeakPage(page);
        page.BindingContext = vm;
        
        return page;
    }
}

Side Note:
The main reason I created the ApplicationBase class was to re-use this implementation in multiple apps. The point I’m trying to stress here is that it is not difficult to bridge the MVVM gaps created by Xamarin.Forms without a separate MVVM framework. Yes, I see the irony in that it appears that I am building Yet Another MVVM Framework. 🙂 To that end, I am not publishing this as a standalone framework and am hoping to find a way to bolster the core Xamarin.Forms project with these lessons.


With our CreatePage method in place, we can add a controlled API surface to our ViewModelBase for Navigation that utilizes it. As mentioned, this is not necessary, but still gives us access closer to the raw Xamarin.Forms API surface and functionality. You’ll also noticed I slimmed down the Navigation calls a bit by using default parameters.

protected ApplicationBase CurrentApplication => Application.Current as ApplicationBase;

private INavigation Navigation => Page?.Navigation ?? Application.Current.MainPage.Navigation;

public Task<Page> PopAsync(bool animated = true) => Navigation.PopAsync(animated);
public Task<Page> PopModalAsync(bool animated = true) => Navigation.PopModalAsync(animated);
public Task PopToRootAsync(bool animated = true) => Navigation.PopToRootAsync(animated);
public void RemovePage(Page page = null) => Navigation.RemovePage(page ?? Page);

public Task PushAsync<TPage, TViewModel>(bool animated = true)
    where TPage : ContentPage
    where TViewModel : ViewModelBase
        => PushAsyncInternal<TPage, TViewModel>(null, animated, false);

public Task PushModalAsync<TPage, TViewModel>(bool animated = true)
    where TPage : ContentPage
    where TViewModel : ViewModelBase
        => PushAsyncInternal<TPage, TViewModel>(null, animated, true);

private Task PushAsyncInternal<TPage, TViewModel>(bool animated = true, bool modal = false)
    where TPage : ContentPage
    where TViewModel : ViewModelBase
    {
        Page page = CurrentApplication.CreatePage<TPage, TViewModel>();

        if (modal)
        {
            await Navigation.PushModalAsync(page, animated);
        }
        else
        {
            await Navigation.PushAsync(page, animated);
        }
    }

While the implementation could be less verbose, I decided to focus on a simpler way to consume the APIs we created in the ViewModelBase from any implementing ViewModel. As such, usage would appear as follows:

public class MyViewModel : ViewModelBase
{
    public async Task SomethingHappened()
    {
        if (DisplayAlert(“Title”, “Message”, “Accept”, “Cancel”))
        {
            await PushAsync<MyOtherPage, MyOtherViewModel>();
        }
    }
}

In order to use the same Navigation pattern when creating the MainPage of the Forms application, we can add the ability to have our CreatePage method wrap a Page in a NavigationPage and use that directly from our Application class. Here, we’ve also added a simple API surface for defining the construction of the MainPage and separated the instantiation of the MainPage from the App instance’s constructor. The principle of doing that is that the App constructor should be as fast and efficient as possible. Instantiating a Page is not always so, particularly when we add dependency injection and parameter passing later.

public abstract class ApplicationBase : Application
{
    protected abstract Task<Page> CreateMainPage();

    public ApplicationBase Init()
    {
        Page mainPage = null;

        // Magic Voodoo to appease the bootstrapping Gods.
        Task.Run(async () 
            => mainPage = await CreateMainPage()).Wait();

        MainPage = mainPage;

        return this;
    }

    public Page CreatePage<TPage, TViewModel>(bool wrapInNavigationPage = false)
        where TPage : ContentPage
        where TViewModel : ViewModelBase
    {
        var vm = default(TPage);
        var page = default(TViewModel);

        vm.SetWeakPage(page);
        page.BindingContext = vm;

        if (wrapInNavigationPage)
        {
            return new NavigationPage(page);
        }

        return page;
    }
}
public partial class App : ApplicationBase
{
    public App() => InitializeComponent();

    protected override Task<Page> CreateMainPage
        => CreatePage(StartPage, StartViewModel>(true);
}

In order to invoke the CreateMainPage Task, we now do so where we create the App instance: In the iOS AppDelegate class and the Android MainActivity class. It’s not the best solution, but I find it a better alternative than constructing the MainPage in App’s constructor.

var myApp = new App().Init();
LoadApplication(myApp);


Honestly, that was more work than needed to bridge those gaps, but we did so with very little code and little-to-no interference with Xamairn.Forms’ functionality.


You can find the entire project on Github:
https://github.com/jacob-maristany/Xamarin.Forms.Mvvm


Some Notes on Xamarin.Forms Shell:
Shell adds the ability to navigate from anywhere using Routes. It does allow parameter passing during navigation, but since they are embedded in the navigation string, they appear to be limited to basic types and are quite kludgy to use. There also does not seem to be support for alerts from the Shell class. Therefore, a lot of this content still holds true under Shell.


Next Time on…

We’ll fill in some additional gaps by responding to view disappearing and appearing the ViewModel and passing complex types as parameters during navigation.