Fundamental Changes from Xamarin.Forms to .NET MAUI

In this article, you will learn about a few of the core changes from Xamarin.Forms to .NET MAUI. MAUI is the evolution of the Xamarin.Forms framework and its transformation has been nothing short of astonishing. I have noticed improved app performance, a better development experience programmatically and with the IDE, and the accessibility enhancements for impaired users help us reach a wider range of users. I find these changes very exciting and I’m eager to build a production application in MAUI once it’s no longer in preview.

.NET MAUI is currently in preview and is only available in Visual Studio for Windows 2022 prelease at this time.

Single Project

With so many new changes in .NET MAUI one of my favorite innovations is how the language was consolidated from several projects, shared and native, to a single project. The Xamarin.Forms framework ships with multiple projects within a solution depending on the number of platforms you choose to support. A new Xamarin project will include a project for your shared code and your native projects that support Android, iOS, and UWP. In MAUI there is only a single project. You can still access the native bits like info.plist or the AndroidManifest from the Platforms folder, and write platform-specific functions but it is all done under one roof.

Application resources such as images, fonts, app icons, splash screen and raw assets have been consolidated to this single project. We no longer have to spend the time and effort of creating different image resolutions for each native project which also saves on application size. Splash screens and app icons are a breeze now and adding font files and other assets is easier than ever.

Images

Now that MAUI supports SVG’s we can use them for our images, splash screens, and app icons. SVG’s do not rely on pixel data to create images, instead they rely on ‘vector’ data. SVG stands for scalable vector graphic which means a single SVG image will scale the same at any size. In an SVG, paths are drawn using MoveTo, LineTo, and Close to paint scalable graphics using coordinates. They work very similar to PathF or SKPath under the hood if you’ve worked with these utilities before.

We are not restricted to only using just SVG’s in MAUI, we can still used pixelated images however I couldn’t imagine any reason to go back. To add an image to your application, you just need to add the file to the Resources > Image folder and set its build action to ‘MauiImage’.

Out of the box, the default template for a new MAUI application does not allow for child folders to be used within the Resources/Images folder, so you won’t be able to create much of a folder structure within it. If you want to put your images in child folders, then you can do so with a quick adjustment. Right-click your MAUI project and click ‘Edit Project File’ to open the .csproj file. Locate the MauiImage node and a \** directly after Images string, like so.

//FROM 
<MauiImage Include="Resources\Images\*" />
//TO
<MauiImage Include="Resources\Images\**\*" />

This change will allow you to add images to child folders within the Resources\Images folder.

Android: Your icon filename must be lower case, contain alphanumeric characters and underscores only, and must start and end with a letter in order to be in compliance.

Splash Screens

For splash screens, we no longer need to create splash screen activities for Android or work with the story board in iOS. All you need to to is add your splash screen to the Resources folder with build action set to MauiSplashScreen. This will generate a MauiSplashScreen node in the .csproj file for you with an include set to the path of your splash screen image. It is recommended to use SVG’s for your splash screen as they scan scale better then a pixel image would across different device resolutions.

<MauiSplashScreen Color="#3cd070"
                  Include="Resources\app-trap.svg" />
  • Include: The SVG image for your splash screen
  • Color: Overrides the color of your icon SVG

Once again, SVGs are great for splash screens because they are so scalable. For more advanced splash screens that require animation you will need to go with a different approach natively.

Customize splash screens in MAUI or Xamarin

For iOS you can create a .xib file in Xcode and add it to your MAUI or Xamarin.iOS project. Ensure that the build action is set to none and assign the UILaunchStoryboardName to the name of your file in the info.plist. For more information on how to create and design a .xib file you can checkout .xib files in Xamarin.Mac.

As for Android, you will need to create a drawable resource, assign it to a theme, create a splash screen activity, and set the splash activity as your MainLauncher. It’s a bit more involved than that, however, the process for doing this has not changed in MAUI so it’s not in scope for this article. If you’re interested in learning about how you can create a more customized Android splash screen then check out this splash screen documentation.

App Icons

Assigning app icons to the native projects used to be quite a pain because we had to create dozens of resolutions for the same image. One of the coolest features in .NET MAUI is that you only need a single image for your app icon to work on all platforms everywhere an icon is required. To assign an app icon, simply drag an SVG icon image to the Resources/Images folder and set its build action to MauiIcon. Doing this will create a MauiIcon node within your .csproj file and you’re all set.

There are some optional attributes in a MauiIcon that you can leverage for further customization. You can assign a foreground image to overlay the image, scale the size of it, or assign color to the SVG.

  • Include: The background SVG image of your icon
  • Color: Overrides the color of your icon SVG
  • ForegroundFile: The foreground SVG image of you icon
  • ForegroundScale: A double value from 0 to 1 to scale the foreground image
<MauiIcon Color="#000080"
          ForegroundScale="0.75"
          Include="Resources\Images\app-trap-icon-background.svg"
          ForegroundFile="Resources\Images\app-trap-icon-foreground.svg"   />

MauiProgram and the Host Builder pattern

The MauiProgram is the entry-point for your application, and it ships with a single method, CreateMauiApp. This method uses the Host Builder design pattern to set up the scaffolding your entire application. This is a popular pattern that is widely adopted by the .NET community and is commonly used to build Blazor apps, APIs, and other .NET Core applications.

You can find overrides that reference this method in the Android MainApplication, iOS AppDelegete, and the WinUI App.xaml.cs. When the native startup files launch they will call the CreateMauiApp for which a MauiApp is returned.

protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

The MauiAppBuilder is used to add fonts to your application, assign services using dependency injection, and configure native application lifecycle events.

  • ConfigureFonts: Add .otf or .ttf fonts to the application.
  • ConfigureLifecycleEvents: Used to configure life cycle events for native applications.
  • Services: Registers a collection of user provided or framework provided services as Singletons.
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts => {
                fonts.AddFont("fa-solid-900.ttf", "FontAwesome");
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-SemiBold.ttf", "OpenSansSemiBold");
            });
        builder.ConfigureLifecycleEvents(lifecycle => {
#if WINDOWS
        lifecycle
            .AddWindows(windows =>
                windows.OnNativeMessage((app, args) => {
                    if (WindowExtensions.Hwnd == IntPtr.Zero)
                    {
                        WindowExtensions.Hwnd = args.Hwnd;
                        WindowExtensions.SetIcon("Platforms/Windows/trayicon.ico");
                    }
                    app.ExtendsContentIntoTitleBar = false;
                }));
#endif
        });

        var services = builder.Services;
#if WINDOWS
            services.AddSingleton<ITrayService, WinUI.TrayService>();
            services.AddSingleton<INotificationService, WinUI.NotificationService>();
#elif MACCATALYST
            services.AddSingleton<ITrayService, MacCatalyst.TrayService>();
            services.AddSingleton<INotificationService, MacCatalyst.NotificationService>();
#endif
        return builder.Build();
    }
}

Fonts

A MAUI application supports True Type Format (.ttf) and Open Type Format (.otf) font files. Adding font files to your app is now so simple it almost feels to easy. Just drag your font file to the Resources/Fonts folder and ensure that its build action is set to MauiFont. Head over to the MauiProgram file and add an entry for your font file to the MauiAppBuilder.ConfigureFonts action.

.ConfigureFonts(fonts =>
{
    ...
    fonts.AddFont("Debrosee-ALPnL.ttf", "Debrosee");
});

With the font configured you can access it anywhere in your application using FontFamily=”Debrosee”, as an example. If you have any trouble accessing a font then it check out the .csproj file and ensure that your MauiFont is using the correct path.

<MauiFont Include="Resources\Fonts\*" />

Assets

Quickly add and access application assets such as videos, HTML, JSON files, etc in .NET MAUI by assigning a folder for your assets to live. The current .NET MAUI App template preview does not create an assets folder so you will have to create this assignment yourself. All you need to do is add an Assets folder under the Resources folder, then open your .csproj file to specify the location of your assets.

<MauiAsset Include="Resources\Assets\*" />

With your assets folder in place, you can drag and drop files to your new folder, assign a build action of MauiAsset to the file, and you are ready to go! Now the asset can be quickly consumed anywhere in your application.

<WebView Source="home.html" />
...
<MediaElement Source="net-frontend-day.mp4" />

Platform Specific Code

The way that we invoke platform specific code has changed extensively. There are now several ways to write platform-specific code, and choosing which way is best depends on the reason you are invoking it. If you need to make a change for only one of your supported platforms but not all, then I would recommend using conditional compilation. Another good reason to use conditional compilation is when you create custom controls, but we will dive into that in just a moment. When you need to perform some action for all of your platforms then I would recommend using partial classes to do so.

The MAUI project contains a Platforms folder with child folders for each supported platform. These child folders have all the required platform specific files that are unique to them. When the solution is compiled, only the child folder for that platform is included in the binary, as we wouldn’t want iOS code in our Android project. This means that you can also add files to a platform folder and they will only compile with that platform. I would not recommend doing this because there are more sagacious ways to place these files, but it’s good to understand how code is separated during compilation.

.NET MAUI Platform folders

Conditional Compilation

Conditional compilation allows you to only execute code for a specific platform. It accomplishes this by using conditional operators such as #if, #elif (else if), and #end against platforms ANDROID, IOS, WINDOWS, to create code blocks that are only executed for a specific OS.

#if WINDOWS
    AppActions.IconDirectory = Application.Current.On<WindowsConfiguration>().GetImageDirectory();
#endif

Platform-Specific Services

There is a far superior way of enacting code based on the platform than a conditional compilation. Services can be created with a combination of multi-targeting naming convention, partial classes, and partial methods to invoke platform-specific code from shared code. With Xamarin we used to do this using DependancyService, but now that we are working with a single-project solution that approach was scrapped for a better one.

To create a service, you first need to create a partial class that defines the method signatures for the functions you would like to perform. For example, you could create a BluetoothService that gets a list of all the Bluetooth connections connected to a device. When you first create this partial class you will see the error “Partial method must have an implementation part because it has accessibility modifiers”. This error was added in C# 9 and you will continue to see it until you create an implementation of this new partial class

public partial class BluetoothService
{
    public partial List<string> GetConnectedDevices();
}

The next thing you need to do is create partial classes that implement your shared partial class in each platform directory. Implement the partial class method signatures with MAUI.Native code to achieve your desired result.

public partial class BluetoothService
{
    public partial List<string> GetConnectedDevices()
    {
        ...
        return adapter.BondedDevices?.Select(d => d.Name).ToList();
    }
}

You can add each platform-specific file to its parent folder within the Platforms folder and which will allow the shared code partial class to map to the native partial class.

To use your platform-specific service, just new up an instance of the object and call its functions. For platform specific services like this one you should register the service as a Singleton in your MauiProgram although it is not required.

Configuring multi-targeting

Instead of adding platform-specific code to the Platforms folder it’s better to configure multi-targeting. A multi-targeting configuration allows OS specific files, such as our Android BluetoothService, to only be compiled to the native binary based on a custom folder or file naming convention.

One way to configure a multi-targeting naming convention is by file name. I prefer this over configuring by folder name because it I feel that it creates less clutter. A popular naming convention is “**\**\*.Platform.cs”. This convention will take any file with a name like BluetoothService.Android.cs for example and the build system the file to the native binary if it meets the naming convention requirements.

.NET MAUI Multi-targeting by file

To configure a file name-based naming convention simply pop open the .csproj file and add the following conditions. Feel free to change the naming convention to whatever you like, but I  believe that this convention is best.

<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-android')) != true">
  <Compile Remove="**\**\*.Android.cs" />
  <None Include="**\**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-ios')) != true AND $(TargetFramework.StartsWith('net6.0-maccatalyst')) != true">
  <Compile Remove="**\**\*.iOS.cs" />
  <None Include="**\**\*.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
  <Compile Remove="**\*.Windows.cs" />
  <None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

Another popular way to configure multi-targeting of files is based on the folder name. The folder name convention is versatile because you can place the platform folders in any directory and if the folder follows your convention, then the file will only be compiled for its native solution.

.NET MAUI Multi-targeting based on folder names

To enable a multi-targeting naming convention based on folder name simply open your .csproj file and add the following code.

<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-android')) != true">
  <Compile Remove="**\Android\**\*.cs" />
  <None Include="**\Android\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-ios')) != true AND $(TargetFramework.StartsWith('net6.0-maccatalyst')) != true">
  <Compile Remove="**\iOS\**\*.cs" />
  <None Include="**\iOS\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
  <Compile Remove="**\Windows\**\*.cs" />
  <None Include="**\Windows\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

You can also combine file-name and folder-name naming conventions, however, I wouldn’t recommend doing that because then you no longer have a convention.

Custom Controls

The UI controls in .NET MAUI are great but oftenwe have requirements to customize them. We can customize a control globally or we can customize a custom control for a more targeted approach. Control customizations are done by overriding the controls handler with your own implementation.

A handler is basically the logic that takes the native control and converts it into the .NET MAUI control. For example, you can think of a handler as something that maps an iOS UITextField to Maui.Controls.Entry. The handler will map the native UITextField attributes of a control to the attributes of a cross-platform Entry control using Mappers. You can customize a handler’s mapping from anywhere in a MAUI app, nevertheless, it makes the most sense to make these sorts of customizations from the App class or from a custom bootstrapping class.

To customize a mapping directly you will need to create a conditional compilation block for each platform that you would like to customize for. We are customizing the Entry in this example so a EntryHandler is used. The AppendToMapping method specifies a new method to be run after an existing property is mapped. This method takes two parameters. The first parameter is the name of the property, and the second parameter is an action that fires after the control mapping occurs. The method takes in a handler parameter which you can use to mutate the native view.

#if IOS
    Microsoft.Maui.Handlers.EntryHandler.EntryMapper.AppendToMapping("NoUnderline", (handler, view) =>
    {
        handler.NativeView.BackgroundTintList = Colors.Transparent.ToNative();
    });
#endif

The example above will remove the underline from all entries in the application, but what if we don’t want this to be a global change? It’s possible to create targeted change by building a custom control. For example, you could develop a custom control by simply creating a class that inherits from the control you wish to modify.

public class NoUnderlineEntry : Entry {}

You can now use this view anywhere that you would like your customization to be applied. Back in your EntryHandler you can add an if statement so that the modification is only applied to your new control.

{
    if (view is NoUnderlineEntry)
    {
        handler.NativeView.BackgroundTintList = Colors.Transparent.ToNative();
    }
}

Just like that you now have the power to customize MAUI controls quickly and easily!

Accessibility

You may have created a beautiful new MAUI application that kicks butt and looks great, but have you ever heard it through a screen reader? A screen reader is a technology that assists people who have difficulties seeing and interacting with digital content using audio or touch senses. People who use screen readers are often blind or have very limited vision. They rely on their device’s native screen reader to describe the UI on their phone to help them interact with it. Each OS has its own screen reader and they have subtle differences in how they operate. Android uses TalkBack, iOS uses VoiceOver, and Windows uses Narrator.

When it comes to working with screen readers .NET MAUI gives us several tools. SemanticProperties are great for describing a UI element and setting the order that the screen reader describes it. We can also programmatically set the screen reader focus of a UI element and use the screen reader to make an announcement.

Semantic Properties

Semantic properties are attributes that assign meaning to .NET MAUI controls and make them easier to navigate with a screen reader. The SemanticProperties class gives us three capabilities for working with screen reader semantics.

  • Description: Describes what the UI element is
  • Hint: How to use the element
  • HeadingLevel: Marks the control as a heading and assigns it a level to make it easier to navigate with a screen reader.

The HeadingLevel attribute can be assigned a value of “None” or a “Level1” through “Level9”. The level of a heading is used differently by each platform; however, it’s generally used to make UI comprehension easier.

You can add semantic properties to any UI control in XAML like this:

<Button Text="Share my blog post!"
        SemanticProperties.Description="Share"
        SemanticProperties.Hint="Share this post"
        SemanticProperties.HeadingLevel="Level1" />

Another way to assign semantic properties is programmatically in C#:

SemanticProperties.SetDescription(shareButton, "Share");
SemanticProperties.SetHint(shareButton, "Share this post.");
SemanticProperties.SetHeadingLevel(shareButton, SemanticHeadingLevel.Level2);

Screen readers use focus to indicate which element currently has the user’s attention. Whichever element is in focus is the one the screen reader will describe to the user. You can set the focus of a view by calling its SetSemanticFocus method.

shareButton.SetSemanticFocus();

You can also access the screen reader directly and use it to make audio announcements. This will only work if the user has enabled the screen reader on their device, so it’s not the same as text-to-speech necessarily.

SemanticScreenReader.Announce("What is your favorite thing about .NET MAUI?");

Conclusion

There are so many cool new innovations and features in MAUI and for Xamarin developers this language feels familiar and the transition to MAUI will be smooth. Many of these changes were created with us, the developers in mind because they make coding in the language a dream. There are soo many new innovation in the .NET MAUI framework and these are just a few of them. I hope that you enjoyed reading this article as much as I enjoyed writing it.

2 thoughts on “Fundamental Changes from Xamarin.Forms to .NET MAUI”

  1. Great Article,
    Can you please discuss what happens to Xamarin Forms String Resources like: Properties.Resources.Cancel that maps to differ languages in resource files. How does this change in MAUI?

    • Hi Salah, I was able to create a bilingual .resx file in .NET Maui so this is still an acceptable method. There may be changes to this. It seems they are publishing new documentation every week on .NET Maui although they’re no always clear on whats new vs what old. I’m keeping my eye on this.

Comments are closed.