Platform Specific Code using Partial Classes in .NET MAUI

Platform Specific Code using Partial Classes in .NET MAUI

When writing a cross platform app it is common to need some platform specific code. In Xamarin.Forms we use DependencyService, in .NET MAUI we can use a similar dependency injection technique or take advantage of MAUI's multi-targeting and partial classes to write platform specific code. In this article I demonstrate how to use partial classes in .NET MAUI to retrieve device information.

As always, the code for my .NET MAUI articles is available on GitHub: irongut/MauiBeach

The Problem

Xamarin Essentials and MAUI Essentials include a DeviceInfo class which provides information about the device the application is running on but on Android it doesn't include the SDK version and on iOS it reports the model using Apple hardware identifiers, which are not very user friendly. In my Xamarin.Forms applications I use DependencyService and platform specific code to improve on the information provided by Xamarin Essentials, here I'll use partial classes to achieve the same results in .NET MAUI.

The Solution

In C# we normally write a class in a single file but it is possible to split a class over multiple files that are combined when the application is compiled by using partial classes. A partial class is created by using the partial keyword and every part of the partial class must be in the same assembly and namespace. Partial classes are part of the code that enables XAML to work so you will have used them before even if you didn't realise it.

Cross Platform Partial Class

First we need to create a cross platform partial class that defines partial methods which will be implemented by our platform specific partial classes, think of this class as like an interface.

namespace MauiBeach.Services;

internal static partial class DeviceInfoService
{
    internal static partial string Model();

    internal static partial string Platform();
}

Our platform specific code will return two strings:

  • Model such as iPhone 13 Pro
  • Platform which will include the platform name and version, for example Android 9 (API 28 - Pie)

Remember, all the parts of our partial class must be in the same namespace - in this case MauiBeach.Services.

Android Partial Class

The Android implementation of our platform specific code goes in the Platforms\Android folder hierarchy.

using Android.OS;

namespace MauiBeach.Services;

internal static partial class DeviceInfoService
{
    internal static partial string Model() => Build.Model;

    internal static partial string Platform()
    {
        return $"Android {Build.VERSION.Release} (API {AndroidSDK} - {AndroidCodename()})";
    }

    private static string AndroidCodename()
    {
        return (int)Build.VERSION.SdkInt switch
        {
            (int)BuildVersionCodes.Lollipop or (int)BuildVersionCodes.LollipopMr1 => "Lollipop",
            (int)BuildVersionCodes.M => "Marshmallow",
            (int)BuildVersionCodes.N or (int)BuildVersionCodes.NMr1 => "Nougat",
            (int)BuildVersionCodes.O or (int)BuildVersionCodes.OMr1 => "Oreo",
            (int)BuildVersionCodes.P => "Pie",
            (int)BuildVersionCodes.Q => "Q",
            (int)BuildVersionCodes.R => "R",
            (int)BuildVersionCodes.S => "S",
            32 => "Sv2",
            _ => "Unknown",
        };
    }

    private static int AndroidSDK => (int)Build.VERSION.SdkInt;
}

On Android the information we want is available from the Build class. I've only included SDK codenames for versions of Android supported by .NET MAUI.

iOS Partial Class

The iOS implementation of our platform specific code goes in the Platforms\iOS folder hierarchy.

using Foundation;
using Microsoft.Maui.Essentials;
using ObjCRuntime;
using System;
using System.Runtime.InteropServices;
using UIKit;

namespace MauiBeach.Services;

internal static partial class DeviceInfoService
{
    // based on code from https://github.com/dannycabrera/Get-iOS-Model

    private const string HardwareProperty = "hw.machine";

    [DllImport(Constants.SystemLibrary)]
    private static extern int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string property,
                                            IntPtr output,
                                            IntPtr oldLen,
                                            IntPtr newp,
                                            uint newlen);

    internal static partial string Model()
    {
        string version = FindVersion();
        if (version == "i386" || version == "x86_64")
        {
            return GetModel(NSProcessInfo.ProcessInfo.Environment["SIMULATOR_MODEL_IDENTIFIER"].ToString()) + " Simulator";
        }
        return GetModel(version);
    }

    internal static partial string Platform() => $"{DeviceInfo.Platform} {UIDevice.CurrentDevice.SystemVersion}";

    private static string FindVersion()
    {
        try
        {
            // get the length of the string that will be returned
            var pLen = Marshal.AllocHGlobal(sizeof(int));
            _ = sysctlbyname(HardwareProperty, IntPtr.Zero, pLen, IntPtr.Zero, 0);

            var length = Marshal.ReadInt32(pLen);

            // check to see if we got a length
            if (length == 0)
            {
                Marshal.FreeHGlobal(pLen);
                return "Unknown";
            }

            // get the hardware string
            var pStr = Marshal.AllocHGlobal(length);
            _ = sysctlbyname(HardwareProperty, pStr, pLen, IntPtr.Zero, 0);

            // convert the native string into a C# string
            var hardwareStr = Marshal.PtrToStringAnsi(pStr);

            // cleanup
            Marshal.FreeHGlobal(pLen);
            Marshal.FreeHGlobal(pStr);

            return hardwareStr;
        }
        catch (Exception ex)
        {
            Console.WriteLine("DeviceHardware.Version Ex: " + ex.Message);
        }

        return "Unknown";
    }

    private static string GetModel(string version)
    {
        if (version.StartsWith("iPhone"))
        {
            switch (version)
            {
                case "iPhone14,2":
                    return "iPhone 13 Pro";
                case "iPhone14,3":
                    return "iPhone 13 Pro Max";
                case "iPhone14,4":
                    return "iPhone 13 mini";
                case "iPhone14,5":
                    return "iPhone 13";
                case "iPhone13,1":
                    return "iPhone 12 mini";
                case "iPhone13,2":
                    return "iPhone 12";
                case "iPhone13,3":
                    return "iPhone 12 Pro";
                case "iPhone13,4":
                    return "iPhone 12 Pro Max";
                case "iPhone12,8":
                    return "iPhone SE (2nd generation)";
                case "iPhone12,5":
                    return "iPhone 11 Pro Max";
                case "iPhone12,3":
                    return "iPhone 11 Pro";
                case "iPhone12,1":
                    return "iPhone 11";
                case "iPhone11,2":
                    return "iPhone XS";
                case "iPhone11,4":
                case "iPhone11,6":
                    return "iPhone XS Max";
                case "iPhone11,8":
                    return "iPhone XR";
                case "iPhone10,3":
                case "iPhone10,6":
                    return "iPhone X";
                case "iPhone10,2":
                case "iPhone10,5":
                    return "iPhone 8 Plus";
                case "iPhone10,1":
                case "iPhone10,4":
                    return "iPhone 8";
                case "iPhone9,2":
                case "iPhone9,4":
                    return "iPhone 7 Plus";
                case "iPhone9,1":
                case "iPhone9,3":
                    return "iPhone 7";
                case "iPhone8,4":
                    return "iPhone SE";
                case "iPhone8,2":
                    return "iPhone 6S Plus";
                case "iPhone8,1":
                    return "iPhone 6S";
                case "iPhone7,1":
                    return "iPhone 6 Plus";
                case "iPhone7,2":
                    return "iPhone 6";
                case "iPhone6,2":
                    return "iPhone 5S Global";
                case "iPhone6,1":
                    return "iPhone 5S GSM";
                case "iPhone5,4":
                    return "iPhone 5C Global";
                case "iPhone5,3":
                    return "iPhone 5C GSM";
                case "iPhone5,2":
                    return "iPhone 5 Global";
                case "iPhone5,1":
                    return "iPhone 5 GSM";
            }
        }

        if (version.StartsWith("iPod"))
        {
            switch (version)
            {
                case "iPod9,1":
                    return "iPod touch 7G";
                case "iPod7,1":
                    return "iPod touch 6G";
            }
        }

        if (version.StartsWith("iPad"))
        {
            switch (version)
            {
                case "iPad14,2":
                    return "iPad mini (6th generation) Wi-FI + Cellular";
                case "iPad14,1":
                    return "iPad mini (6th generation) Wi-FI";
                case "iPad13,11":
                case "iPad13,10":
                    return "iPad Pro (12.9-inch) (5th generation) Wi-Fi + Cellular";
                case "iPad13,9":
                case "iPad13,8":
                    return "iPad Pro (12.9-inch) (5th generation) Wi-Fi";
                case "iPad13,7":
                case "iPad13,6":
                    return "iPad Pro (11-inch) (3rd generation) Wi-Fi + Cellular";
                case "iPad13,5":
                case "iPad13,4":
                    return "iPad Pro (11-inch) (3rd generation) Wi-Fi";
                case "iPad13,2":
                    return "iPad Air (4th generation) Wi-Fi + Cellular";
                case "iPad13,1":
                    return "iPad Air (4th generation) Wi-Fi";
                case "iPad12,2":
                    return "iPad (9th Generation) Wi-Fi + Cellular";
                case "iPad12,1":
                    return "iPad (9th generation) Wi-Fi";
                case "iPad11,7":
                    return "iPad (8th Generation) Wi-Fi + Cellular";
                case "iPad11,6":
                    return "iPad (8th Generation) Wi-Fi";
                case "iPad11,4":
                    return "iPad Air (3rd generation) Wi-Fi + Cellular";
                case "iPad11,3":
                    return "iPad Air (3rd generation) Wi-Fi";
                case "iPad11,2":
                    return "iPad mini (5th generation) Wi-Fi + Cellular";
                case "iPad11,1":
                    return "iPad mini (5th generation) Wi-Fi";
                case "iPad8,12":
                    return "iPad Pro (12.9-inch) (4th generation) Wi-Fi + Cellular";
                case "iPad8,11":
                    return "iPad Pro (12.9-inch) (4th generation) Wi-Fi";
                case "iPad8,10":
                    return "iPad Pro (11-inch) (2nd generation) Wi-Fi + Cellular";
                case "iPad8,9":
                    return "iPad Pro (11-inch) (2nd generation) Wi-Fi";
                case "iPad8,8":
                    return "iPad Pro 12.9-inch (3rd Generation)";
                case "iPad8,7":
                    return "iPad Pro 12.9-inch (3rd generation) Wi-Fi + Cellular";
                case "iPad8,6":
                case "iPad8,5":
                    return "iPad Pro 12.9-inch (3rd Generation)";
                case "iPad8,4":
                    return "iPad Pro 11-inch";
                case "iPad8,3":
                    return "iPad Pro 11-inch Wi-Fi + Cellular";
                case "iPad8,2":
                    return "iPad Pro 11-inch";
                case "iPad8,1":
                    return "iPad Pro 11-inch Wi-Fi";
                case "iPad7,12":
                    return "iPad (7th generation) Wi-Fi + Cellular";
                case "iPad7,11":
                    return "iPad (7th generation) Wi-Fi";
                case "iPad7,6":
                    return "iPad (6th generation) Wi-Fi + Cellular";
                case "iPad7,5":
                    return "iPad (6th generation) Wi-Fi";
                case "iPad7,4":
                    return "iPad Pro (10.5-inch) Wi-Fi + Cellular";
                case "iPad7,3":
                    return "iPad Pro (10.5-inch) Wi-Fi";
                case "iPad7,2":
                    return "iPad Pro 12.9-inch (2nd generation) Wi-Fi + Cellular";
                case "iPad7,1":
                    return "iPad Pro 12.9-inch (2nd generation) Wi-Fi";
                case "iPad6,12":
                    return "iPad (5th generation) Wi-Fi + Cellular";
                case "iPad6,11":
                    return "iPad (5th generation) Wi-Fi";
                case "iPad6,8":
                    return "iPad Pro 12.9-inch Wi-Fi + Cellular";
                case "iPad6,7":
                    return "iPad Pro 12.9-inch Wi-Fi";
                case "iPad6,4":
                    return "iPad Pro (9.7-inch) Wi-Fi + Cellular";
                case "iPad6,3":
                    return "iPad Pro (9.7-inch) Wi-Fi";
                case "iPad5,4":
                    return "iPad Air 2 Wi-Fi + Cellular";
                case "iPad5,3":
                    return "iPad Air 2 Wi-Fi";
                case "iPad5,2":
                    return "iPad mini 4 Wi-Fi + Cellular";
                case "iPad5,1":
                    return "iPad mini 4 Wi-Fi";
                case "iPad4,9":
                    return "iPad mini 3 Wi-Fi + Cellular (TD-LTE)";
                case "iPad4,8":
                    return "iPad mini 3 Wi-Fi + Cellular";
                case "iPad4,7":
                    return "iPad mini 3 Wi-Fi";
                case "iPad4,6":
                    return "iPad mini 2 Wi-Fi + Cellular (TD-LTE)";
                case "iPad4,5":
                    return "iPad mini 2 Wi-Fi + Cellular";
                case "iPad4,4":
                    return "iPad mini 2 Wi-Fi";
                case "iPad4,3":
                    return "iPad Air Wi-Fi + Cellular (TD-LTE)";
                case "iPad4,2":
                    return "iPad Air Wi-Fi + Cellular";
                case "iPad4,1":
                    return "iPad Air Wi-Fi";
                case "iPad3,6":
                    return "iPad (4th generation) Wi-Fi + Cellular (MM)";
                case "iPad3,5":
                    return "iPad (4th generation) Wi-Fi + Cellular";
                case "iPad3,4":
                    return "iPad (4th generation) Wi-Fi";
            }
        }

        return string.IsNullOrWhiteSpace(version) ? "Unknown" : version;
    }
}

The iOS implemntation is a lot more complicated because Apple does not provide an API for developers to get the model of a device. The hw.machine string provides a hardware identifier like iPhone10,6 which we need to map to the device model. Apple doesn't provide official mappings but non-official mappings can be found at The iPhone Wiki or Danny Cabrera's Get iOS Model project. My mapping is based on Danny's but I only include devices capable of running versions of iOS supported by .NET MAUI.

Windows Partial Class

The Windows implementation of our platform specific code goes in the Platforms\Windows folder hierarchy.

using Windows.Security.ExchangeActiveSyncProvisioning;
using Windows.System.Profile;

namespace MauiBeach.Services;

internal static partial class DeviceInfoService
{
    internal static partial string Model() => new EasClientDeviceInformation().SystemProductName;

    internal static partial string Platform() => $"UWP {GetVersionString()}";

    private static string GetVersionString()
    {
        var version = AnalyticsInfo.VersionInfo.DeviceFamilyVersion;

        if (ulong.TryParse(version, out var v))
        {
            var v1 = (v & 0xFFFF000000000000L) >> 48;
            var v2 = (v & 0x0000FFFF00000000L) >> 32;
            var v3 = (v & 0x00000000FFFF0000L) >> 16;
            var v4 = v & 0x000000000000FFFFL;
            return $"{v1}.{v2}.{v3}.{v4}";
        }

        return version;
    }
}

The Android and iOS implementations are the ones where we want more information and the Windows implementation is very similar to the way Xamarin / MAUI Essentials works.

MacCatalyst Partial Class

The MacCatalyst implementation of our platform specific code goes in the Platforms\MacCatalyst folder hierarchy.

using Microsoft.Maui.Essentials;

namespace MauiBeach.Services;

internal static partial class DeviceInfoService
{
    internal static partial string Model() => DeviceInfo.Model;

    internal static partial string Platform() => $"{DeviceInfo.Platform} {DeviceInfo.VersionString}";
}

I have no experience with MacCatalyst but if a MAUI app targets a platform it must include an implementation of the cross platform partial class for that platform. In order to fulfil that condition we simply return values from MAUI Essentials' DeviceInfo class. If that proves to be insufficient in future it will be easy to expand this class to provide more information.

Final Thoughts

Using partial classes to write platform specific code in .NET MAUI is quick to learn and I think slightly easier than the old DependencyService in Xamarin.Forms. And, most of the code in this article is copied and pasted from my Xamarin code so it is also easy to change from the old way to this new way of writing platform specific code. 🎉

 

 

Cover image includes a vector created by brgfx from www.freepik.com.

Did you find this article valuable?

Support Dave Murray by becoming a sponsor. Any amount is appreciated!