Platform Specific Code using Partial Classes in .NET MAUI

.NET developer with a passion for mobile and DevOps. I build cross platform apps using Xamarin, backend systems using Azure and GitHub Actions using Docker.
Search for a command to run...

.NET developer with a passion for mobile and DevOps. I build cross platform apps using Xamarin, backend systems using Azure and GitHub Actions using Docker.
No comments yet. Be the first to comment.
A collection of articles about developing awesome cross platform apps with Xamarin Forms and .NET MAUI.
.NET MAUI Preview 11 was released a little over a week ago but it was something of a stealth release due to a late bug in Visual Studio 2022 17.1 Preview 2. We can still install Preview 11 using the CLI but there are some caveats. In this article I e...
In my previous article, Secure your .NET builds with StepSecurity and GitHub Actions, I discussed software supply chain attacks and showed how to use StepSecurity Secure Workflows to improve the security of your GitHub Actions software supply chain. ...

Software supply chain attacks are increasing in severity and frequency. StepSecurity are working to help secure DevOps workflows using GitHub Actions. In this article I show how you can use StepSecurity's tools to improve the security of your workflo...

I have a confession to make, I'm working on a new cross platform app and we're using Xamarin.Forms rather than .NET MAUI.😲 Xamarin.Forms will be unsupported after November 2023 so why did we make this choice? Background First a little background......

I'm pleased to announce the latest release of my popular GitHub Action Code Coverage Summary. Code Coverage Summary v1.3.0 brings new features, compatibility with more test frameworks and improvements to supply chain security. Code Coverage Summary r...

I woke up this morning...🎶 No, I'm not writing a blues song and actually this was yesterday but I coudn't resist that opening line.😄 I awoke to find a text message telling me I had a donation via GitHub Sponsors! Who was this and which project had ...

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
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.
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.
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 ProPlatform 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.
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.
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.
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.
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.
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.