Xamarin.Froms: Bindnig and Navigation

comments edit

To recap, I’m writing a shopping cart app for Windows Phone, Android, and iOS. The purpose of the app is primarily to let me use Forms. Each post will build on top of the previous one.

Last time we did little more than get our feet wet. The plan for today is to create a page with data binding At this stage of the app, that sounds like a login page to me.

An aside. It got rather tiring saying “Xamarin.Forms” all the time and I’m not quite comfortable just calling it “Forms” because I keep thinking back to WinForms whenever I do. So I’m just going to go the abbreviation route and call it XF. The abbreviation has the benefit of starting with an “X” and those are just cool.

Recap and Code

This is the second post in the series, you can find the rest here:

  • Day 0: Getting Started (blog / code)
  • Day 1: Binding and Navigation (blog / code)

The latest version of the code can always be accessed on the GitHub project page.

Binding

Before I’m able to do anything fancy, I need to figure out how to do some basic binding. To test this out, I create a basic view model with one property that returns some constant text.

public class MainViewModel
{
    public string Message {get { return "Hello World";}}
}

I update the view to simply bind to this message now instead of hard coding the text in the XAML.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ShoppingCart.Views.MainPage">
    <Label Text="{Binding Message}" HorizontalOptions="Center" />
</ContentPage>

For now, I’ll just set the BindingContext in the page’s constructor.

public partial class MainPage : ContentPage
{
    public MainPage(MainViewModel vm)
    {
        this.BindingContext = vm;
        InitializeComponent();
    }
}

All that’s left is to pass in a new view model when creating the page.

new MainPage(new MainViewModel())

Firing up the emulator, this works out of the gate. Nice. Time to move on to something a little more difficult.

The Main Page

I don’t want the login page to be the first page of the app. I think some sort of splash screen or welcome message would make more sense. For now It will simply just have some text and a button that’ll take you to the login page. I’ll add the standard Command=”{Binding GoToLoginPageCommand}” now and wire it up next.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ShoppingCart.Views.MainPage"
             xmlns:local="clr-namespace:ShoppingCart;assembly=ShoppingCart"
             BindingContext="{x:Static local:App.MainViewModel}">
 
  <StackLayout
    VerticalOptions="Center">
    <Label Text="Welcome to the main page." HorizontalOptions="Center" />
 
    <Button Text ="Log In" Command="{Binding GoToLoginPageCommand}" HorizontalOptions="Center" />
  </StackLayout>
</ContentPage>

Now that I know I need to bind to a command, I add the MainViewModel. The command adds some complexity for two reasons. First it’s a command so I need to either implement ICommand or find an already ready implementation. There’s nothing that I could find in XF that implements ICommand for me and I don’t want to bring in an outside framework like MvvmLight just yet so I go ahead and add add a simple implementation of ICommand. It’s so simple that I even named it SimpleCommand.cs. I’m not really sure if this will stay long term, but it’ll do the job for now.

The second bit of complexity isn’t as easy. It’s the fact that I don’t know how to do navigation in XF. So it’s time for some research. To the LIBRARY!

Figuring Out Navigation

According to the doco, all you need to do is wrap your ContentPage in a Navigation page. This is as simple as

new NavigationPage(new MainPage())

Then in your page you can access the Navigation property. After playing with this property a bit I learned that how it works depends on your platform. On Windows Phone it was non null and worked regardless of whether or not you created a NavigationPage. Android was less forgiving. One nice touch was that as soon as you wrap your droid page you get the ActivityBar back button.

Playing around with the Navigation property on the page on both Windows and Android, I found it to be buggy. I set up a simple pair of pages with one button each. The first page would merely navigate to the second page. The second page would just pop the stack and return to the first. With some experimenting it looked to me that the XF infrastructure worked best if you always used the same exact instance of INavigation.

In my opinion, there’s a problem with navigation living in the page. Navigation is important business logic and business logic should not live in the view layer. I don’t want to fight the infrastructure too much but if at all possible, I’m going to want to see what can be done to move navigation to the view model layer.

With this knowledge and my desire to abstract the navigation property out to the view model, I created my own wrapper navigation service.

public class NavigationService : Xamarin.Forms.INavigation
{
    public INavigation Navi { get; internal set; }
 
    public Task<Page> PopAsync()
    {
        return Navi.PopAsync();
    }
 
    public Task<Page> PopModalAsync()
    {
        return Navi.PopModalAsync();
    }
 
    public Task PopToRootAsync()
    {
        return Navi.PopToRootAsync();
    }
 
    public Task PushAsync(Page page)
    {
        return Navi.PushAsync(page);
    }
 
    public Task PushModalAsync(Page page)
    {
        return Navi.PushModalAsync(page);
    }
}

Very simply, it directs all calls to an internal implementation. Again, the static App class is responsible for coordinating the creation of these classes

NavigationService navi = new NavigationService();
MainViewModel = new MainViewModel(navi);
 
MainPage = new NavigationPage(new MainPage());
 
navi.Navi = MainPage.Navigation;

Back to the MainViewModel

Now that we’ve solved the problems of commands and navigation, it’s pretty simple to finish off our MainViewModel.

public class MainViewModel
{
    private readonly INavigation _navi;
 
    public MainViewModel(INavigation navi)
    {
        _navi = navi;
    }
 
    public ICommand GoToLoginPageCommand
    {
        get { return new SimpleCommand(() => _navi.PushAsync(App.LoginPage)); }
    }
}

I left the creation of the login page in the App. This way all my page creation can live in one place.

The Login Page

The login page and view model model are pretty simple. Fields for username and password, and a button to submit. Upon “successfully” logging in we’ll just return back to the the main page. If there’s a failure then we display a message box.

Like navigation, displaying a message box is tightly coupled to the page. I wound up wrapping it up in my Navigation class in a similar fashion as navigation. It’s not really the best spot for it, but it cut down on the number of classes and interfaces i was creating. Also, it was a fast addition. Given the size of the project I don’t really have a problem with it now, but I foresee it being moved out at some point.

Here’s what the navigation interface and implementation look like now:

public interface INavigationService : INavigation
{
    Task<bool> DisplayAlert(string title, string message, string accept, string cancel = null);
}
 
public class NavigationService : INavigationService
{
    public INavigation Navi { get; internal set; }
    public Page myPage { get; set; }
 
    public Task<Page> PopAsync()
    {
        return Navi.PopAsync();
    }
 
    public Task<Page> PopModalAsync()
    {
        return Navi.PopModalAsync();
    }
 
    public Task PopToRootAsync()
    {
        return Navi.PopToRootAsync();
    }
 
    public Task PushAsync(Page page)
    {
        return Navi.PushAsync(page);
    }
 
    public Task PushModalAsync(Page page)
    {
        return Navi.PushModalAsync(page);
    }
 
    public Task<bool> DisplayAlert(string title, string message, string accept, string cancel = null)
    {
        return myPage.DisplayAlert(title, message, accept, cancel);
    }
}

Wiring up ViewModels

A pattern I have always liked from the MvvmLight toolkit is setting up the binding to the view model directly in the view’s XAML. I had to tweak the default MvvmLight template a bit to get this to work, but in the end it wasn’t pretty simple. In the view, create a static binding to a property exposed by the App class. This could be any class, but I went with the App class since it is already being used as a static resource for all of the views anyway.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ShoppingCart;assembly=ShoppingCart"
             BindingContext="{x:Static local:App.LoginViewModel}"
             x:Class="ShoppingCart.Views.LoginPage">
  <!-- Code removed for brevity-->
 
</ContentPage>

This lets us simplify App.cs just a little bit, now that we don’t have to pass in the view models to the views.

public static class App
{
    public static MainViewModel MainViewModel { get; set; }
    public static LoginViewModel LoginViewModel { get; set; }
 
    static App()
    {
        ILoginService login = new LoginService();
        NavigationService navi = new NavigationService();
 
        MainViewModel = new MainViewModel(navi);
        LoginViewModel  = new LoginViewModel(login, navi);
 
        MainPage = new NavigationPage(new MainPage());
        LoginPage = new NavigationPage(new LoginPage());
 
        navi.Navi = MainPage.Navigation;
        navi.myPage = MainPage;
    }
 
    public static Page LoginPage { get; private set; }
    public static Page MainPage { get; private set; }
}

Summary

This is pretty much it for this week. I’ve added binding, commands, and navigation as well as wired up the view models. Next week I’ll flesh out the products pages and try to actually add the shopping cart. Until then, happy coding.

this post was originally on the MasterDevs Blog

Comments