.NET MAUI – Locking Orientation into Portrait or Landscape

Android device orientation lock in portrait & landscape.
Android device orientation lock in portrait viewed from portrait & landscape mode.

With .NET MAUI sometimes you need some soft of application functionality that has operating system specific solutions. In cases like this MAUI allows you to write what it considers platform specific code. This allows you to write code for each operating system that solves the app functionality based on the requirements of that operating system. The .NET MAUI page on platform specific code in the link above does show you how to get the device orientation for either iOS or Android but not how to set the orientation. In this tutorial we are going to look at the app specific code that locks the device orientation into either portrait or landscape.

First create a new folder called DeviceServices:

DeviceServices folder to hold all our device specific partial classes & interfaces

For each device specific code we will create a service class and interface for that service class. This is the cross platform API that .NET MAUI will use in your pages. We will then inject that service that defines the cross platform API into the ViewModels that need the device specific code implementation. In our case we are creating a service that controls device orientation:

Your service class that defines the device specific code has to be a partial class. You define the methods that the class will later define the app specific implementation for as a partial method. This is why for “DeviceOrientationService” we don’t actually define the details of the method. This will be done in the OS version defined in the “Platform” folder:

Define application specific code in the “Platform” folder

Platform folder that holds the application specific code for various OS’s

Let’s first look at the Android solution. Our file name is AndroidDeviceOrientationService. You can name the file whatever you want but the class name inside the file has to match the class name from the class we defined in the “DeviceServices” folder.

using System;
using Android.Content.PM;

namespace ScoreKeepersBoard.DeviceServices;

public partial class DeviceOrientationService
{

	private static readonly IReadOnlyDictionary<DisplayOrientation, ScreenOrientation> _androidDisplayOrientationMap =
		new Dictionary<DisplayOrientation, ScreenOrientation>
		{
			[DisplayOrientation.Landscape] = ScreenOrientation.Landscape,
			[DisplayOrientation.Portrait] = ScreenOrientation.Portrait,
		};

	public partial void SetDeviceOrientation(DisplayOrientation displayOrientation)
	{
		var currentActivity = ActivityStateManager.Default.GetCurrentActivity();
		if(currentActivity is not null)
		{
			if(_androidDisplayOrientationMap.TryGetValue(displayOrientation, out ScreenOrientation screenOrientation))
			{
				currentActivity.RequestedOrientation = screenOrientation;
			}
		}
	}
}

The code above is the actual Android implementation. Notice that this is the place that we define the actual implementation of the “SetDeviceOrientation” method which was only a partial method when declared in the “DeviceOrientationService” class defined in the “DeviceServices” folder.

The above code is a working solution for Android devices.

Now we will define the iOS version of the platform specific code:

We add a class that we are calling “IosDeviceOrientationService”. Just like the Android version You can name the file whatever you want but the class name inside the file has to match the class name from the class we defined in the “DeviceServices” folder.

using System;
using Foundation;
using UIKit;

namespace ScoreKeepersBoard.DeviceServices;

public partial class DeviceOrientationService
{

    private static readonly IReadOnlyDictionary<DisplayOrientation, UIInterfaceOrientation> _iosDisplayOrientationMap =
        new Dictionary<DisplayOrientation, UIInterfaceOrientation>
        {
            [DisplayOrientation.Landscape] = UIInterfaceOrientation.LandscapeLeft,
            [DisplayOrientation.Portrait] = UIInterfaceOrientation.Portrait,
        };

    public partial void SetDeviceOrientation(DisplayOrientation displayOrientation)
    {
        if (UIDevice.CurrentDevice.CheckSystemVersion(16, 0))
        {

            var scene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()[0] as UIWindowScene);
            if (scene != null)
            {
                var uiAppplication = UIApplication.SharedApplication;
                var test = UIApplication.SharedApplication.KeyWindow?.RootViewController;
                if (test != null)
                {
                    test.SetNeedsUpdateOfSupportedInterfaceOrientations();
                    scene.RequestGeometryUpdate(
                        new UIWindowSceneGeometryPreferencesIOS(UIInterfaceOrientationMask.Portrait), error => { });
                }
            }
        }
        else
        {
            UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)UIInterfaceOrientation.Portrait), new NSString("orientation"));
        }
        

    }
}

This is the iOS device specific solution. Notice that this is the place that we define the actual implementation of the “SetDeviceOrientation” method which was only a partial method when declared in the “DeviceOrientationService” class defined in the “DeviceServices” folder.

Above iOS specific code is buggy and breaks for iOS 16.2

Warning, the above iOS specific code version did work for Xcode versions before 14.2. After updating to version 14.2 all of my virtual emulators were running the latest version of iOS which is 16.2 and the above code DOES NOT work for iOS version 16.2.

I have a StackOverflow post about this and I am waiting for a proper solution. If you are writing iOS code that is specific to versions before 16.2 you can use the above solution to lock the device orientation into either portrait or landscape. Once I find a solution (preferably one that works on all iOS versions) for 16.2 I will post that here. (NOTE: If you have a 16.2 solution for this please respond to my StackOverflow post and help out our community!)

.NET MAUI will bind your device specific code to the cross platform API

.NET MAUI will take your cross platform API and bind your platform specific code you defined in the “Platforms” folder to the API classes/methods based upon what sort of OS executable is being compiled. This way for our Android executable we only have the Android specific code bound to our cross platform API, etc.

Register your DeviceOrientationService class & Interface as an injectable resource and use dependency injection to inject that into your ViewModels

Now that you have your application specific code created we need to do something with it. The best way to get access to this is register the DeviceOrientationService class and it’s interface as a singleton resource and inject this into our ViewModels that are bound to the ContentViews that we want to be able to lock into a particular orientation layout. To do so we register these classes in MauiProgram (see bolded code).

using CommunityToolkit.Maui;
using CommunityToolkit.Maui.Core;
using Microsoft.Extensions.Logging;
using ScoreKeepersBoard.Database;
using ScoreKeepersBoard.DeviceServices;
using ScoreKeepersBoard.ViewModels;
using ScoreKeepersBoard.Views;
using ScoreKeepersBoard.ContentViews;

namespace ScoreKeepersBoard;

public static class MauiProgram
{
	public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();

		builder
			.UseMauiApp<App>()
			.ConfigureFonts(fonts =>
			{
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
			});

		builder.UseMauiCommunityToolkitCore();
		builder.UseMauiCommunityToolkit();

		//register device display orientation
		builder.Services.AddSingleton<IDeviceOrientationService, DeviceOrientationService>();



        //Add Data Access Objects
        builder.Services.AddSingleton<IGameDataAccess, GameDataAccess>();
		builder.Services.AddSingleton<IGameDataAccessSync, GameDataAccessSync>();
		builder.Services.AddSingleton<IGameTypeDataAccess, GameTypeDataAccess>();
		builder.Services.AddSingleton<IGameTypeDataAccessSync, GameTypeDataAccessSync>();
		builder.Services.AddSingleton<ITeamDataAccess, TeamDataAccess>();
		builder.Services.AddSingleton<ITeamDataAccessSync, TeamDataAccessSync>();

		//Add Views Dependency Injection
		builder.Services.AddSingleton<HomePage>();
		builder.Services.AddSingleton<GamesPage>();
		builder.Services.AddSingleton<NewGamePage>();
		builder.Services.AddSingleton<GameTypePage>();
		builder.Services.AddSingleton<GameTypesPage>();
		builder.Services.AddSingleton<TeamsPage>();
		builder.Services.AddSingleton<TeamPage>();
        builder.Services.AddSingleton<GamesByTeamsPage>();


        //add ViewModel Dependency Injection
        builder.Services.AddSingleton<HomeViewModel>();
		builder.Services.AddSingleton<GamesViewModel>();
		builder.Services.AddSingleton<NewGameViewModel>();
		builder.Services.AddSingleton<GameTypeViewModel>();
		builder.Services.AddSingleton<GameTypesViewModel>();
		builder.Services.AddSingleton<TeamsViewModel>();
		builder.Services.AddSingleton<TeamViewModel>();
		builder.Services.AddSingleton<GamesByTeamsViewModel>();

#if DEBUG

		builder.Services.AddSingleton<DatabaseTesting>();

		builder.Logging.AddDebug();

#endif

		return builder.Build();
	}
}

We can then inject these into our ViewModels where we want to control device orientation:

using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ScoreKeepersBoard.Database;
using ScoreKeepersBoard.DTO;
using ScoreKeepersBoard.DeviceServices;

namespace ScoreKeepersBoard.ViewModels;

public partial class GamesViewModel : ObservableObject
{
	IGameDataAccessSync gameDataAccessSync;
	IGameTypeDataAccessSync gameTypeDataAccessSync;
	ITeamDataAccessSync teamDataAccessSync;
    IDeviceOrientationService deviceOrientationService;

    public GamesViewModel(IGameDataAccessSync iGameDataAccessSync, IGameTypeDataAccessSync iGameTypeDataAccessSync, ITeamDataAccessSync iTeamDataAccessSync, IDeviceOrientationService iDeviceOrientationService)
    {
        gameDataAccessSync = iGameDataAccessSync;
        gameTypeDataAccessSync = iGameTypeDataAccessSync;
        teamDataAccessSync = iTeamDataAccessSync;
        deviceOrientationService = iDeviceOrientationService;

        deviceOrientationService.SetDeviceOrientation(DisplayOrientation.Portrait);

        InitializeControls();
    }
    .....
}

Below you will see 2 images that depict the solution on Android. The orientation is locked into portrait mode and even when the phone is placed in landscape mode it continues to stay in portrait!

Android OS locked into portrait orientation seen from portrait mode
Android OS locked into portrait orientation seen from landscape mode

For details on dependency injection for .NET MAUI please see our CodeShadowHand tutorial on dependency injection!

Comments

Leave a Reply

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