.NET MAUI Different layouts for Portrait vs Landscape

An example of using a different layout for portrait vs landscape orientation

.NET Maui allows you to create views for your applications pages but you might run into the problem where you have an app that has a page(s) that are really crowded with Controls. The page looks find on portrait orientation but when you switch to Landscape your Controls run over the portion of the page that is visible. A trick you can use to get all your Controls to fit on the landscape orientation is to rearrange layout of the Controls in a different way than the layout of portrait. This tutorial will show you how to create different layouts for portrait vs landscape using ContentViews. When I initially tried to solve this problem I wasn’t able to find any tutorials online about how to load different views by orientation and after getting a tip by FreakyAli on StackOverflow I came up with the solution so I wanted to wrap it all up in one place so I can help my fellow programmers!

For the page that you want a different layout for landscape vs orientation we will be creating 3 different ContentViews, one that defines the portrait orientation layout, one that defines the landscape orientation layout and one ContentView that controls what is shown, either the portrait or the landscape orientation ContentViews. I have coined the name “orientation view loader” for the ContentView that controls what ContentView (whether portrait or landscape) is shown. The page we are going to demonstrate this technique with is from an app that lets you track game scores. The page itself is the “new game” page that allows you to create a new sports game that you will be tracking the score of. This page has a lot of Controls and they don’t all fit in portrait vs landscape unless you have different layouts for each.

Below is the ContentPage’s XAML:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:ScoreKeepersBoard.ContentViews"
             x:Class="ScoreKeepersBoard.Views.NewGamePage"
             Title="NewGamePage">

    <controls:NewGamePageOrientationViewLoader/>

</ContentPage>

Notice that the XAML for this ContentPage doesn’t have any layout Control like Grid or HorizontalStackLayout. The only thing that this XAML defines is the orientation view loader. The class name of the orientation view loader is “NewGamePageOrientationViewLoader” and you use this class name as the XAML name that you add to the XAML of your ContentPage. Notice that we have to define a xmlns namespace called “controls” that points the ContentView’s XAML to the correct folder in your project.

Now that we have the orientation view loader defined within the ContentPage’s XAML when the page is loaded it will load the ContentView for you automatically. The ViewModel that you bind to the ContentPage’s code behind will automatically be inherited by the ContentView. Let’s take a look at the orientation view loaders code now!

using ScoreKeepersBoard.ViewModels;

namespace ScoreKeepersBoard.ContentViews;

public partial class NewGamePageOrientationViewLoader : ContentView
{

    public ContentView portraitContentView;
    public ContentView landscapeContentView;

    public NewGamePageOrientationViewLoader()
	{
        portraitContentView = new NewGamePagePortrait();
        landscapeContentView = new NewGamePageLandscape();
          
        DeviceDisplay.Current.MainDisplayInfoChanged += Current_MainDisplayInfoChanged;
        this.Content = DeviceDisplay.Current.MainDisplayInfo.Orientation == DisplayOrientation.Portrait ? portraitContentView : landscapeContentView;
    }
    private void Current_MainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e)
    {           
        if (e.DisplayInfo.Orientation == DisplayOrientation.Landscape)
        {
            this.Content = landscapeContentView;
        }
        else if (e.DisplayInfo.Orientation == DisplayOrientation.Portrait)
        {
            this.Content = portraitContentView;
        }
        else
        {
            this.Content = portraitContentView;
        }
    }
    protected override void OnBindingContextChanged()
    {
        NewGameViewModel viewModel = ((NewGameViewModel)BindingContext);
        
        landscapeContentView.BindingContext = viewModel;
        portraitContentView.BindingContext = viewModel;
    }
}

The code above is the code behind for the “NewGamePageOrientationViewLoader”. First we have to define 2 ContentView fields, “portraitContentView” and “landscapeContentView”. In the constructor for the class we then instantiate these fields. Notice that each one has its own class. This makes sense because these are the code behind classes for each of our portrait and landscape ContentView classes.

We then create a new event handler for the DeviceDisplay for MAUI that handles the event that is triggered when the display information is changed. When the user switches holding their phone in portrait to landscape (and vice versa) this will trigger that event. Our event handler that we assign to this event will load either the portrait or the landscape ContentView.

DeviceDisplay.Current.MainDisplayInfoChanged += Current_MainDisplayInfoChanged;

The above custom event handling for the orientation change works for future orientation changes, but when we are at this stage in the app lifecycle, the DeviceDisplay has already loaded, so we need to create code that checks the current state and decides if we should have the portrait or the landscape ContentView loaded. The next bit of code does exactly that!

this.Content = DeviceDisplay.Current.MainDisplayInfo.Orientation == DisplayOrientation.Portrait ? portraitContentView : landscapeContentView;

This is just a simple ternary operator that sets the current ContentView’s Content (the current ContentView being the orientation view loader) to either our portrait ContentView or our landscape ContentView.

Lets take a look at our event handler that we set for:
DeviceDisplay.Current.MainDisplayInfoChanged

    private void Current_MainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e)
    {           
        if (e.DisplayInfo.Orientation == DisplayOrientation.Landscape)
        {
            this.Content = landscapeContentView;
        }
        else if (e.DisplayInfo.Orientation == DisplayOrientation.Portrait)
        {
            this.Content = portraitContentView;
        }
        else
        {
            this.Content = portraitContentView;
        }
    }

This event handler checks our current orientation from your event arguments and if the orientation is equal to the enum value DisplayOrientation.Landscape then it sets the orientation view loader’s Content to the ContentView NewGamePageLandscape by setting this.Content. “this.Content” is what view will be displayed for the orientation view loader and this can be overridden from the XAML defined for the orientation view loader. If the orientation from our event args is equal to the enum value DisplayOrientation.Portrait the it will load the NewGamePagePortrait ContentView. That is it for our event handler.

The ViewModel for our orientation view loader is not loaded at the time it is initialized!

As I mentioned above the ContentPage’s ViewModel is automatically inherited by the orientation view loader ContentView (in our case: NewGamePageOrientationViewLoader). The portrait or landscape ContentViews that we will load DO NOT inherit this as well. We have to set them to have the ViewModel programmatically. When the orientation view loader is first created though the ViewModel inherited by the ContentPage is not yet available! This means when you load either the portrait or the landscape ContentView from the constructor you can’t set those ContentViews to have the same ViewModel as the orientation view loader because for the orientation view loader the ViewModel will be null at the time. The way to get around this is to take advantage of the event for ContentView that is fired when the ViewModel is bound:

    protected override void OnBindingContextChanged()
    {
        NewGameViewModel viewModel = ((NewGameViewModel)BindingContext);
        
        landscapeContentView.BindingContext = viewModel;
        portraitContentView.BindingContext = viewModel;
    }

The above code which is an event handler override found on the orientation view loader gets called when the ViewModel for the orientation view loader is bound. This always happens after the constructor is called. In our case this allows us to set the ViewModel for both our landscape ContentView and our portrait ContentView. After this is called then these ContentViews, the orientation view loader, and our ContentPage will all be sharing the same ViewModel.

That above tutorial shows you what you need to do in order to load a different view for landscape vs portrait. I’m going to add a bit of code below to fill out the example in case some of you are wondering about extra setup details.

The below XAML is the front end XAML of the orientation view loader “NewGamePageOrientationViewLoader”:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ScoreKeepersBoard.ContentViews.HomePageOrientationViewLoader">
    <VerticalStackLayout>

    </VerticalStackLayout>
</ContentView>

This XAML file is empty because right away in our code behind for the orientation view loader we are replacing this by calling:

 this.Content = DeviceDisplay.Current.MainDisplayInfo.Orientation == DisplayOrientation.Portrait ? portraitContentView : landscapeContentView;

The below code is the code behind for our ContentPage “NewGamePage.xaml.cs”. The responsibility of this class is to get the ViewModel injected into the constructor so it can be bound to the code behind class. The class also has an override event handler that executes a method on the view model that loads the ViewModels data so it will be ready for our ContentViews.

using ScoreKeepersBoard.ViewModels;

namespace ScoreKeepersBoard.Views;

public partial class NewGamePage : ContentPage
{
	public NewGamePage(NewGameViewModel newGameViewModel)
	{
		InitializeComponent();
		BindingContext = newGameViewModel;
    }


    protected override void OnNavigatedTo(NavigatedToEventArgs args)
    {
		((NewGameViewModel)BindingContext).LoadData();
                base.OnNavigatedTo(args);
		
    }
}

The results of this are that you can have a complicated page that has lots of Controls and we can manage to fit all of those controls on pages using different layouts depending on whether we are in portrait vs landscape mode!

Landscape orientation

Portrait orientation

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *