Dual Screen in Xamarin.Forms

Scott Kuhl
6 min readSep 21, 2020

Let’s look at how to update the default Shell template experience to appropriately handle dual screen support using the Master-Detail Pattern.

Xamarin.Forms up to now has mostly had to deal with one screen as it is a mobile operating system development environment at heart. You can provide a pleasant experience by avoiding the seam and using all that extra screen real estate, but the Two Pane View is a different beast. For example, with the Master-Detail Pattern you have several use case scenarios you need to support:

  1. When the application is running on one screen, and you are viewing the list and you select an item, you should show a new screen with the details.
  2. When the application is running on two screens, and you are viewing the list and you select an item, you should show its detail on the other screen.
  3. When the user changes the application from one screen to two screens, you must gracefully transition to the new view, and vice versa.

Microsoft has a full learn module available for implementing a sample solution to support this design pattern. But its missing Shell support and doesn’t match the experience you get from the Shell template.

Note: You can download a working sample from my ShellDuo GitHub repo.

Shell Template project running the Surface Duo.

Creating a New Project

  1. Create a new project from Visual Studio and select the Mobile App (Xamarin.Forms) project template.
  2. On the New Mobile App dialog pick either Flyout or Tabbed to get a Shell based template. (I picked Tabbed for my example solution).
  3. Add the Xamarin.Forms.DualScreen NuGet package to your projects.
  4. Add Xamarin.Forms.DualScreen.DualScreenService.Init(this); to the MainActivity class just below the Forms.Init() method OnCreate();

Update the Base View Model

The view models are going to need to be aware of what type of screen is being displayed. We can put a lot of this functionality right into the base view model.

Navigation will be needed.

public INavigation Navigation { get; set; }

We can detect if the device is currently spanning two screens.

protected bool DeviceIsSpanned => DualScreenInfo.Current.SpanMode != TwoPaneViewMode.SinglePane;

We can detect if the device is on a large screen to also support tablet and desktop mode.

protected bool DeviceIsBigScreen => (Device.Idiom == TargetIdiom.Tablet) || (Device.Idiom == TargetIdiom.Desktop);

We want to track if the view was originally launch across two screens.

protected bool WasSpanned = false;

We want to track if the user is currently looking at only the detail view.

protected bool IsDetail = false;

We want to bind the Two Pane View control to a view model controlled configuration.

private TwoPaneViewTallModeConfiguration _tallModeConfiguration;public TwoPaneViewTallModeConfiguration TallModeConfiguration
{
get => _tallModeConfiguration;
set => SetProperty(ref _tallModeConfiguration, value);
}
private TwoPaneViewWideModeConfiguration _wideModeConfiguration;public TwoPaneViewWideModeConfiguration WideModeConfiguration
{
get => _wideModeConfiguration;
set => SetProperty(ref _wideModeConfiguration, value);
}
private TwoPaneViewPriority _panePriority;public TwoPaneViewPriority PanePriority
{
get => _panePriority;
set => SetProperty(ref _panePriority, value);
}

When the layout is changed, we want to adapt our screens to the new layout. This means:

  1. If the detail page is being viewed on a single screen and the user spans the application across two screens, we want to pop the detail page off the navigation stack.
  2. If the application is spanning two screens or it is on a tablet we want to configure the layout for dual screen support.
  3. If the application is now on one screen and it was on two screens with an item selected, we want to show the detail screen.
  4. The specific view models must be allowed to set item selected state and a route to the detail page.
protected virtual void UpdateLayouts()
{
UpdateLayouts(false, null);
}
protected async void UpdateLayouts(bool itemSelected, string route)
{
if (IsDetail && DeviceIsSpanned)
{
if (Navigation.NavigationStack.Count > 1)
{
await Navigation.PopToRootAsync();
}
}
else if (DeviceIsSpanned || DeviceIsBigScreen)
{
TallModeConfiguration = TwoPaneViewTallModeConfiguration.TopBottom;
WideModeConfiguration = TwoPaneViewWideModeConfiguration.LeftRight;
WasSpanned = true;
}
else
{
PanePriority = TwoPaneViewPriority.Pane1;
TallModeConfiguration = TwoPaneViewTallModeConfiguration.SinglePane;
WideModeConfiguration = TwoPaneViewWideModeConfiguration.SinglePane;
if (WasSpanned && itemSelected)
{
await Shell.Current.GoToAsync(route);
}
WasSpanned = false;
}
}

We want this update functionality to run when the screen is first appearing or when the user changes the view, either from one screen to two or from portrait to landscape. We do this with custom appearing and disappearing methods and hooking into the changing event.

protected void DualScreen_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
UpdateLayouts();
}
public void OnAppearing(bool isDetail = false)
{
IsBusy = true;
IsDetail = isDetail;
DualScreenInfo.Current.PropertyChanged += DualScreen_PropertyChanged;
UpdateLayouts();
}
public void OnDisappearing()
{
DualScreenInfo.Current.PropertyChanged -= DualScreen_PropertyChanged;
}

Combining View Models

Next we will combine the two view models for items into a single view model. This will allow it to work when the list view is also showing the detail view without duplicating code.

  1. We’ll need to update the OnAppearing method to indicate which type of view we are bound to.
  2. Remove the OnTapped event. We’ll need to change the selection mode on the collection view and we can just let the existing SelectedItem property handle it.
  3. When an item is selected we need to navigate to a new page only when the device is in single screen mode on a small device and protect against any navigation if we are already on the detail view.
  4. Call the base view models UpdateLayouts method with our selection mode and route.
[QueryProperty(nameof(ItemId), nameof(ItemId))]
public class ItemsViewModel : BaseViewModel
{
private Item _selectedItem;
public ObservableCollection<Item> Items { get; }
public Command LoadItemsCommand { get; }
public Command AddItemCommand { get; }
public ItemsViewModel()
{
Title = “Browse”;
Items = new ObservableCollection<Item>();
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
AddItemCommand = new Command(OnAddItem);
}
async Task ExecuteLoadItemsCommand()
{
IsBusy = true;
try
{
Items.Clear();
var items = await DataStore.GetItemsAsync(true);
foreach (var item in items)
{
Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
public new void OnAppearing(bool isDetail = false)
{
base.OnAppearing(isDetail);
if (!IsDetail) SelectedItem = null;
}
public Item SelectedItem
{
get => _selectedItem;
set
{
SetProperty(ref _selectedItem, value);
OnItemSelected(value);
}
}
private async void OnAddItem(object obj)
{
await Shell.Current.GoToAsync(nameof(NewItemPage));
}
async void OnItemSelected(Item item)
{
if (item == null)
return;
if (!DeviceIsSpanned && !DeviceIsBigScreen && string.IsNullOrEmpty(ItemId))
{
await Shell.Current.GoToAsync($”{nameof(ItemDetailPage)}?{nameof(ItemId)}={item.Id}”);
}
}
private string itemId;public string ItemId
{
get
{
return itemId;
}
set
{
itemId = value;
LoadItemId(value);
}
}
public async void LoadItemId(string itemId)
{
IsBusy = true;
try
{
var item = await DataStore.GetItemAsync(itemId);
SelectedItem = item;
}
catch (Exception)
{
Debug.WriteLine(“Failed to Load Item”);
}
finally
{
IsBusy = false;
}
}
protected override void UpdateLayouts()
{
UpdateLayouts(SelectedItem != null, $”{nameof(ItemDetailPage)}?{nameof(ItemsViewModel.ItemId)}={SelectedItem?.Id}”);
}
}

Splitting the Detail Page

The detail page needs to be split into a page and a view, this way it can be embedded in the items page as well.

Detail Page XAML

<?xml version=”1.0" encoding=”utf-8" ?>
<ContentPage
x:Class=”ShellDuo.Views.ItemDetailPage”
xmlns=”http://xamarin.com/schemas/2014/forms"
xmlns:x=”http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views=”clr-namespace:ShellDuo.Views”
Title=”{Binding Title}”>
<views:ItemDetailView />
</ContentPage>

Detail Page Code

public partial class ItemDetailPage : ContentPage
{
ItemsViewModel _viewModel;
public ItemDetailPage()
{
InitializeComponent();
BindingContext = _viewModel = new ItemsViewModel();
_viewModel.Navigation = Navigation;
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing(isDetail: true);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_viewModel.OnDisappearing();
}
}

New Detail View XAML

<?xml version=”1.0" encoding=”UTF-8" ?>
<ContentView
x:Class=”ShellDuo.Views.ItemDetailView”
xmlns=”http://xamarin.com/schemas/2014/forms"
xmlns:x=”http://schemas.microsoft.com/winfx/2009/xaml">
<StackLayout Padding=”15" Spacing=”20">
<Label FontSize=”Medium” Text=”Text:” />
<Label FontSize=”Small” Text=”{Binding SelectedItem.Text}” />
<Label FontSize=”Medium” Text=”Description:” />
<Label FontSize=”Small” Text=”{Binding SelectedItem.Description}” />
</StackLayout>
</ContentView>

New Detail View Code

public partial class ItemDetailView : ContentView
{
public ItemDetailView()
{
InitializeComponent();
}
}

Adding Dual Mode to the Items Page

Finally we can use the Two Pane View Control on the items page.

<?xml version=”1.0" encoding=”utf-8" ?>
<ContentPage
x:Class=”ShellDuo.Views.ItemsPage”
xmlns=”http://xamarin.com/schemas/2014/forms"
xmlns:x=”http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:dualScreen=”clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen”
xmlns:local=”clr-namespace:ShellDuo.ViewModels”
xmlns:model=”clr-namespace:ShellDuo.Models”
xmlns:views=”clr-namespace:ShellDuo.Views”
x:Name=”BrowseItemsPage”
Title=”{Binding Title}”>
<ContentPage.ToolbarItems>
<ToolbarItem Command=”{Binding AddItemCommand}” Text=”Add” /></ContentPage.ToolbarItems>
<dualScreen:TwoPaneView
x:Name=”TwoPaneView”
Pane1Length=”1*”
Pane2Length=”2*”
PanePriority=”{Binding PanePriority}”
TallModeConfiguration=”{Binding TallModeConfiguration}”
WideModeConfiguration=”{Binding WideModeConfiguration}”>
<dualScreen:TwoPaneView.Pane1>
<! —
x:DataType enables compiled bindings for better performance and compile time validation of binding expressions.
https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/data-binding/compiled-bindings

<RefreshView
x:DataType=”local:ItemsViewModel”
Command=”{Binding LoadItemsCommand}”
IsRefreshing=”{Binding IsBusy, Mode=TwoWay}”>
<CollectionView
x:Name=”ItemsListView”
ItemsSource=”{Binding Items}”
SelectedItem=”{Binding SelectedItem}”
SelectionMode=”Single”>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding=”10" x:DataType=”model:Item”>
<Label
FontSize=”16"
LineBreakMode=”NoWrap”
Style=”{DynamicResource ListItemTextStyle}”
Text=”{Binding Text}” />
<Label
FontSize=”13"
LineBreakMode=”NoWrap”
Style=”{DynamicResource ListItemDetailTextStyle}”
Text=”{Binding Description}” />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
</dualScreen:TwoPaneView.Pane1><dualScreen:TwoPaneView.Pane2><! — view is shared with FlagDetailsPage →
<views:ItemDetailView />
</dualScreen:TwoPaneView.Pane2>
</dualScreen:TwoPaneView>
</ContentPage>

And update the code behind.

public partial class ItemsPage : ContentPage
{
ItemsViewModel _viewModel;
public ItemsPage()
{
InitializeComponent();
BindingContext = _viewModel = new ItemsViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_viewModel.OnDisappearing();
}
}

Models

The starting template does not provide an editable interface on the master detail view. If you want to enhance it, don’t forget to make the models implement INotifyPropertyChanged.

Summary

Supporting dual screens adds some new challenges to mobile development.

The pattern shown here:

  1. Adds dual screen handling to the base view model.
  2. Combines two view models into one.
  3. Moves the detail user interface down to a Content View.
  4. Adds a Two Pane View control to the list page.

This example should get you started on implementing one of the more complex patterns using the Shell templates that are shipped as part of Xamarin.Forms.

If you would like to see something like this built right into the template, go upvote and leave feedback on this issue.

--

--