At my current project we have an extensive mobile application built with Xamarin.Android and Xamarin.iOS. As Xamarin.Forms evolved, we started adapting this into our applications by embedding Xamarin.Forms pages into our “native” apps, as rewriting the entire app would be too time-consuming. As Xamarin’s end of support is closing in (May 1, 2024), I started the process about a month ago of rewriting the app to what is now called “.NET for Android” and “.NET for iOS”. This also included rewriting the embedding technique and the Xamarin.Forms pages to .NET MAUI pages. In this post I will explain my process and how my experience was with this migration. I will also talk about the .NET Upgrade Assistant and how this worked for me. Bear in mind that I am still not done with the whole rewrite, but I have tackled the most difficult problems thus far.
Migration tool vs. manual upgrade
Microsoft has a migration tool called the .NET Upgrade Assistant, which can help you upgrade your projects to the latest target framework. You can download this and use it to migrate your Xamarin.iOS and Xamarin.Android applications to “.NET for iOS” and “.NET for Android”, respectively. It also supports many other project types.
For my first attempt at migrating the iOS application, I used the upgrade assistant and pointed it at the csproj for the iOS project:
upgrade-assistant upgrade .\Njord.Iphone.csproj
Pro tip: when using the upgrade assistant, use the --non-interactive
flag. That way you don’t have to click Enter for each step. If you’re curious as to what the tool actually does or you want to skip certain steps, you can omit it, but otherwise I highly recommend you use it. Due to the somewhat complex project structure of the iOS application, I ended up getting some cryptic error messages on this first migration try that I didn’t get to the bottom of (some XAMLC errors). I ended up reverting and started my second attempt by going for the more manual approach and created a new .NET 7 iOS project and moving piece by piece over. That way I got an iterative approach which ultimately led me to the cause of the error message (a lingering reference to the Xamarin.Forms NuGet package somewhere that I forgot was there).
I started with copying the contents of the AppDelegate.cs
, since this is usually the starting point of any iOS application. It’s also where most of the initialization of app structure and plugins/packages happen, so I could focus on solving these issues before moving on. The advantage of doing the manual upgrade was that I was able to clean up some old files, but it is substantially more time consuming than using the upgrade assistant.
For the Android application – since I now had solved a lot of the issues in the iOS application that were common for Android – I used the upgrade assistant with the --non-interactive
flag:
upgrade-assistant upgrade .\Njord.Android.csproj --non-interactive
This went far better than expected. The project structure was upgraded to the new SDK style, target framework was updated to .NET 7 and redundant references were removed. The only thing was that a NuGet package was erroneously removed with the claim that it did not support .NET 6/7 (which it did).
MAUI embedding
In this project, we use a shared class library (targeting netstandard2.0
) which houses the Xamarin.Forms pages. This class library had to be upgraded to target both net7.0-ios
and net7.0-android
. In addition, the Xamarin.Forms pages had to be upgraded to .NET MAUI pages. What I will focus on here, though, is how I got the embedding of the MAUI pages to work with both the iOS and Android application.
James Clancey explains how to use MAUI embedding here. I used this to create a global static MauiContext
, which could be accessed from anywhere in the application so that I could embed pages from anywhere and not just from AppDelegate.cs
or MainActivity.cs
.
For iOS, this turned out to be a bit tricky when you’re dealing with storyboards. If your application starts with a storyboard, you won’t be able to save the MauiContext
, as you need the application’s root UIWindow
. I solved this by programatically instantiating my starting point controller and setting this as the window’s root view controller:
Window.RootViewController = new StartingPointController();
That way I didn’t need the storyboard to handle it. And yes, storyboards are a nightmare and I am glad I was forced to get rid of this approach.
A small side effect from enabling MAUI embedding in your iOS/Android application is that implicit usings seem to take a hit. You’ll have to explicitly reference your common Android and iOS namespaces (Foundation
, UIKit
, AndroidX
etc.). There are also some ambiguous references between MAUI and Android/iOS namespaces, so you will probably have to resolve some of those too.
When it comes to Android, I did experience some issues with rendering MAUI pages. It seems at the time of writing, any MAUI page with a Button
is unable to render on Android. You just get the following error:
The style on this component requires your app theme to be Theme.MaterialComponents (or a descendant).
Even though I did set the app’s theme to this theme, it still threw the error. I’ve filed an issue and hope that this will be resolved soon.
Supported packages
A very important part of this process was to investigate if the packages we used in these projects had been updated to .NET 6/7. If they hadn’t, I had to look for replacements. Luckily, in my case, it seems that all the packages we use have been updated or has gotten replacements. Some of the ones we had to replace were:
- amay077/Xamarin.Forms.GoogleMaps -> themronion/Maui.GoogleMaps
- rotorgames/Rg.Plugins.Popup -> LuckyDucko/Mopups
Some of these also required some special initialization in order to work with the embedding technique.
Bumps in the road
I did encounter some issues along the way:
- Visual Studio does not fully supporting MAUI embedding. The available deployment targets disappear from the dropdown list in Visual Studio once you add
<UseMaui>true</UseMaui>
to your csproj and you’ll be forced to deploy to the selected deployment target before you added this to your csproj. I’ve filed a bug for this. - XAML Hot Reload slowed down debugging since it was struggling to initialize it. Not sure why, but I disabled it in Visual Studio to ease the testing of the migration.
- Android required some extra rewriting, as the Xamarin.Forms embedding formerly returned the converted page to a
Fragment
but now returns aView
. - Some Android theming caused (and is still causing) issues, as mentioned before.
- Lifecycle events for navigation didn’t work as intended any longer.
OnAppearing
now only fires once andOnDisappearing
never fires. This might be because we tap into the native application to do the navigation so we have some rewriting ahead of us.
Key takeaways
If you are about to embark on the same journey that I have been on, here are some tips:
Start early
Create a branch today and start migrating now. Even though some of your packages might not have been replaced/updated yet, there are probably other challenges to face that you’ll want to tackle as soon as possible.
Use the upgrade assistant
Unless you want or need to do a manual upgrade, use the .NET Upgrade assistant to save time. Remember to use the --non-interactive
flag.
Map out your packages
Get an overview of what packages you need to replace or update. Most of the package replacements can be found through Google searches, but you could also reach out to the .NET MAUI engineering team (f.ex. Gerald Versluis) as they are pretty on top of it.
Be patient
In my case, there were – and are – quite a bit of hurdles to jump over in this process. Depending on your strategy (manual vs. automatic upgrade) this migration might take some time, so make sure to be patient.
I hope you found this post useful. If you’re about to start your migration process, do reach out if you need some help or have questions. Unfortunately the project I have been working on is closed-source, but I might be able to provide snippets if needed.
Have you experience any problems with migrating custom renderers or custom controls?
Not really, we only had one custom renderer and it was converted just fine