Tuesday 3 July 2018

Xamarin Master-Detail page

Thanks to the Xamarin team this is very simple to accomplish, however I really don't like the way the pages are dumped into the same folder, i don't like the way they are named and i don't like the model and view models are added to the project, so let's get started. First create a Xamarin application and in your shared code wheather that's a shared project or a .net Standard library create folder for Models, Views and ViewModels.


now that you have those laid out add a new "Master Detail Page" to your views folder.



Now this will create four files, in your views folder.


Now this is what i don't like:
  • Firstly the PageMenuItem.cs is a model, not view and I don't like it's naming convention, 
    • I rename it to BurgerMenuItem.cs 
    • Move it into my Models folder
    • Update its namespace to Models
    • Include the missing using statement
  • Next You'd think that PageMaster is the master page, well it's not; it actually represents the Burger menu, 
    • I  rename it to MenuPage.
      • rename the file
      • rename the code-behind
      • rename the xaml markup
    • In its code-behind you'll see that the view-model is included
      • i create a separate file in my view-models folder rename it to MenuPageViewModel and include all of the missing using statements. 
      • I also extract the INotifyPropertyChanged implementation into an abstract base class called BaseViewModel
      • I Inherit from BaseViewModel in my MenuPageViewModel
      • I also make some modifications to the notification code
  • Next let's look at the Page.xaml file, this is in fact the masterpage
    • I Rename it to MasterPage.xaml
    • I change the markup to MasterPage
    • In the markup i change the name of the MenuPage to MenuPage
    • I change the codebehind to MasterPage
    • in the codebehind i change the reference to the MenuPage to the name i gave it.
  • Now the PageDetail is actually the contentPage that loads, so i like to give it a more meaning full name, in this case i'll call it GamePage.
  • Finally i open the App.xaml page and set my apps MainPage to MasterPage
When all is said and done my project looks like so.

So let's look at the BurgerMenuItem.cs class

using System;
using pav.tictactoe.Views;

namespace pav.tictactoe.Models
{
    public class BurgerMenuItem
    {
        public BurgerMenuItem() => TargetType = typeof(GamePage);
       
        public int Id { get; set; }
        public string Title { get; set; }
        public Type TargetType { get; set; }
    }
}

No great leaps of faith here, it's basically the same as before, just changed the constructor a bit for the sake of brevity.

Next let's look at our BaseViewModel.cs

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace pav.tictactoe.ViewModels
{
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Implementation

        bool SetProperty<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(backingField,value))
                return false;
            backingField = value;

            NotifyPropertyChanged(propertyName);
            return true;
        }

        void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        {
            if (!String.IsNullOrEmpty(propertyName))
                  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    }
}

also nothing special here, this code has been around since wpf, just creates a generic setter for bindable fields and returns a Boolean letting you now if the backing field was updated or not and notifies any bound controls via the Observable Pattern.

next let's take a look at the MenuPageViewModel.cs file 

using pav.tictactoe.Models;
using pav.tictactoe.Views;
using System.Collections.ObjectModel;

namespace pav.tictactoe.ViewModels
{
    public class MenuPageViewModel : BaseViewModel 
    {
        public ObservableCollection<BurgerMenuItem> MenuItems { get; set; }

        public MenuPageViewModel() 
        {
            MenuItems = new ObservableCollection<BurgerMenuItem>(new[] {
                new BurgerMenuItem { Id = 0, Title = "Game", TargetType= typeof(GamePage) },
                new BurgerMenuItem { Id = 1, Title = "Settings" },
                new BurgerMenuItem { Id = 2, Title = "Stats" }
            });
        }
    }
}

also no rocket science here, i did include the TargetType for the first Menu item to illustrate how you'd set up the navigation to a page; later we'll add a settings and stats page.

Next we'll take a look at the game page only because we have to update the class name in two places and because we're just going to leave it as a place holder it'll be easiest to show where, you'll have to do the same for the MasterPage and the MenuPage.

firstly in the xaml

<?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="pav.tictactoe.Views.GamePage"
             Title="Tic Tac Toe">
    <StackLayout>
        <Label Text="This is test data" />
    </StackLayout>

</ContentPage>

In the above snippet make sure to update the x:class path to your new page name

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace pav.tictactoe.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class GamePage : ContentPage
    {
        public GamePage() => InitializeComponent();
    }

}

and make sure that it's aligned with the Class name in the codebehind

Now the Menu page, this is

<?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="pav.tictactoe.Views.MenuPage"
             Title="Master">
  <StackLayout>
    <ListView x:Name="MenuItemsListView"
              SeparatorVisibility="None"
              HasUnevenRows="true"
              ItemsSource="{Binding MenuItems}">
      <ListView.Header>
        <Grid BackgroundColor="#03A9F4">
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="10"/>
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="80"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="10"/>
          </Grid.RowDefinitions>
          <Label
              Grid.Column="1"
              Grid.Row="2"
              Text="AppName"
              Style="{DynamicResource SubtitleStyle}"/>
        </Grid>
      </ListView.Header>
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <StackLayout Padding="15,10" HorizontalOptions="FillAndExpand">
              <Label VerticalOptions="FillAndExpand"
                    VerticalTextAlignment="Center"
                    Text="{Binding Title}"
                    FontSize="24"/>
            </StackLayout>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>

</ContentPage>

This page represents the Slider for the Hamburger Menu


and again make sure to update it in the codebehind

using pav.tictactoe.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace pav.tictactoe.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MenuPage : ContentPage
    {
        public ListView ListView;

        public MenuPage()
        {
            InitializeComponent();
            BindingContext = new MenuPageViewModel();
            ListView = MenuItemsListView;
        }
    }

}

And finally let's take a look at the MasterPage

<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="pav.tictactoe.Views.MasterPage"
             xmlns:pages="clr-namespace:pav.tictactoe.Views">
    <MasterDetailPage.Master>
        <pages:MenuPage x:Name="MenuPage" />
    </MasterDetailPage.Master>
    <MasterDetailPage.Detail>
        <NavigationPage>
            <x:Arguments>
                <pages:GamePage />
            </x:Arguments>
        </NavigationPage>
    </MasterDetailPage.Detail>

</MasterDetailPage>

here we have a couple things to update:

  • Updated that the class name to MasterPage
  • Update the MenuPage and give it a more descriptive name 
  • Update the detailpage to GamePage

next take a look into the code behind

using pav.tictactoe.Models;
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace pav.tictactoe.Views 
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MasterPage : MasterDetailPage 
    {
        public MasterPage() 
        {
            InitializeComponent();
            MenuPage.ListView.ItemSelected += ListView_ItemSelected;
        }

        private void ListView_ItemSelected(object sender, SelectedItemChangedEventArgs e) 
        {
            var item = e.SelectedItem as BurgerMenuItem;
            if (item == null)
                return;

            var page = (Page)Activator.CreateInstance(item.TargetType);
            page.Title = item.Title;

            base.Detail = new NavigationPage(page);
            base.IsPresented = false;
           
            MenuPage.ListView.SelectedItem = null;
        }
    }

}

In the codebehind again you have to update the class name but also the MenuItem cast to whatever we renamed our Model to, in this case BurgerMenuItem.

And finally let's open our App.Xaml.cs class

using pav.tictactoe.Views;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace pav.tictactoe 
{
    public partial class App : Application 
    {
              public App () 
              {
                 InitializeComponent();
                 MainPage = new MasterPage();
              }

              protected override void OnStart () { /* Handle when your app starts */ }
              protected override void OnSleep () { /* Handle when your app sleeps */ }
              protected override void OnResume () { /* Handle when your app resumes */ }
       }
}


Here in our constructor we simply set the page we want our application to set as it's MainPage.

this is involved and easy to screw up; however working though this exercise gave me a much better understanding how the burger layout works, at least at the superficial level of Xamarin forms.