Migrating From XamarinForms To NET MAUI - Iris Classon
Migrating From XamarinForms To NET MAUI - Iris Classon
Xamarin.Forms
Xamarin’s story starts all the way back in 1999, with Miguel de Icaza and
Nat Friedman. The two of them cofounded Helix Code, later known as
Ximian. The company focused on GNOME-oriented software and Mono, a
project that aimed to implement Microsoft’s .NET development platform.
When Ximian was acquired in 2011, Miguel de Icaza started Xamarin
(Figure 1-1), a company that developed a platform for mobile app
development. In the subsequent years, Xamarin released Xamarin Studio, an
open source IDE, and Xamarin.Forms (officially launched in 2014).
https://doi.org/10.1007/979-8-8688-1215-6_1
Chapter 1 the evolution from Xamarin.forms
Common Questions
MAUI, you won’t be able to publish new apps or update existing ones, as
Xamarin.Forms won’t meet these new platform requirements.
Yes, it is. MAUI has been stable for a while, and the team continues to
address issues and bugs. Several major apps are built with .NET MAUI,
such as
MAUI. Business logic and non-UI code can mostly be left as is. However,
some adjustments are necessary for UI code, particularly custom renderers
and layout controls. There are some significant layout behavior changes, and
we will cover those later in the book. The new single project structure is
recommended in .NET MAUI, which means you’ll have to consolidate
platform-specific code into one project. This requires some reorganization of
your current project setup.
Many, but not all, third-party libraries support .NET MAUI, and the number
is growing by the day. You could, depending on the license, migrate a library
yourself, but chances are somebody has already done that. If not, look for
alternatives.
Forms, which required a separate project for each platform. .NET MAUI
provides a unified API that abstracts platform-specific details. Performance
is greatly improved; we have Blazor integration, better data binding, richer
controls, and Hot Reload. Although these are a handful of noteworthy
differences, we will explore many more as we go further into the book.
Summary
For many cross-platform developers, .NET MAUI ticks all the boxes, but the
most important box is future-proofing your application. What started as the
Mono project evolved into Xamarin, then .NET MAUI, and today we have a
framework that has been around for more than a decade. It has endured a
great deal, and as is typical with open source projects, there are diverse
viewpoints. With that said, this is not the only cross-platform framework out
there, and in the next chapter, we will dive into .NET
MAUI’s features, compare it with alternatives, and address its future.
CHAPTER 2
Alternatives, and
Future
.NET MAUI isn’t the only choice for cross-platform development, as there
are other options available regardless of your preferred programming
language or framework. My team thoroughly explored different options and
had numerous in-depth discussions. In the end, we felt confident about our
decision to use .NET MAUI. In this chapter, we will take a look at the
features and improvements of .NET MAUI and examine different
alternatives, such as Avalonia and Flutter. Last, we will discuss the future of
.NET MAUI. By the end of this chapter, I hope you feel confident about
your choice.
Features
Earlier in the book, I briefly touched upon some of the features and
enhancements, but I haven’t delved into them extensively. Let’s examine
those more closely.
https://doi.org/10.1007/979-8-8688-1215-6_2
Performance
Performance has been an important goal for the .NET MAUI team, and
we’ve seen improvements right off the bat with the first release. One specific
test, called LOLs per second, was used to measure page rendering
performance. This test involves rendering a large number of labels with the
text “LOL,” each with random colors and rotations. In this test, .NET 7
.NET 6 MAUI.
6
Chapter 2 .Net MaUI: FeatUres, alterNatIves, aNd FUtUre In addition to
faster rendering, scrolling performance also improved significantly, with
smoother scrolling and reduced GPU spikes, especially on lower-end
Android devices. Figure 2-1 highlights these performance improvements,
measured using the LOLs per second test.
Figure 2-1. LOLs per second measures the number of “LOL” labels per
second. Performance has more than doubled from Xamarin.
Don’t miss out on the comments; they’re really good, so give them a read.
Improved Abstractions
Besides performance, my second favorite improvement was more and better
abstractions. With the launch of what was formerly referred to as
.NET Core (.NET 5), Microsoft went all in with abstractions, best practices,
7
MAUI. .NET MAUI has built-in support for dependency injection (DI),
leveraging the same DI framework as ASP.NET Core, but it doesn’t stop
there. For example, .NET MAUI replaces custom renderers with unified
handlers, which are more efficient and easier to implement. Platform-
specific APIs are abstracted, allowing us to access platform-specific
capabilities with a unified API call. You can also see this with UI elements.
For example, I’ve spent more time getting a shadow just right, using custom
renderers for the different platforms, than any human should use.
I used to joke that the shadow literally, and metaphorically, followed me.
Hot Reload
Hot Reload in .NET MAUI enables real-time updates to the app’s UI and
logic without restarting the application. This feature significantly speeds up
the development process by allowing developers to see changes instantly. It
really makes UI work less painful and supports live debugging.
.NET MAUI’s Hot Reload feature primarily supports XAML Hot Reload
and does not support C# code reloading directly. This means that while you
can make changes to your XAML code and see them reflected in real time
without rebuilding the application, changes to C# code, such as event
handlers, require a rebuild. Additionally, XAML Hot Reload supports
simultaneous debugging of multiple platforms (using Visual Studio).
However, separate head projects per platform is a requirement, rather than a
single project app. Simultaneous debugging allows you to deploy your code
to both Android and iOS devices simultaneously, so you can see the changes
on both platforms at the same time.
Border
In the past, I’ve used the Frame control and padding or a third-party library,
but .NET MAUI includes a Border control that allows for easy
customization.
GraphicsView
• Custom drawing
• Animations
• Event handling
View Shadow
MAUI views have a shadow property, which means we don’t need to handle
the shadow for the different platforms. However, I want to add that there
have been a few issues with the shadow property, in particular with nested
views. And at the time of writing, the shadow property renders differently on
the different platforms. We’ve had to add handlers to set different shadow
properties depending on the platform.
There are more improvements beyond those I mentioned earlier, and I’m
sure I’ve left out some of them by accident. The following is a concise list,
though it is not extensive:
10
to streamline app development, you can specify images in one place within
your project, and they will be automatically resized to the appropriate
resolution for the target platform and included in the app package when
building. please note that .Net MaUI converts svG files to pNG files. If you
use svG files, make sure you reference them with the .png extension in your
views (regardless of whether it’s XaMl or C#).
Alternatives
11
Chapter 2 .Net MaUI: FeatUres, alterNatIves, aNd FUtUre
.NET Alternatives
Being able to use the existing expertise of .NET developers on your team is
a big plus, but .NET MAUI isn’t the only .NET alternative. Let me provide
you with a few options.
Uno Platform
Uno Platform allows you to build native mobile, desktop, and web apps with
a single codebase using C# and XAML. It focuses on providing a consistent
UI experience across all platforms.
Pros: Strong alignment with Microsoft’s ecosystem, support for WinUI, and
extensive platform coverage
Pros: Rich styling capabilities, support for MVVM, and a vibrant open
source community
Cons: Still maturing in terms of mobile support and may lack some of the
out-of-the-box integrations available in .NET MAUI Non-.NET
Alternatives
12
Pros: Fast performance, expressive and flexible UI, and a large community
Cons: Uses Dart instead of C#, which might require learning a new language
for .NET developers
Ionic
This is a framework for building cross-platform mobile and web apps using
web technologies like HTML, CSS, and JavaScript.
Titanium
I had to give Titanium a mention, even though I’m not sure how up to date
the framework is. Remember the App1 I mentioned at the start of the book?
Well, when I ported my Windows Phone app to iOS, I used Titanium and
Titanium Studio. Titanium is an older framework for building native mobile
apps using JavaScript.
13
.NET MAUI has a bright future ahead, thanks to ongoing investments from
both Microsoft and the open source community. While there may be
skeptics, even negative feedback is highly valuable and actively contributes
to the growth of this framework. I have personally submitted multiple issues
to the repository and actively participated in discussions without hesitating
to provide constructive criticism. While MAUI has a promising future, I
understand the concerns and hesitations of some who fear it might meet the
same fate as Silverlight, Windows Phone, and other technologies. One thing
I am certain of is that the .NET MAUI team is fully committed to the
framework. There is no evidence to suggest that this is a temporary project.
On the contrary, the framework has experienced significant advancements
and an increase in contributors and followers.
MAUI apps sends out a clear message: companies are willing to invest. The
road maps are public and can be found on GitHub at https://github.
com/dotnet/maui/wiki/Roadmap.
Release Schedule
Unlike .NET itself, .NET MAUI does not have Long-Term Support (LTS)
versions, meaning each major release is supported for a minimum of six
months after the next major version is released. To maintain support,
developers need to upgrade to the latest major version within this period.
14
.NET MAUI is here to stay, and as the next step in the Xamarin.Forms
evolution, it brings better performance, new and improved controls, simpler
project structure, Hot Reload, and so much more to the table.
alternatives, such as Flutter. Only you and your team know what is the best
fit, but .NET MAUI has long-term support, a public road map, and has been
adopted by large enterprise companies. In other words, .NET MAUI is a
robust solution for cross-platform development.
15
CHAPTER 3
Introducing the
Application
In this chapter, I’ll introduce the fitness application I built specifically for
this book— AppForFitness. It’s going to be our case study as we navigate
the migration process from Xamarin.Forms to .NET MAUI. This app,
designed to track One Rep Max (1RM) progress across different exercises,
will help us explore the practical challenges and benefits of migrating a real-
world application to MAUI.
Let’s dive into what the app does, how it’s structured, and what we need to
keep in mind as we move to .NET MAUI.
Application Overview
At its core, AppForFitness helps users track their 1RM progress for various
exercises, like the bench press, squat, and deadlift. The app fetches exercise
data from an external API, caches the data, and displays user progress
through interactive charts. The app securely stores local data and preferences
such as app language, user information, and user preferences for the
calculations. It’s a simple, yet incomplete, fitness tool, with custom effects,
deep linking, gesture-based interactions, and real-time localization.
© Iris Classon 2025
17
https://doi.org/10.1007/979-8-8688-1215-6_3
These pages were designed to showcase migration challenges, but I’ve also
aimed to make it function and behave like a fully working application.
Main Page
The main page of the app provides users with quick access to the core
functionalities. It uses a TabbedPage that lets the user navigate to key areas
like progress tracking, entering new data, and adjusting settings.
Progress Page
The Progress page (Figure 3-1) is the app’s core feature, allowing users to
visualize their One Rep Max (1RM) progress across different exercises and
muscle groups. Using OxyPlot to generate dynamic charts, users can
• View how their 1RM has progressed over time for various exercises.
• The charts retrieve data from an external API and plot it based on the
selected exercise and muscle group.
• A Quick Add feature lets the user add an entry faster, by preselecting the
exercise.
18
Chapter 3 IntroduCIng the Case study: a FItness applICatIon
Figure 3-1. Progress page shows the 1RM chart and Quick Add entries
19
The Entry page (Figure 3-2) allows users to input new workout data. The
user can access this page from the Quick Add or by clicking the tab for the
page or by deep linking from an external source such as a website.
Deep linking allows users to navigate directly to specific pages in the app
from external sources, such as a website or an email. For example, a link like
http://appforfitness.com/entrypage?exerciseName=Squat takes users directly
to the Entry page for the squat exercise without needing to manually
navigate. The app listens for these URLs, extracts parameters (like the
exercise name), and navigates the user to the correct page.
Key features:
weight, reps)
• Accessible directly from deep links, enabling users to add entries from
external sources, like a website 20
Chapter 3 IntroduCIng the Case study: a FItness applICatIon
The Settings page (Figure 3-3) gives users control over key app settings, for
example, the default formula for 1RM. I’ve omitted other settings.
Key features:
Figure 3-3. Settings page lets the user select the default formula 22
The Language Selection pop-up (Figure 3-4) is a custom pop-up built using
a pop-up control. It allows users to choose the language they wish to use
within the app. The app will update text dynamically when a new language
is selected, without requiring a restart.
A little note on updating the text: I’ve only added localization to parts of the
app to avoid adding too much code which would make it harder to follow the
examples in the book.
Key features:
23
Chapter 3 IntroduCIng the Case study: a FItness applICatIon
Figure 3-4. Language pop-up lets the user set the default language for the
app
24
The Email pop-up (Figure 3-5) is designed for direct interaction with users
who need to provide feedback or request support.
Key features:
• Opens the user’s email client with a prefilled template for ease of use
Figure 3-5. Email pop-up contains an email link that will open the phone
email app with prefilled template
26
Chapter 3 IntroduCIng the Case study: a FItness applICatIon Application
Structure
27
Chapter 3 IntroduCIng the Case study: a FItness applICatIon
Figure 3-7. Android and iOS project structure I structured the app with
separation of concerns, using defined models, views, services, and platform-
specific implementations to handle functionality across iOS and Android.
28
Chapter 3 IntroduCIng the Case study: a FItness applICatIon These are the
main components:
• Custom controls and effects: I’ve built controls such as the IconButton,
LanguagePicker, and LongPressView to extend standard Xamarin.Forms
components.
Technical Features
29
The app uses OxyPlot to generate interactive charts showing the user’s 1RM
progress. This was added so we can take a look at dealing with third-party
dependencies. There are other libraries as well, such as SkiaSharp,
Newtonsoft, and more. Some third-party libraries do not have .NET MAUI
equivalent.
Navigation
Users can quickly log their most recent workout from the main screen by
clicking a button. This is done by using the Navigation object. Navigation in
.NET MAUI has changed (for the better).
Deep Linking
As mentioned earlier, the app supports deep linking, making it possible for
users to navigate directly to specific pages from external sources. This is
particularly useful for linking to the Entry page from the app’s website or
from other external content like blog posts. Deep linking is slightly different
in .NET MAUI.
Localization
The app supports multiple languages, and users can change the language on
the fly without restarting the app. .NET MAUI provides options to
localization implementation.
MessagingCenter
30
Long-Press Gesture
Accessibility is a crucial focus of the app, and several features are in place to
ensure it’s usable for all audiences. .NET MAUI has further improved this
with SemanticProperties.
The app’s UI is built with a resource dictionary, which defines global styles
and colors for consistency. Although the app doesn’t yet support theme
switching (e.g., light/dark modes), the structure is in place for easy future
updates. We will take a look at what has changed or improved in
.NET MAUI.
Splash Screen Updates
31
Custom Renderers
The app has a couple of custom renderers which we’ll use to discuss at the
.NET MAUI equivalent, handlers. For example, we’ll take a look at the
app’s custom image button that supports shadows for transparent images
with irregular shapes. Custom renderers have for many been one of the pain
points when migrating.
Custom Controls
In addition to the shadow effect, the app also features custom controls like a
pop-up layout and a LanguagePicker. These components demonstrate how
custom features can enhance the user experience and will be streamlined
during the migration to MAUI.
Layouts
Image Management
The app uses both embedded images and platform-specific resources which
will be helpful for us when we look at image management in .NET MAUI.
32
Practices
If you take a look at the source code of the AppForFitness project, you’ll
probably notice something right away—the application doesn’t exactly
follow best practices or use the most popular design patterns. In fact, the app
relies heavily on the dreaded code-behind pattern, something many
developers aren’t too fond of. You might be thinking, “Why not use MVVM
Had I written this app using a clean MVVM pattern or other separation of
concerns from the beginning, the migration would certainly have been easier.
But that’s not what many of you will be dealing with when migrating your
own apps. I want to make sure we cover scenarios that reflect reality—
where technical debt is a factor and where refactoring may be necessary as
part of the migration process.
That being said, I don’t want you to focus too much on the code itself.
Remember, this is an example app with a specific purpose: to show the
process of migrating to .NET MAUI. There are gaps in the implementation
—missing exception handling, placeholder features, and the like—because
the goal here is to demonstrate the before and after and highlight the
challenges we face along the way.
33
But for now, let’s keep our focus on the migration itself and how .NET
Summary
The app has several pages—Main, Progress, Entry, and Settings—that each
serve a specific purpose for our migration walk-through.
In the next chapter, we’ll prepare for the migration process by setting up the
development environment and assessing the current state of the application.
We’ll review third-party dependencies, ensure everything is up to date, and
compile a checklist for a smooth migration.
34
CHAPTER 4
Preparing
for Migration
Good preparation is key to a smooth migration, and taking the time to set
things up correctly at the start can save a lot of headaches down the line.
In this chapter, I’ll walk you through the essential steps to get everything
ready before diving into the migration process. We’ll cover setting up your
development environment for .NET MAUI, making sure your Xamarin.
Forms project is using the latest version, and taking stock of your app’s
dependencies and native code.
As this book covers migrating from Xamarin.Forms, I’m assuming you have
a working Xamarin.Forms development environment for your current app,
but I will briefly cover the steps for setting up your environment and a
remote Mac host in a Windows virtual machine (VM). We are going to use
Visual Studio on Windows for parts of the migration. The reason is because
there is a very helpful tool that we can use that has not been ported to Mac
OS at the time of writing. This tool is .NET Upgrade Assistant, a tool that
helps automate much of the migration process by analyzing your Xamarin.
Forms project and making the necessary changes to convert it into a .NET
MAUI project. Although this tool simplifies many steps, it’s currently only
available on Windows, which is why we’ll be using a Windows VM
alongside our Mac setup. Don’t worry; we’ll walk through the manual steps
as well, ensuring you understand every part of the process.
35
https://doi.org/10.1007/979-8-8688-1215-6_4
app extensions, or binding libraries (to name a few), please refer to the
Microsoft documentation here: https://learn.microsoft.com/en-us/
dotnet/maui/migration.
OpenGL has been removed from iOS, and if your app relies on OpenGL
features.
36
Here is a short summary for setting up Rider on your Mac: 1. Install Rider:
https://www.jetbrains.com/rider.
en-us/download.
android.com/studio.
3. Create a first project and accept the prompt to install missing tools.
37
OceanofPDF.com
Chapter 4 preparing for Migration
Figure 4-1. Modify your Visual Studio installation to include the Desktop
and Mobile development module
Setting up the environment for .NET MAUI is the same for both platforms
as it can be done using the global dotnet tool which is installed with the
dotnet SDK. .NET MAUI is installed as a workload.
38
When you run the command above, it installs the .NET MAUI workload and
its associated tooling. By default, it includes support for all the target
platforms that .NET MAUI supports: Android, iOS, macOS, and Windows.
However, to confirm that it is installed for all platforms, you can check for
specific workload packs, such as
• maui-android
• maui-ios
• maui-maccatalyst
• maui-windows
When you run a workload installation command for the first time, it will take
a few minutes.
Setting Up a Windows VM
39
com/en-us/software-download/windows11.
3. Create a new virtual machine (the steps are similar for all virtualization
software):
40
OceanofPDF.com
Chapter 4 preparing for Migration
2. Run the following command in your terminal to get the username for your
Mac: whoami
3. In Visual Studio, select Tools ➤ iOS ➤ Pair to Mac (Figure 4-2) and
select your Mac.
4. If your Mac is not on the list, confirm it’s on the same network, and
manually add the Mac by using
deploying to iOS.
Figure 4-2. Pair to Mac can be found under the Tools tab in the main menu
41
OceanofPDF.com
Chapter 4 preparing for Migration
Figure 4-3. By default, your Mac should show up in the list. If you can't see
it there, add it using the IP address from earlier Premigration Checklist
Before we begin the code inventory, there are a few foundational tasks we
should complete to ensure a clean starting point for the migration. These
steps shouldn’t take long, and while steps 1 and 2 are not mandatory, I
strongly recommend following them.
of Xamarin.Forms
If you’ve already upgraded your project, please make sure you do indeed
have the latest version of Xamarin.Forms.
Chapter 4 preparing for Migration more time to migrate their apps to .NET
MAUI. The extra time allowed Microsoft to address remaining bugs and
compatibility issues and ensure better support for developers who might not
be ready to migrate immediately.
Another significant reason for this update was Apple’s strict requirements.
Apple regularly releases updates for iOS, macOS, and Xcode, and they
enforce strict compatibility rules. Any app developed for iOS must be built
with a recent version of Xcode to remain compliant with Apple’s App Store
policies (which is also why we have to migrate to
.NET MAUI, if we want the app to run on iOS18). By upgrading to the latest
version of Xamarin.Forms, your project will have improved compatibility
with the latest versions of iOS, Android, and their respective SDKs, which
will in turn make the migration to .NET MAUI easier.
Before you upgrade your NuGet packages, make sure that you have the
appropriate version of Xcode:
xcode-select –install
xcodebuild -version
download the version. If you update via App Store, you’ll end up with
version 16 or later, which will not work with Xamarin.Forms 5.x.
https://developer.apple.com/download/.
43
Chapter 4 preparing for Migration 5. Download the file, expand and unpack
by double-clicking it, and give it a unique name.
Xcode<version>.app
xcodebuild -version
You can also set the Xcode version by setting the path for the Xcode version
you want to use in Rider or Visual Studio.
Visual Studio: Tools ➤ Options ➤ Xamarin ➤ iOS Settings Rider: Settings
➤ Build, Execution, Deployment ➤ Apple Platforms In addition, get the
latest versions of Android SDK for your IDE. If you are using Rider, then
this can be done through the Android SDK
Build your solution, run any tests you might have, deploy the app, and verify
the app starts without issues.
This step is crucial as we want to ensure the app is functioning perfectly and
have a solid backup before proceeding with the migration.
1. Update dependencies:
44
c. If any errors or issues arise during testing, fix them immediately and rerun
the tests.
b. Commit changes: Once you’re confident that the app is updated and
running as expected, commit the changes to your version control system.
c. If you don’t have version control in place, at the very least, create a
manual backup of the entire codebase.
a. Test deep links and other external entry points to make sure they behave
correctly. The AppForFitness deep link can be tested by using the terminal:
io/AppForFitnessApi/entrypage?exerciseName=Squat"
com.companyname.AppForFitness
adb devices
45
Chapter 4 preparing for Migration You can also use this site, where we fetch
the
exercise catalog and other data. Click the button to get routed:
https://irisclasson.github.io/
AppForFitnessApi/
5. Test the app’s functionality. Pay special attention to custom controls and
renderers to ensure they work without issues.
6. If your app uses Bluetooth, network connectivity, or requires permissions
(e.g., for the camera or
By ensuring that everything works and is backed up, you’ll minimize risks
during the migration process and ensure you have a stable fallback in case
anything goes wrong. I’ve had to learn this the hard way a few times; don’t
make the same mistake. It’s better to commit and push often. Now, let’s shift
our focus to the inventory.
46
• Identify code that would benefit from refactoring. If your inventory reveals
significant technical debt or outdated patterns, you can make a note, and if
time allows, address those during or before the migration.
allows you to evaluate how the migration will impact your testing
infrastructure and CI/CD pipelines. This book does not cover this aspect, as
these pipelines vary wildly from project to project. I would like to point out
that the retirement announcement of Microsoft App Center (used for
analytics and crash reporting) had a direct impact on our pipeline. We also
discussed maintaining two solutions side by side, which I’ll get back to in
the next chapter.
47
Chapter 4 preparing for Migration Note if the migration is urgent, keep the
inventory succinct and don’t delve too long on specific points. You will
discover additional issues and maybe find out that some issues were non-
issues, as you migrate, so don’t let the inventory slow down the process too
much.
two developers should be able to complete this task in less than half a day or
maybe a full day, if there are some points that need to be discussed. Do take
note of code that could be refactored but make it a lower priority if you are
pressed for time.
You can use these categories as a starting point, but feel free to customize
them to match the app’s functionality:
• Controls
• Views
• Renderers
• Effects
• Other
48
OceanofPDF.com
Chapter 4 preparing for Migration
The NuGet window, in Rider or Visual Studio, shows explicitly added
packages, as well as implicit dependencies. There are more than 50
implicit dependencies for this small app, so the list is going to be very long.
Place emphasis on the direct dependencies and identify their usage locations.
In Rider, you can right-click a dependency and select Find Dependent Code
(Figure 4-4), and in Visual Studio you can install ReSharper by JetBrains
(use a free trial) to access the same feature.
49
OceanofPDF.com
Chapter 4 preparing for Migration
Figure 4-5. AppForFitness references the Newtonsoft package, but uses it
for a single line of code that can be replaced by .NET libraries
Figure 4-6. SkiaSharp is used to resize a bitmap but doesn't require custom
parameters that do not exist in the built-in resize function for the .NET
bitmap object
However, OxyPlot doesn’t have the Find Dependent Code option, but we
know it is used for the progress chart. Another way to check dependency
usage is to remove the dependency and try to build. Follow the errors, and
you’ll find the usage.
You can see the dependency spreadsheet for AppForFitness in Table 4-1.
50
Category Dependency
Version
MAUI
Action
Priority
Compatible
Upgrade to .net
high
MaUi
external netStandard.
2.0.3
Yes
Library
external newtonsoft.
13.0.3
Yes
Compatible. Can
Low
Json
be replaced with
System.text.Json
external oxyplot.
2.1.0
no (needs
find alternative
high
Xamarin.forms
migration)
external rg.plugins.
2.1.0
no (needs
high
popup
migration)
external SkiaSharp
2.88.8
Yes
Compatible, but
Low
can be replaced
external SkiaSharp.
2.88.8
Yes
Compatible. See
Low
Views
above
external Xamarin.
1.8.1
Yes
included in .net
Medium
essentials
MaUi
It may not seem like a significant number, but eight dependencies are
essential for the AppForFitness app to work effectively. However, in larger,
more complex applications, you’re likely to have many more dependencies.
For example, in the Plejd app that my team and I work on, we have 22
dependencies—and that’s after a lot of effort to keep the number low. We
also have over 150 implicit dependencies, which means that even though the
visible list of dependencies isn’t overwhelming, there’s a lot more happening
under the surface.
51
Even if you’re pressed for time and can’t immediately refactor out
unnecessary dependencies during migration, it’s important to revisit your
dependency inventory periodically. Use your spreadsheet as a living
document to track dependencies, mark potential issues, and plan for future
cleanups.
Internal Dependencies
52
• Sub Git Submodules: Shared code repositories that are linked into your
main project.
• A Git Subtree allows you to include one repository (your internal library)
inside another repository (your main app) while still treating the internal
repository as a separate entity. Unlike a submodule, a subtree copies the
internal library into the main project.
• Linked files: Files that are added as a direct link in several projects.
• Build artifacts: Artifacts added to the project or app as a part of the build
process.
Platform-Specific Code
53
As with renderers, you’ll likely find that you need fewer platform-specific
views and controls in MAUI. The framework has added many cross-
platform capabilities, meaning that some of the custom views you built for
each platform in Xamarin.Forms may now be unnecessary in MAUI. This
results in cleaner and more maintainable code, with fewer platform-specific
dependencies.
Resources
When documenting resources, focus on where they are located and how they
are accessed in your project (e.g., whether they are embedded resources,
platform-specific images, or shared resources). You don’t need to spend too
much time on specifics at this stage, but it’s helpful to note their paths and
usage patterns to ensure they migrate smoothly.
54
\tree -I 'bin|obj'
On Windows, you can use PowerShell and the following function: function
Show-Tree {
param (
[string]$Path = ".",
[int]$Depth = 0
if ($item.PSIsContainer) {
} else {
Show-Tree
55
aUi
tibility
w effect
Notes
Layout adjustments
may be needed
Layout adjustments
may be needed
ensure layout
compa
be removed
Simplify with M
shado
tor
ven
Priority
high
high
Medium
Medium
Medium
Medium
ode in
Action
refactor to
handler
refactor to
handler
refactor to
handler
refactor to
handler
replace
replace
specific c
alent
aUi
aUi
aUi
aUi
aUi built-in
aUi built-in
latform-
MAUI
Equiv
Use M
handlers
Use M
handlers
Use M
handlers
Use M
handlers
replace with
replace with
f the p
tforms
tforms
tforms
tforms
ult o
Current
Usage
all
pla
all
pla
all
pla
all
pla
ioS,
android
ioS,
android
es
g the rinail
et det
pickerge
effect
effect
pressView
eadshe
Component
iconButton
Langua
Long
popupLayout
Logging
Shado
Spr
effect
effect
Table 4-2.
Categor
Custom Control
Custom Control
Custom Control
Custom Control
Custom
Custom
56
page
tibility
tion
tin
tabbed
( con
android
for
tform compa
Layout adjustments
ensure gesture
handling
adjust
changes.
version is empty
android
pla
Standard localiza
file
high
high
high
Lo
Lo
refactor to
handler
refactor to
handler
refactor to
handler
add to
shared
folder
no action
needed
aUi
aUi
aUi
Use M
handlers
Use M
handlers
Use M
handlers
Shared
resource
Shared
resource
tforms
tforms
ioS,
android
ioS,
android
ioS,
android
all
pla
all
pla
renderer
renderer
renderer
page
pressView
iconButton
Long
tabbed
bg.png
appres.resx
tion)
ge)
Custom
renderer
Custom
renderer
Custom
renderer
embedded
resource
(ima
embedded
resource
(localiza
57
ge
ge on both
Can be
tform
tion
tibility
tion works
te for M
gement
tforms.
tform independent
integra
Notes
Same ima
pla
shared
Upda
compa
Cache mana
pla
Secure stora
integra
cross-pla
api
permission
mana
Priority
Medium
Lo
Lo
Lo
Lo
Lo
Action
replace
with
shared
resource
no action
needed
no action
needed
no action
needed
no action
needed
no action
needed
alent
MAUI
Equiv
replace
with shared
resource
no change
needed
no change
needed
no change
needed
no change
needed
no change
needed
tforms
tforms
tforms
tforms
tforms
Current
Usage
ioS,
android
all
pla
all
pla
all
pla
all
pla
all
pla
vice
vice
vice
vice
vice
talogueCacheSer
geSer
d) ue
apiSer
tin
Component
dumbell.png
DeepLinkingSer
exerciseCa
SecureStora
fitness
permissionSer
( con
ge)
vice
vice
vice
vice
vice
Table 4-2.
Categor
resource
(ima
Ser
Ser
Ser
Ser
Ser
58
• Current usage: Which platforms are currently using this code or resource?
Is it Android specific, iOS specific, or used across all platforms?
• Priority: Rank the priority of the migration for this element (High,
Medium, Low). This helps allocate
• Notes: Any additional notes or context for the component, such as potential
layout adjustments or any platform-specific quirks.
59
3. Prioritize actions to ensure that high-priority tasks are handled first, such
as critical renderers or views.
4. Track resources (images, fonts, etc.), noting their location. Later, you’ll
add information regarding whether they can be shared or if they need
App Navigation
Navigating between pages and views has evolved with .NET MAUI, and
understanding these changes is essential for a smooth migration from
Xamarin.Forms. So make sure to document how navigation is implemented
within your app, highlighting the use of built-in navigation, custom
navigation, and deep linking.
60
Navigation Type
Params (Yes/No)
Mainpage
tabs
no
progresspage
Built-in (navigationpushasync)
Yes
entrypage
Built-in (navigationpushasync)
no
no
emailpopup
Custom (popup)
no
entrypage
CHAPTER 5
Planning Your
Migration
With the data we collected in the previous chapter, we will strategically plan
our migration in this chapter, considering different approaches to project
structure and deciding between a complete or incremental migration.
Forms and .NET MAUI, we will be ready to transition smoothly to the next
chapter, where we will initiate the migration process with some minor
cleanup addressing some of the easy-to-fix findings from our inventory.
Migration Strategies
When you migrate to .NET MAUI, you can either do a full migration or an
incremental migration where you only migrate specific platforms. For
example, iOS is the most urgent migration due to the reasons I mentioned at
the start of the book. Android, on the other hand, has for many been more
challenging to migrate (mostly due to UI differences), and I know several
companies that kept Xamarin.Forms for Android and focused on iOS first.
63
https://doi.org/10.1007/979-8-8688-1215-6_5
For Android 15 (API level 35) and up and for Xcode 16 and up, you need
I’d like to delve deeper into the topic of versions. Migrating to .NET MAUI
impacts your app’s support for legacy versions for the targeted platforms.
At the time of writing, .NET MAUI apps can be written for the following
platforms:
64
Moreover, if you decide to incorporate Blazor along with. NET MAUI (more
information will be provided later in this chapter), you will have extra
criteria to fulfill:
I haven’t touched upon other platforms much as we are focusing on the two
major platform targets, iOS and Android, in this book, but I do want to
mention MacOS and UWP.
As with Android, there is better backward compatibility, but also the same
issue of missing out on newer features. In addition, if your app is distributed
through the Mac App Store, there are minimum SDK requirements for apps
to remain listed. Apple may enforce using certain features (like supporting
Apple Silicon architecture or integrating sandboxing). Not updating your
Xamarin.Mac app might lead to its removal from the Mac App Store.
65
Fortunately, a lot has changed since then. The multi-project template added
later is much more stable and widely used by many developers today. It
allows for greater flexibility, especially for those who want to stick to the
multi-project structure. For many developers, especially those with large,
complex apps or specific needs, the multi-project setup offers greater
separation between platforms, making it easier to manage platform-specific
resources, custom renderers, or third-party dependencies that differ by
platform.
66
Chapter 5 planning Your Migration In this book, I’ll be using the single
project structure when we migrate, to demonstrate the difference and
showcase how the unified project structure simplifies management for most
apps. While I’ve personally been tempted to recommend this approach to all
developers, especially because the single project structure received more
attention and “love” early on in the MAUI lifecycle, it’s not always the best
fit for every app.
Major Differences and How to Address Them
Let’s have a closer look at some of the bigger differences, how they affect us
and how we address them. This is not an exhaustive list, but I’ve done my
best to focus on what’s most noticeable.
Project Structure
Design Patterns
67
Handler Pattern
We’ll cover this in more detail further down, but custom renderers are
replaced with Handlers, using the handler pattern, which allow more
modular and flexible customization of platform-specific control behavior.
Builder Pattern
Service Pattern
These are just some of the many design patterns .NET MAUI has made first-
class citizens, following the pattern (pun intended) we see overall for newer
.NET frameworks.
68
When upgrading to .NET MAUI, one of the biggest changes is how layouts
and controls behave compared to Xamarin.Forms. Some layout behaviors
have changed, and default values for spacing and padding have been
adjusted, as well as internal logic. Here’s a breakdown of the major
difference.
In .NET MAUI, layout behavior has been improved to ensure that sizing
requests are honored more consistently. Some changes in layout behavior
might cause your existing Xamarin.Forms layouts to behave differently in
MAUI.
Frame
MAUI, Frame has been replaced with Border, which offers consistent
measurement across all platforms. However, Frame is still supported for
migration purposes. Consider transitioning to Border, and use styles to adjust
padding and behavior.
69
The biggest change in Grid is that .NET MAUI does not infer columns or
rows as Xamarin.Forms did. You now have to explicitly declare
ColumnDefinitions and RowDefinitions.
StackLayout
RelativeLayout
.NET MAUI discourages the use of RelativeLayout, which can only be used
if you include the Microsoft.Maui.Controls.Compatibility package.
ScrollView
the ScrollView will stretch to the full height of its content and won’t allow
scrolling. We had quite a few issues with ScrollView when we migrated, so
take note if your app uses ScrollView.
70
In .NET MAUI, resources such as fonts, images, and styles are managed
centrally in the shared project. Although you probably already centralized a
lot of the resource management in a shared project, .NET MAUI allows for
more shared resources. In addition, resources such as fonts and more can be
registered in the builder. We’ll get back to this during the migration.
Accessibility
.NET MAUI improves accessibility with better support for Semantic
Properties, which help make your app more accessible across platforms.
Localization
Localization in .NET MAUI has been simplified. All resource files (.resx)
are centrally located in the shared project, and MAUI provides better support
for switching languages dynamically by using the CultureInfo API to
dynamically switch between languages.
MAUI introduces more efficient and streamlined APIs for animations and
transitions, allowing for smoother performance across platforms.
71
In cases where you need highly custom gestures (e.g., multi-touch gestures,
drawing apps, etc.), .NET MAUI provides more flexibility in defining
custom gesture recognizers or extending the built-in gesture handling
system.
Performance
improves
compilation, which significantly reduces the time it takes for your app to
launch.
72
Both Hot Reload and Hot Restart have been improved in .NET MAUI,
allowing for faster debugging and testing. Use Hot Reload to update XAML
and C# code without rebuilding and Hot Restart to test changes on physical
devices without needing a full deployment. This is by far my favorite
improvement, as deploying to Android would take up to four minutes. Most
of our UI, in contrast to the AppForFitness app, is in C# and not XAML, and
we had to deploy often.
This is an opt-in feature, and there are some known limitations, for example,
reflection-based serializers such as Newtonsoft (which we are able to replace
based on our inventory), runtime code generation via JIT, dynamic assembly
loading, and some platform-specific limitations for WPF and Windows
Forms.
• Linker optimizations: The .NET Linker strips out unused code, resulting in
smaller binaries and faster startup times. This needs to be explicitly set for
application and can effect debugging experience.
73
initialization, which means that certain services and libraries are only loaded
when they are needed, instead of being initialized at app startup.
Summary
The fifth chapter concludes our journey before we commence our migration.
In this chapter, we’ve outlined our migration strategy, focusing on the choice
between complete and incremental migrations and project structure. We
discussed key differences between Xamarin.Forms and
In the next chapter, we’ll address some of the findings from our inventory,
introduce the .NET Upgrade Assistant, and kick off the migration process
with a focus on project structure, UI migration, and platform-specific
integrations.
74
CHAPTER 6
Executing the
Migration
We’ve finally arrived at the core part of the book—the migration! With all
the preparation we’ve done, the migration should proceed smoothly, though
some additional cleanup is still needed. In this chapter, we’ll go through
some minor refactoring opportunities before using the .NET
Upgrade Assistant.
Terminology
75
https://doi.org/10.1007/979-8-8688-1215-6_6
• Migrated shared project: The migrated version of the shared project, which
serves as an intermediary project during migration. This project will
eventually be discarded after the migration is complete.
• Main MAUI project: The final .NET MAUI project where all code,
including both shared and platform-specific resources and logic, will reside.
This project includes platform folders (like Platforms/Android and
Platforms/iOS) for platform-specific code, creating a unified project
structure.
This is to stay consistent and clear in our intent, as the process involves
significant restructuring and platform changes, not just a simple version
upgrade.
I cannot stress enough the significance of starting with the cleanest codebase
possible. If you’re pressed for time, you might need to leave some cleanup
for later, but we’ve identified a few minor refactoring opportunities in the
inventory chapter that we’ll address here:
76
being used.
Before making any changes, commit the current state of the app to ensure we
can revert if needed.
Removing Newtonsoft
77
Chapter 6 exeCuting the Migration We can modify this in three simple steps:
necessary because we are still using .NET Standard 2.0, which doesn’t have
System.Text.Json built-in.
ReadAsStringAsync();
Deserialize<T>(content);
3. Run and test the app, then commit the changes and push them to a
separate branch (e.g., migration
branch).
string values.
78
memory limits.
PropertyNameCaseInsensitive to true.
For example:
PropertyNameCaseInsensitive = true
};
https://learn.microsoft.com/en-us/dotnet/standard/
79
Before (SkiaSharp):
imageView.Height);
(bitmapDrawable.Bitmap,
bitmapDrawable.Bitmap.Width,
bitmapDrawable.Bitmap.Height,
filter: true);
3. Run and test the app again. Commit and push the changes.
We don’t want to clutter our code with unused classes, and therefore we’ll
remove the following unused classes and dependencies:
• LoggingEffect.cs
• TabbedPageRenderer.cs
80
Chapter 6 exeCuting the Migration Make separate commits for each of these
changes and push them to the remote repository.
Now that we’ve cleaned up unnecessary code and dependencies, we’re ready
to start the migration by using the .NET Upgrade Assistant. This tool will
handle much of the heavy lifting by updating our project structure and
converting key components to their .NET MAUI equivalents.
Let’s proceed!
.NET Upgrade Assistant
namespaces.
81
For this reason, even if you use the Upgrade Assistant, you’ll still need to go
through the code to resolve certain cases, especially when refactoring legacy
patterns to modern .NET MAUI equivalents.
Visual Studio Extension vs. CLI Tool
Installing the .NET Upgrade Assistant in Visual Studio You can install
the .NET Upgrade Assistant through Visual Studio’s Extension Manager:
Extensions.
Installing the .NET Upgrade Assistant CLI Tool Assuming you already
have dotnet installed, you can simply run the following in your preferred
command line which will install it globally: dotnet tool install -g upgrade-
assistant
82
OceanofPDF.com
Chapter 6 exeCuting the Migration
Once installed, the Upgrade Assistant can guide you through much of the
migration process, leaving you with fewer manual changes to handle.
However, you’ll still need to review your project for more complex cases,
such as third-party library compatibility and custom platform code. This is
particularly important for projects that use Xamarin-specific features,
custom renderers, or platform effects.
and Dependencies
In this section, we’ll use the tool to migrate the main project. I’ll cover the
steps for the CLI tool as they do the same things behind the scenes. The
fundamental difference is that the extension visualizes the steps and the
results, but the steps, information, and output are the same. The extension
equivalent of the CLI tool upgrade command is to right-click a project and
select Upgrade from the context menu (Figure 6-1).
Figure 6-1. The Update Assistant adds an Upgrade option to the project
context menu in Visual Studio
83
Chapter 6 exeCuting the Migration As for the CLI tool, which will be our
preferred method in this book, we’ll simply use our favorite terminal or
command-line tool, navigate to the solution root, and run
upgrade-assistant upgrade
Droid.csproj)
AppForFitness.iOS (AppForFitness.iOS/AppForFitness.
iOS.csproj)
Navigation
Exit
We have three projects, and we want to select the shared project, not the
platform-specific projects. If your solution has additional non-platform-
specific projects with Xamarin.Forms dependencies, run the tool for those as
well when you have followed the steps for the shared project.
The tool will always give the option to continue, or exit, so I’ll omit these
lines from here on. The extension tool in Visual Studio has the same steps
and options:
Side-by-side: This option copies your project and upgrades the copy, leaving
the original project alone.
84
In-Place Migration
What it does: This option upgrades your project directly without creating a
copy.
• Your project has been fully backed up via version control or manual means.
• You prefer to directly modify the current project files and don’t foresee
needing to refer to the original Xamarin.Forms setup.
Side-by-Side Migration
What it does: This creates a copy of your project and upgrades the copy,
leaving the original project intact.
.NET MAUI.
85
transition.
Let’s disregard the fact that AppForFitness is a simple app and imagine it as
something more sophisticated. Also, we might want to refer to the
Xamarin.Forms projects when we make changes, so we’ll select side-byside,
create a new project, and call it AppForFitnessMaui. We can always do a
name change when we are done.
In the next step, we can select the Target Framework, and we’ll go with the
option of the latest Long-Term Support (LTS) version of .NET, which at the
time of writing is .NET 8.
If you want to skip the step by step, you can use the following line: upgrade-
assistant upgrade <PROJECT_PATH> --operation SideBySide --
targetFramework LTS
--destination <NEW_PROJECT_NAME_OR_PATH>
86
OceanofPDF.com
Chapter 6 exeCuting the Migration
You’ll see the details of the migration if you scroll up, although they might
not be so easy to read. Copy the terminal output to a file for a better
overview. In Visual Studio, the Upgrade Assistant Window will show the
results using icons for the result.
For example, for this app, most of our items have a green checkmark, but a
couple have a lighter shade. These have the following message: No
applicable transformations found for this component, it is left unchanged.
See details in the Upgrade Assistant output pane.
In Visual Studio, you can view the output in the Output window by using the
drop-down to select Upgrade Assistant (Figure 6-2).
Figure 6-2. While the CLI tool shows the output in the terminal, the
extension in Visual Studio outputs the details in the Output window 87
"sdk": {
"version": "8.0.0",
"rollForward": "latestMajor",
"allowPrerelease": false
}
The migrated shared project is going to use the structure we had for our
shared Xamarin.Forms project, with some additions such as new
dependencies. But most noticeable is the MauiProgramExtensions.cs. This
class extends the configuration of the MauiAppBuilder by introducing a
custom extension method, UseSharedMauiApp. This method allows us to
configure our app and platform logic and is called as part of the app’s startup
pipeline. When you create a brand-new Single Project .NET MAUI project,
you’ll get a MainProgram.cs instead, which essentially does the same thing.
We’ll cover the structure of a .NET MAUI project later in this chapter when
we create our main .NET MAUI project, where we’ll move all the code. As
mentioned earlier, the migrated shared project is only temporary.
Once we have addressed the errors and made it build, we will move the
content to a new project.
But, before we do anything more, we’ll commit the work in progress with an
appropriate commit message, for example:
88
Generally, I’d say that the best place to start after running the Upgrade
Assistant is dealing with dependencies. Our inventory list is of great help,
and we can use it to identify the dependencies that need an upgrade or
replacement.
89
OceanofPDF.com
Chapter 6 exeCuting the Migration
Figure 6-3. OxyPlot has a version that supports .NET MAUI With the
replacement in place, the error list grows considerably in the Problems
window (Error window in Visual Studio). We might as well continue solving
the errors related to the OxyPlot package so the next commit can be
somewhat scoped.
mauiColor) {
return OxyColor.FromArgb((byte)(mauiColor.
Alpha * 255),
(byte)(mauiColor.
Red * 255),
(byte)(mauiColor.
Green * 255),
90
OceanofPDF.com
Chapter 6 exeCuting the Migration
(byte)(mauiColor.Blue
* 255));
I also update the namespace references for OxyPlot, on the XAML.cs file, as
well as the XAML file:
xmlns:oxyPlot="clr-namespace:OxyPlot.Maui.
Skia;assembly=OxyPlot.Maui.Skia"
Updating Namespaces
You’ve probably noticed by now that the Upgrade Assistant didn’t change
the namespaces to match the new project name. Since this is an intermediary
project, we don’t really need to update the namespaces as we’ll have to
update them again once we move the code. Renaming the namespaces is an
easy fix as both Rider and Visual Studio has quick actions for this (Figure 6-
4). If you use an IDE that doesn’t support this, then a Find and Replace can
do the trick as well.
Figure 6-4. Both Rider and Visual Studio have quick actions to do
refactoring, such as renaming namespaces
Quick actions are nice, but won’t cover all the references that need
renaming. For example, in XAML files if you’ve referenced the project
assembly, you’ll have problems as the assembly has a new name. Find this:
assembly=AppForFitness"
91
Chapter 6 exeCuting the Migration And replace with this:
assembly=AppForFitnessMaui"
We aren’t done with namespaces and references. If you take a look at the
errors in the Problems window (Error window in Visual Studio), you’ll
probably see a few with this error code: CS01104.
Being this early in the migration process, I recommend you remove the
reference to the Compatibility package where there is a direct equivalent in
.NET MAUI. By this point, we know the Grid is very different, in particular
because the rows and columns have to be explicitly added. And we know
*AndExpand is not supported, but we’ll ignore all that for now, to get to a
build.
In the files where you don’t want to remove the Compatibility package, say,
for example, StackLayout, you can add an aliased reference. Right-click the
type and select the reference, and Rider or Visual Studio will add one for
you. Do not do this for all in one go; you really want to do this one reference
at a time:
StackLayout;
92
Chapter 6 exeCuting the Migration Note that properties that are tied to a
specific compatibility control, for example, RelativeLayout.XConstraint,
will also need a reference:
<StackLayout Padding="10">
<Frame HasShadow="False">
<Frame.Effects>
<effects:ShadowEffect />
</Frame.Effects>
<StackLayout>
<compatibility:RelativeLayout Padding="10"
VerticalOptions="Start">
<customControls:IconButton x:Name="image"
Clicked="OnIconClicked"
Source="dumbells.png"
HeightRequest="70"
WidthRequest="70"
compatibility:RelativeLayout.
YConstraint="{compatibility:
ConstraintExpression
Type=Constant, Constant=10}"
compatibility:RelativeLayout.
XConstraint="{compatibility:
ConstraintExpression
Type=Constant,
Constant=10}"/>
93
Xamarin.Forms Namespace
xamarin.Forms
Controls
xamarin.Forms.DualScreen
Microsoft.Maui.Controls.Foldable
xamarin.Forms.Maps
Microsoft.Maui.Controls.Maps and
Microsoft.Maui.Maps
xamarin.Forms.platformConfiguration
Microsoft.Maui.Controls.
platformConfiguration
xamarin.Forms.platformConfiguration. Microsoft.Maui.Controls.
androidSpecific
platformConfiguration.androidSpecific
xamarin.Forms.platformConfiguration. Microsoft.Maui.Controls.
androidSpecific.appCompat
platformConfiguration.androidSpecific.
appCompat
xamarin.Forms.platformConfiguration. Microsoft.Maui.Controls.
tizenSpecific
platformConfiguration.tizenSpecific
xamarin.Forms.platformConfiguration. Microsoft.Maui.Controls.
WindowsSpecific
platformConfiguration.WindowsSpecific
xamarin.Forms.platformConfiguration. Microsoft.Maui.Controls.
ioSSpecific
platformConfiguration.ioSSpecific
xamarin.Forms.Shapes
Microsoft.Maui.Controls.Shapes
xamarin.Forms.StyleSheets
Microsoft.Maui.Controls.StyleSheets
xamarin.Forms.xaml
Microsoft.Maui.Controls.xaml
Sort out the error in this category one by one, commit, push, and try to build.
If you encounter build errors that are not related to dependencies, code logic,
references, namespaces, or alike, have a look at Appendix B.
94
OceanofPDF.com
Chapter 6 exeCuting the Migration
Creating Our Main Project
At this point in the migration, we have migrated the shared project, but it
won’t be able to deploy to neither of our target platforms. The project is a
temporary project, as we’ll create a new .NET MAUI project where we’ll
move all our files. Why? A new MAUI project will give us the default folder
structure and everything else we need to deploy to our target platforms,
which will save us time. Create a new project using the .NET MAUI
template (Figure 6-5).
Figure 6-5. Create a new .NET MAUI project The .NET project template has
a predetermined structure. Here’s a breakdown of the folders and their
significance in the .NET MAUI default template (Figure 6-6).
95
OceanofPDF.com
Chapter 6 exeCuting the Migration
Figure 6-6. .NET MAUI single project structure Dependencies
This section lists the NuGet packages and the SDK versions your project
targets.
Platforms
96
Resources
• AppIcon: This folder contains the icons for the app, and these icons are
shared across all platforms.
• Fonts: You can place any custom fonts in this folder, and they will be
available to all platforms.
MAUI will automatically handle loading the correct image size for each
platform (e.g., retina images for iOS).
• Raw: A folder where raw files (like videos or other assets) can be placed
and accessed by your app.
• Splash: Contains splash screen assets, which are used when the app is
starting.
• Styles: This folder stores styles for your app that are shared across all
platforms. In MAUI, resource dictionaries are used for things like colors,
styles, and other theme-related configurations.
97
This is the entry point for your application. It defines shared resources for
your app and initializes the app’s components. It is where global styles and
resource dictionaries can be defined.
AppShell.xaml/AppShell.xaml.cs
.NET MAUI uses the Shell concept to simplify navigation in your app. The
AppShell.xaml file contains your app’s navigation structure (e.g., tabs,
flyouts, routes), which can be customized to manage how users navigate
between pages.
MainPage.xaml/MainPage.xaml.cs
MauiProgram.cs
This file is the main configuration file for your app. It follows a similar
pattern to ASP.NET Core’s Startup.cs and is where services, configurations,
fonts, handlers, and dependencies are registered using the MauiApp.
CreateBuilder() method.
98
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-
maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPl
atform('windows'))">$(TargetFrameworks);net8.0-
windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by
following this: https://
github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-
tizen</TargetFrameworks> -->
maccatalyst-x64;maccatalyst-arm64.
<RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY
maccatalyst-arm64 indicated;
macatalyst-x64. -->
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;</
TargetFrameworks>
<OutputType>Exe</OutputType>
99
Chapter 6 exeCuting the Migration Also, remove the folders for the targets
that were removed. If on Windows, make sure the folder was in fact deleted
by double-checking in File Explorer or using the command line.
MAUI Project
Merging the two is straightforward and requires very little work. These are
the steps:
In the .NET MAUI project, delete the following files as we already have
these in the migrated shared project:
• App.XAML and cs file
We want to make sure we have the dependencies in the new project, so open
the project file for the migrated shared project, and copy the references:
<PackageReference Include="OxyPlot.Maui.Skia"
Version="1.0.1" />
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls"
Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.
<PackageReference Include="Microsoft.Extensions.
<PackageReference Include="OxyPlot.Maui.Skia"
Version="1.0.1" />
</ItemGroup>
Step 3: Drag in the files from the migrated shared project to the
.NET MAUI project. Update namespaces; clean up MainProgram.cs if
needed. Commit and push.
Test Deploying
If we try to deploy the app now, we’ll encounter an error because we haven’t
added .UseMauiCompatibility() to the MauiProgram.cs file. This method is
essential for maintaining backward compatibility with Xamarin.
Forms components that haven’t yet been updated to work with native
To fix this issue, you’ll need to modify your MauiProgram.cs file. Here’s
how you add .UseMauiCompatibility() to the builder pattern: public static
class MauiProgram
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
fonts.AddFont("OpenSans-Regular.ttf",
"OpenSansRegular");
101
Chapter 6 exeCuting the Migration fonts.AddFont("OpenSans-Semibold.ttf",
"OpenSansSemibold");
});
return builder.Build();
For example:
builder.ConfigureFonts(fonts =>
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
102
If we try to run the app again, we get another error indicating that we are
missing another chain configuration. The error itself is as follows: Handler
not found for view SkiaSharp.Views.Maui.Controls.
SKCanvasView.
We haven’t talked much about handlers, but in this particular case, it’s not
one of our own handlers missing. I took a look at the OxyPlotMaui sample
app and noticed that they register OxyPlot using chain configuration in the
builder. This code essentially replaces the init code we removed when we
updated the package to the .NET MAUI version.
builder
.UseMauiApp<App>()
.UseMauiCompatibility()
.UseSkiaSharp()
.UseOxyPlotSkia()
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf",
"OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf",
"OpenSansSemibold");
});
As you can see, even without the platform-specific code we can get
somewhat up and running.
103
Chapter 6 exeCuting the Migration You could be lucky and be able to deploy
the app to a simulator, emulator, or device without errors. It won’t look
pretty, as not only do the UI layout and controls differ, we are also missing
our platform-specific implementations. However, try to deploy on the target
platforms and assess the result before we move on to the next step, migrating
the platform-specific projects.
.NET MAUI DI
[assembly: Dependency(typeof(SecureStorageService_iOS))]
Then retrieved like this:
Get<ISecureStorageService>();
In .NET MAUI, you now leverage the built-in DI container to register and
use services.
104
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
fonts.AddFont("OpenSans-Regular.ttf",
"OpenSansRegular");
});
return builder.Build();
//TODO: Implement the real secure storage service later public class
TmpSecureStorage : ISecureStorageService
FromResult("something");
105
Once the service is registered, inject it into the constructor of any class (like
a page or view model) that needs it. Here’s how you inject it into
ProgressPage.cs:
_storageService = storageService;
InitializeComponent();
(Config.UserId);
Chapter 6 exeCuting the Migration Our EntryPage has the following code:
Instance.GetExercisesCatalogueAsync()).Exercises; var id =
exercises.First(x => x.Name == _
preSelectedExerciseName).Id;
saveEntryBtn.IsEnabled = false;
if (Navigation.NavigationStack.Any()) {
await Navigation.PopAsync();
} else {
We will talk more about Navigation in the next chapter. But even with this
fix, this won’t quite solve our issues. MainPage directly uses the
ProgressPage in its XAML view, and ProgressPage doesn’t have a default
constructor.
We don’t have this issue in App.cs as the builder creates it, and it knows to
inject the dependency. ProgressPage tabbed item is different though.
107
<TabbedPage.Children>
<views:SettingsPage Title="Settings"/>
</TabbedPage.Children>
IServiceProvider
To make MainPage work without rewriting too much of our logic, we could,
for example, inject the IServiceProvider. The IServiceProvider is a core
interface in .NET’s dependency injection (DI) system and can be used to
retrieve services from the DI container. If we register the ProgressPage in the
builder, we can do something like this:
Children.Insert(0, (Page)_serviceProvider.GetService
(typeof(ProgressPage)));
108
Inside it, we can check if the Handler for the class is available, with its
MauiContext, and if so access the DI container and retrieve an instance of
ISecureStorageService.
This approach is used to safely access services in MAUI when you don’t
have direct access to the dependency injection container through constructor
injection. It’s a way to bridge the gap between MAUI’s UI lifecycle and the
application’s service container; however, as mentioned earlier, while this
method works, it’s generally preferable to use constructor injection when
possible, as it makes dependencies more explicit and easier to manage and
test, but then the rest of the class has to follow suit.
With the constructor injection removed, I set the storage service like this
instead:
base.OnHandlerChanged();
_storageService = Handler.MauiContext?.Services.GetServ
ice<ISecureStorageService>();
109
Chapter 6 exeCuting the Migration While this works, and our app builds, we
will get back to this when we discuss navigation in the next chapter. But for
now, let’s commit and push, happy that we have a working app (albeit
without platform-specific code).
Summary
In this chapter, we took our first big steps in migrating the AppForFitness
solution from Xamarin.Forms to .NET MAUI. We started with important
cleanup tasks, including removing unused dependencies like Newtonsoft and
SkiaSharp, simplifying the codebase in preparation for migration.
The focus was on upgrading the shared project, creating a new .NET
MAUI project and moving the files. As we have dealt with errors and
configurations, we’ve learned more about some of the default design
patterns in .NET MAUI, and we will cover more of that as we migrate.
In the next chapter, we will dive deeper into platform-specific migration and
UI differences and more.
110
CHAPTER 7
Migrating Platform
Projects
With the shared project successfully migrated, it’s time to focus on the
platform-specific projects (iOS and Android). While the app may now build,
it’s still far from fully functional. For many projects, particularly more
complex ones, the app may not even build at this stage. You may encounter
issues because some platform-specific code hasn’t yet been migrated. For
instance, we created a temporary dummy implementation for our storage
service earlier, but if your app contains many such services, it could become
a tedious and complex task.
111
https://doi.org/10.1007/979-8-8688-1215-6_7
Chapter 7 Migrating platforM projeCts Ultimately, the best approach is to
migrate platform-specific code as early as possible in the migration process.
Let’s start by moving the platform-specific code for iOS.
The first step is to copy over the platform code from the original Xamarin.
Forms iOS project to the Platforms/iOS folder in your new .NET MAUI
project. Be selective here—exclude files like Main.cs and AppDelegate.cs,
as these will be handled differently in .NET MAUI.
In the AppForFitness project, for example, the Main.cs file is primarily used
to register OxyPlot, which has already been handled with a handler
extension in our .NET MAUI code. If your iOS platform project contains
more complex logic, you’ll need to move it manually, placing initialization
logic in either Program.cs (inside the Platforms/iOS folder) or the
MauiProgram.cs file.
AppDelegate
using AppForFitnessCore.Services;
using Foundation;
using UIKit;
112
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
MauiProgram.CreateMauiApp();
DeepLinkingService.HandleDeepLink(uri);
return true;
This code ensures that the app can still handle deep linking on iOS as it did
in Xamarin.Forms. We’ll deal with navigation and deep linking further in the
navigation section of this book.
After moving the files to the Platforms/iOS folder, you should rename any
namespaces to reflect their new location. Following that, here are some
additional cleanup tasks you’ll want to do for the AppForFitness project: 1.
Remove
113
Find: public\s+class\s+(\w+)Renderer
114
OceanofPDF.com
Chapter 7 Migrating platforM projeCts
Figure 7-1. Regex search and replace in Rider is powerful. If you want more
control over the results, try narrowing down the search to a specific
directory
115
In .NET MAUI, the Handler pattern replaces the older renderer pattern from
Xamarin.Forms. This shift is one of the most significant changes in the way
platform-specific customization is handled in .NET
• Performance: Handlers are generally faster than renderers, as they are more
efficient in how they handle platform-specific elements.
116
/// <summary>
/// </summary>
base.OnElementChanged(e);
if (Control == null)
return;
if (e.NewElement is IconButton)
SetDropShadow();
117
base.ConnectHandler(platformView);
SetDropShadow(platformView);
imageView.Layer.ShadowRadius = 2.0f;
imageView.ClipsToBounds = false;
base.DisconnectHandler(platformView);
118
OceanofPDF.com
Chapter 7 Migrating platforM projeCts
Figure 7-2. The suggestions in the drop-down indicate there is an
ImageButtonHandler, which means there is an ImageButton we could use
instead, but we’ll take a look at that later in the book Registering the
Handler
Like we did with the OxyPlot handler, which was added as an extension
method, we’ll register the handlers in the builder. However, to avoid
cluttering the entry point of the application with conditional compilation,
we’ll instead use a delegate so we can register platform handlers in the
platform-specific entry points.
builder
.UseMauiApp<App>()
.UseMauiCompatibility()
.UseSkiaSharp()
.UseOxyPlotSkia()
.ConfigureMauiHandlers(handlerDelegate)
119
CreateMauiApp(handlerCollection =>
handlerCollection
.AddHandler(typeof(Image), typeof(IconButtonHandler));
});
CreateMauiApp(handlerCollection =>
});
Handler Lifecycle
In .NET MAUI, the handler lifecycle revolves around various events and
methods that manage the creation, connection, and disconnection of
platform-specific native controls with the cross-platform UI elements.
• ConnectHandler
120
• This is where you apply the initial setup, bindings, and any customization
needed for the platform
control.
• DisconnectHandler
disconnected from the platform view (i.e., when the view is about to be
destroyed or recycled).
• You can use this method to clean up any resources or remove event
listeners to avoid memory leaks.
We have two more renderers to deal with: the TabbedPageRenderer and the
LongPressViewRenderer.
We’ll keep this section short and summarize the migration, but you can look
at the code for more details.
TabbedPageRenderer
The TabbedPageRenderer.cs was used to force iOS to use custom styling for
the tabs. Since we aren’t sure if we need this anymore, we’ll just comment
out the handler instead of migrating it and leave a //TODO
LongPressViewRenderer
We migrated this renderer to a handler that extends the ContentViewHandler.
Initially, I used the ViewHandler by mistake.
121
Before we dive into more handler examples, it’s worth noting that handlers
aren’t always needed. In some cases, it’s better to modify or extend the
behavior of existing controls using mappers.
• Mapper: The mapper is where you can append to or modify the behavior of
the handler. Instead of creating a custom handler from scratch, you can use a
mapper to add custom behavior to the existing control.
Example: IconButtonMapper
To demonstrate, let’s use the IconButton, a control we’ve referred to earlier,
where shadows were added via a custom handler. Using mappers, we can
achieve the same functionality with less code.
122
ImageHandler.Mapper.AppendToMapping(nameof(IconButton), (handler,
view) =>
if (view is IconButton)
handler.PlatformView.Layer.ShadowOffset = new
CoreGraphics.CGSize(0, 3);
handler.PlatformView.Layer.ShadowOpacity
= 0.25f;
handler.PlatformView.Layer.ShadowRadius = 2.0f;
handler.PlatformView.ClipsToBounds = false;
}
});
CreateMauiApp(handlerCollection =>
123
.AddHandler(typeof(LongPressView),
typeof(LongPressViewHandler));
});
platform.
ImageHandler.Mapper.AppendToMapping(nameof(IconButton), (handler,
view) =>
});
ReplaceMapping
This method is used to replace an existing mapping entirely. This allows you
to redefine how a property or behavior is handled for a specific platform or
control:
124
});
ModifyMapping
});
RemoveMapping
If you want to remove a specific mapping, this method allows you to do so.
ImageHandler.Mapper.RemoveMapping(nameof(IconButton));
Preprocessor Directives
125
ImageHandler.Mapper.AppendToMapping(nameof(IconButton), (handler,
view) =>
{
#if IOS
if (view is IconButton)
handler.PlatformView.Layer.ShadowOffset = new
CGSize(0, 3);
handler.PlatformView.Layer.ShadowOpacity
= 0.25f;
handler.PlatformView.Layer.ShadowRadius = 2.0f;
handler.PlatformView.ClipsToBounds = false;
#endif
#if ANDROID
#endif
});
Preprocessor directives are instructions in our code that are processed before
the actual compilation of the code begins and start with the
126
• #else: Defines code that should run when none of the other conditions are
met
Effects
127
/// </summary>
/// <summary>
cref="ShadowEffect"/> class.
/// </summary>
public ShadowEffect()
: base($"AppForFitness.{nameof(ShadowEffect)}")
Before:
assembly: ResolutionGroupName("AppForFitness")]
[assembly: ExportEffect(typeof(AppForFitness.iOS.Effects.
ShadowEffect), nameof(ShadowEffect))]
namespace AppForFitness.iOS.Effects
128
if (view != null)
view.Layer.ShadowColor = UIColor.Black.
CGColor;
CGSize(-2, 2);
view.Layer.ShadowOpacity = 0.4f;
view.Layer.ShadowRadius = 4;
}
}
System.Diagnostics.Debug.WriteLine($"Cannot
Message}");
Controls.Platform;. Chances are the Upgrade Assistant has already done this.
129
using Microsoft.Maui.Controls.Platform;
#if IOS
using UIKit;
#elif ANDROID
#endif
namespace AppForFitnessCore.Effects
/// <summary>
/// </summary>
public ShadowEffect()
#if ANDROID
internal class PlatformShadowEffect : PlatformEffect
130
#elif IOS
try
System.Diagnostics.Debug.
WriteLine("ShadowEffect2");
view.Layer.ShadowColor = UIColor.Black.
CGColor;
CGSize(-2, 2);
view.Layer.ShadowOpacity = 0.4f;
view.Layer.ShadowRadius = 4;
131
System.Diagnostics.Debug.WriteLine($"Cannot
{ex.Message}");
}
protected override void OnDetached()
#endif
...
.ConfigureMauiHandlers(handlerDelegate)
.ConfigureFonts(fonts =>
fonts.AddFont("OpenSans-Regular.ttf",
"OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf",
"OpenSansSemibold");
});
builder.ConfigureEffects(effects =>
effects.Add<ShadowEffect, PlatformShadowEffect>();
});
...
132
Common Features
When transitioning from Xamarin to .NET MAUI, you’ll find that many
things remain familiar. Microsoft has aimed to keep features and patterns as
consistent as possible, with only minor changes such as namespace updates.
For instance, features like Xamarin.Essentials, which provided access to
native device capabilities, are now built directly into .NET MAUI.
#if DEBUG
// Use Preferences in Debug mode since Keychain Sharing doesn't work with
a free Apple ID.
Preferences.Set(key, value);
return Task.CompletedTask;
#else
In the next chapter, we’ll explore more significant changes as we dive deeper
into UI migration. But before that, let’s apply the same steps we used for iOS
to migrate the Android platform-specific code.
133
OceanofPDF.com
Chapter 7 Migrating platforM projeCts
Migrating the Android Code
Start by copying over the pure code folders and files from your
Xamarin.Android project to the .NET MAUI Android platform folder, as
well as the Resources folder (see Figure 7-3).
Figure 7-3. Some of the folders and files we are copying over Next, move
the Assets folder. We’ll deal with the specifics of assets and resources in the
following chapter, but for now, place them in the Android platform folder.
• Move ShadowEffect: Combine the shadow effect code into the unified
implementation for Android.
IconButton.
134
Summary
We also delved into key changes in .NET MAUI, such as how the Handler
pattern has replaced the renderer pattern from Xamarin.Forms.
135
CHAPTER 8
Migrating the UI
and Navigation
137
https://doi.org/10.1007/979-8-8688-1215-6_8
Navigation.PushAsync(new EntryPage(button.Text));
We certainly don’t want to mix the two, and using GoToAsync with Shell is
our best approach. Shell has PushAsync, which uses stack-based navigation.
However, GoToAsync is more flexible. When you navigate using
GoToAsync(), it still maintains a navigation stack under the hood works on
top of the navigation stack, but with added flexibility through route-based
navigation.
AppForFitness Navigation
This will allow us to manage navigation and tabs more declaratively and
make future extensions easier.
139
<Shell
x:Class="AppForFitnessCore.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:AppForFitnessCore.Views"
Title="App For Fitness">
<Shell.ToolbarItems>
</ToolbarItem>
Clicked="OnEmailClicked" >
</ToolbarItem>
Clicked="OnLanguageSettingsClicked" >
</ToolbarItem>
</Shell.ToolbarItems>
<TabBar>
<ShellContent
Title="Progress"
ContentTemplate="{DataTemplate
views:ProgressPage}"/>
<ShellContent
Title="New Entry"
ContentTemplate="{DataTemplate views:EntryPage}"/>
<ShellContent
Title="Settings"
140
views:SettingsPage}"/>
</TabBar>
</Shell>
The code-behind file looks the same as MainPage.cs, but I suspect that our
pop-up doesn’t need the current complicated logic that it has. Due to
TabbedPage limitations with adding a control on top of it, we had to create a
grid, insert the content, and overlay the pop-up. We’ll get back to this as
soon as we’re done with the navigation. The AppShell is then added to the
App.cs constructor.
Before:
Now:
With that in place, we can remove MainPage, test the application, commit,
and push.
How does this work? The ShellContent elements within the TabBar
automatically create implicit routes based on the titles of the content.
However, if you pass parameters, then you have to explicitly register the
route. EntryPage has two constructors, and one of them has a parameter.
Chapter 8 Migrating the Ui and navigation Note While the second option
makes sure the routes are updated if the cage name changes, it does create a
dependency whenever we navigate to that page. declaring constants using
nameof will also create transitive dependencies. however, the best way to
deal with this is outside the scope of this book, so for now we’ll use
nameof().
Let’s change all the navigation code to URI-based navigation. For example,
ProgressPage.cs will navigate like this: private async void
OnEntryButtonClicked(object sender, EventArgs e)
“//” is used to reset the navigation stack entirely and navigate to the new
route as the "root" of the stack. This is useful when you want to navigate to a
page without keeping a history of the previous navigations.
[QueryProperty(nameof(ExerciseName), Config.RouteParams.
exerciseName)]
public partial class EntryPage
142
set
if (_exerciseName != value)
_exerciseName = Uri.UnescapeDataString(value);
ExercisePicker.SelectedIndex = _exercises.
}
This frees up the constructor from query parameters and leaves space
(metaphorically speaking) to inject dependencies instead. I’ve added
UnescapeString since we’ll be passing in the parameter through external
query parameters.
_apiService = apiService;
InitializeComponent();
143
builder.Services.AddTransient<FitnessApiService>();
builder.Services.AddTransient<EntryPage>(); If we want to clean this up
even more, we can define our magic strings in the config file:
/// <summary>
/// Contains route parameter constants used for deep linking and navigation
within the application.
/// </summary>
/// <summary>
/// </summary>
await Shell.Current.GoToAsync($"///{nameof(EntryPage)}?{Config.
When we sorted out our navigation, we even sorted out our deep link
navigation without much work. Unfortunately, I ran into some problems with
my deep linking, and although in theory you should be able to just handle
them in App.xaml.cs in the OnAppLinkRequestReceived method, I was
redirected to the app but not the specific page, and the method was never
called. I solved this by catching the routing in the platform-specific entry
points (you can see the solution in Appendix B), but I’m hoping this issue is
resolved by the time the book comes out.
144
.UseMauiApp<App>()
.UseMauiCompatibility()
.UseSkiaSharp()
.UseOxyPlotSkia()
.UseMauiCommunityToolkit()
Color = Colors.White
};
...
145
Chapter 8 Migrating the Ui and navigation The Popup class handles all the
logic, similar to our previous PopupLayout implementation which I removed
after adding the Community Toolkit.
TitleView code:
<Shell.TitleView>
<HorizontalStackLayout HorizontalOptions="End">
Clicked="OnEmailClicked"/>
Clicked="OnLanguageSettingsClicked"/>
</HorizontalStackLayout>
</Shell.TitleView>
Layouts
recommended that you use the Border control for more uniform padding and
behavior across platforms.
147
Chapter 8 Migrating the Ui and navigation
• RelativeLayout: Its usage is discouraged, and it’s only available via the
compatibility package. Use Grid whenever possible.
Infinite Expansion
ScrollView
For some views, such as ScrollView, this can be problematic. Since it can
grow indefinitely, the ScrollView may expand to fit all content, resulting in
different behavior from your Xamarin.Forms app. You’ll need to add
constraints or adjust child elements if this is a problem. There are also a few
issues that at the time of writing haven’t been solved yet, for example,
shadow property not being applied to content inside the ScrollView. See the
Appendix B for examples.
148
Chapter 8 Migrating the Ui and navigation
*AndExpand
The layout engine has been redone, so we’d have more predictable layout
behavior in .NET MAUI in terms of how child elements are arranged and
sized. And while this is helpful when dealing with complex layouts, it also
means we need to rethink our layouts and adjust post-migration.
UI Advice
Many of the problems my team encountered with layouts and controls were
related to mixing .NET MAUI and .NET MAUI Compatibility pack.
Given the differences between Xamarin.Forms and .NET MAUI, here are a
few migration tips to get you started:
• Explicitly declare Grid rows and columns: Update any grids to explicitly
declare RowDefinitions and ColumnDefinitions, as the auto-inferred layout
An extra note on StackLayouts: When you nest these, the more layout
calculations will be performed. This can and probably will impact
performance. Use a Grid instead of trying to recreate one using other layout
controls (or views).
We’ll use that list to address the layouts in AppForFitness. Here are some
changes made to AppForFitness.
I used Find Usages for StackLayout (Figures 8-1 and 8-2) and
150
Chapter 8 Migrating the Ui and navigation
Figure 8-1. Find Usages is a handy feature in many IDEs and lets us find
usages of a type or member
.NET MAUI has a shadow property for the controls (and views), which
means unless we want something very specific, or specific behavior for a
platform, we can remove the custom effects.
MAUI. You can still have platform-specific images, but you’ll find that the
majority of resources can be shared and therefor placed in one location,
Resources/Images. Use the inventory we did earlier to identify images that
are identical visually and that don’t need platform-specific versions. Let’s
take a look at the changes made in AppForFitness.
Images
Our background image, bg.png, was in the shared project and was migrated
to the Resources/Images folder in the .NET MAUI project, but the dumbbell
image is still duplicated, once for each platform. I’ve now moved it to the
Resources/Images.
152
Chapter 8 Migrating the Ui and navigation Note Keep in mind that Svg files
are converted to pngs, so use the .png extension when referencing them in
your app. additionally, image names should be lowercase containing only
alphanumeric characters or underscores and start and end with a letter
character.
When you add images to a project, you can do so by dragging and dropping
or by adding an item. If you add an image to the Resource/
Images folder, the build action should be set to MauiImage; however, I’ve
noticed that the Upgrade Assistant doesn’t always do that, so you want to
double-check by taking a look at the image properties (right-click ➤
Properties).
MauiImage BaseSize
.NET MAUI lets you set a base size for an image in the project file by
setting the BaseSize and Resize attribute. The BaseSize attribute helps us
define a logical size that the image will use as a basis for resizing. This can
be useful when working with, for example, a background image or
thumbnail image that needs to scale properly across different screen sizes
and aspect ratios.
The BaseSize defines the "logical" dimensions that the image is going to be
treated as before it is resized based on the actual screen size. The values in
BaseSize should correspond to the aspect ratio of your original image to
avoid distortion.
In our case, if we want to set a new base size to the background image,
which is 1242 × 2688, and we want to maintain its aspect ratio across
various devices, the BaseSize should be something close to this, but scaled
down to a logical size that works well for the screen resolution and
performance.
153
BaseSize="414,896"/>
However, this won’t sort out our background image issue in AppForFitness,
which shows a distorted image. Just as with Xamarin.
• Use a control to wrap content and set background image on that control
• Dynamically but manually create or use images with size matching the
device
You can see an example for the first option in the AppForFitness repository,
and I’ve left one page as before, but with a new base size.
Icons
Continuing on the topic of image management, managing icon assets is also
a whole lot easier now. For example, you no longer need to manually handle
Assets.xcassets the same way you would in a Xamarin.
For example, it will create the appropriate image sizes for Android (hdpi,
xhdpi, xxhdpi, etc.) and iOS (@1x, @2x, @3x).
154
1. Add the icon: Place your image in the Resources\ AppIcon folder (or
similar). Supported formats
2. Declare in project file: Add the icon in the .csproj file using
<ItemGroup>
<MauiIcon Include="Resources\AppIcon/
appicon.svg" />
</ItemGroup>
Condition="$([MSBuild]::GetTargetPlatformIdentifier
('$(TargetFramework)')) == 'windows'"
3. Composed icons: To use separate background and foreground images,
specify the ForegroundFile:
<MauiIcon Include="Resources/AppIcon/appiconbg.png"
<MauiIcon Include="Resources/AppIcon/appicon.png"
BaseSize="128,128" />
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string> 155
<application
android:allowBackup="true"
android:icon="@mipmap/appicon"
android:roundIcon="@mipmap/appicon_round"
Clean the solution, and uninstall the app, before building and deploying.
Splash Screen
Steps:
<key>UILaunchStoryboardName</key>
<string>MauiSplash</string>
= ConfigChanges.ScreenSize | ConfigChanges.
Orientation | ConfigChanges.UiMode |
ConfigChanges.ScreenLayout | ConfigChanges.
SmallestScreenSize)]
156
something like:
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<EnableCodeSigning>true</EnableCodeSigning>
<CodesignProvisioningProfile>iOS Development
Profile</CodesignProvisioningProfile>
<CodesignRequireProvisioningProfile>true</
CodesignRequireProvisioningProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='
Release'">
<EnableCodeSigning>true</EnableCodeSigning>
<CodesignProvisioningProfile>iOS Distribution
Profile</CodesignProvisioningProfile>
<CodesignRequireProvisioningProfile>true</
CodesignRequireProvisioningProfile>
<CodesignTeamIdentifier>YOUR_TEAM_ID</
CodesignTeamIdentifier>
</PropertyGroup>
After moving images and icons and creating the splash screen, build and run
the app. If everything works and looks as expected, go ahead and remove the
folder and files we moved over from the old platform projects that are no
longer needed. For example, I’ve deleted my drawable folders for Android
and the assets folder for iOS.
157
Styling is what adds that extra oomph to an app, and thankfully .NET
MAUI has continued to build on the flexible and powerful styling options in
Xamarin.Forms. Even more so now that we can centralize styling yet cater
for platform differences with OnPlatform in XAML or conditional
compilation.
<Style TargetType="Label">
<Setter Property="BackgroundColor">
<Setter.Value>
<OnPlatform x:TypeArguments="Color">
</OnPlatform>
</Setter.Value>
</Setter>
</Style>
Likewise, CSS styling has improved. CSS in .NET MAUI works seamlessly
(mostly) with new controls and views introduced in the framework, unlike
Xamarin.Forms where some controls had limited CSS
support.
CSS styling is added by adding a stylesheet to the Styles folder, making sure
build action is set to MauiCSS, and referencing it where you want to use it.
Fully styling a .NET MAUI app using CSS alone is not possible, but you can
combine CSS and XAML.
CSS example:
.titleViewButton{
background-color: transparent;
color: #3b9651;
158
Chapter 8 Migrating the Ui and navigation
<ContentPage.Resources>
</ContentPage.Resources>
Specifying class:
<Shell.TitleView>
<HorizontalStackLayout HorizontalOptions="End"
HeightRequest="50">
Clicked="OnEmailClicked"/>
Clicked="OnLanguageSettingsClicked"/>
</HorizontalStackLayout>
</Shell.TitleView>
.NET MAUI has also improved styling options for controls and views, for
example, you can now easily set TabbedPage colors using properties directly
in XAML or C# without needing a platform-specific handler or renderer like
we had in the original implementation.
For example:
<TabbedPage BarBackgroundColor="LightGray"
BarTextColor="Black"
SelectedTabColor="Blue"
UnselectedTabColor="Gray">
</TabbedPage>
159
It’s frustrating, but at some point, we will run into problems with styling
MAUI Views and Controls. For example, at the time of writing, while I can
style some aspects of the Shell and ShellContent, some properties do not
exist and can’t be styled. Nor is there a handler we can use.
I’ve set the same style for the background and foreground colors on the tab
items, but changing font size was not available out of the box. The
reorganization of the Shell and NavigationPage components into Handlers is
not yet complete. Our option at this point would be to reintroduce the
compatibility pack and use the ShellRenderer or create our own Shell
navigation.
Note if your styling isn’t showing, make sure to check the Styles.
xaml file if you have one. the default for .net MaUi declares a lot of styles.
do a search for the property in your project; you might be surprised to find
several declarations sprinkled throughout. generally, it’s better to stick to
styling declarations in resource dictionaries than incline styling. if you use
an ai assistant with your ide, for example, rider ai assistant, github Copilot,
or augment plugin, you can ask the ai assistant to locate incline styling and
help refactor to centralize styling.
160
Chapter 8 Migrating the Ui and navigation Shadows
I’ve spent a lot of time on shadow in Xamarin.Forms, and I’m very happy
about the improvements in .NET MAUI. As mentioned earlier, there is now
a shadow property on views and controls.
In .NET MAUI, shadows are applied to the outline or boundary of the child
elements, not the parent layout, for example, a VerticalStackLayout, which
we used before wrapping it in a Border control. Therefore, even though you
are applying a shadow to the VerticalStackLayout, the shadow will be drawn
around the layout’s content and not the layout frame itself.
The Shadow is applied to the content inside the layout, but not the entire
layout’s boundary as the layout itself doesn’t have a "visual presence" (frame
or border) to cast a shadow. Only the child elements inside the layout do.
Summary
We also dealt with styling as some was lost during migration, .NET
MAUI’s unified styling has greatly reduced the need for platform-specific
customization, and we’ve used that to our advantage while also discussing
existing issues and controls that lack styling options.
161
Chapter 8 Migrating the Ui and navigation We also updated layouts,
switching from Frame to Border and replacing StackLayout with more
efficient alternatives like VerticalStackLayout and HorizontalLayout and
replacing RelativeLayout with Grid. Image and asset management has been
unified, and we addressed splash screen and app icon improvements. We are
almost done, but first let’s do some cleaning up and focus on removing
obsolete code, improving performance and memory usage and accessibility.
162
CHAPTER 9
and Optimization
We’ve successfully migrated the app, but there’s one more crucial step:
cleaning up, optimizing, and testing. When it comes to mobile apps,
performant and robust apps are everything. Users expect apps to be snappy,
responsive, quick to load, and mostly bug-free. If those expectations aren’t
met, they’re likely to leave quickly, possibly with a negative review.
In this chapter, I’ll dive into ways to ensure your .NET MAUI app doesn’t
just work but works fast and efficiently while future-proofing by removing
obsolete code and adding tests.
As we’ve discussed earlier in the book, there are quite a few namespaces,
types, and members that are now obsolete in .NET MAUI. So far, we’ve
focused on areas like the *ExpandAll layout properties, but it’s important to
clean up any other outdated code as well.
If you’re using Rider for development, you’ll notice that it doesn’t have a
group by type option in the Problems window, which could make it tricky to
identify all obsolete code in one go. However, there’s a quick workaround:
you can search for the following problem codes in your Solution Files
(Figure 9-1):
© Iris Classon 2025
163
https://doi.org/10.1007/979-8-8688-1215-6_9
Additionally, it’s a good idea to double-check for any missed obsolete code
by searching for the word “obsolete” across your project. This ensures
nothing gets left behind during cleanup and helps prevent future issues with
outdated functionality.
Figure 9-1. You can search for the obsolete error code to locate warnings
for obsolete types or members
164
The first obsolete type we are going to address is MessageCenter. This is the
warning description, which even includes recommended replacement:
WeakReferenceMessenger`’.
base.OnAppearing();
WeakReferenceMessenger.Default.Register
<LanguageChangedMessage>(this, OnLanguageChanged);
// Code omitted
}
LanguageChangedMessage message)
165
ToString());
base.OnDisappearing();
WeakReferenceMessenger.Default.Unregister
<LanguageChangedMessage>(this);
EventArgs e)
{
// Code omitted
WeakReferenceMessenger.Default.Send
(new LanguageChangedMessage
(selectedLanguage));
Run the app; confirm the changes work as intended. Commit the changes to
source control.
166
Our next two warnings are related to threading and the Device object.
try
{
// Use Preferences in Debug mode for iOS. Keychain Sharing does not work
with a free Apple ID.
Preferences.Set(key, value);
else
// Code omitted
167
plotModel);
with this:
Dispatcher.Dispatch(() => plotView.Model = _plotModel); Using
Dispatcher.Dispatch is optimal for classes that have access to a Dispatcher
(like pages, views, and other classes that inherit from BindableObject), as it
provides synchronous, main-thread dispatching for quick, direct UI updates.
However, in contexts where a Dispatcher might not be available—such as
view models or services—you can use
MainThread.InvokeOnMainThreadAsync from Microsoft.Maui.Essentials:
await MainThread.InvokeOnMainThreadAsync(() => DoStuff());
MainThread.InvokeOnMainThreadAsync is an asynchronous alternative that
automatically checks if the code is already on the main thread and only
dispatches if necessary, making it useful in non-UI-bound contexts. You can
use the MainThread.IsMainThread property if you want to explicitly check if
you are on the main thread.
For example:
168
MAUI 8 and 9:
• TargetIdiom
• FocusRequest
• MauiImageView
• RootPanel
• MauiTimePicker.DateSelected
• Entry.ControlsEntryMapper
• Layout.Mapper
• Picker.Mapper
• WebView.Mapper
• IFontNamedSizeService
• AcceleratorTypeConverter
• PopupManager
• ControlsSwipeMapper
• ControlsToolbarMapper
• SearchBar.MapIsSpellCheckEnabled
• ControlsFlyoutPageMapper
• ControlsScrollViewMapper
• UiContainerView
• GradientShader
169
• LinearGradientShader
• ClickGestureRecognizer.cs
• TemplateBinding
• MessagingCenter.cs
• *AndExpand
• AutomationProperties.*
After cleaning up the obsolete types and members, I decided to take it a step
further and perform some general cleanup to remove leftover warnings and
improve the overall clarity of the project. One of the best practices I use
regularly is the quick action feature that I mentioned earlier in the book. This
tool is incredibly helpful for tasks like removing unused namespaces,
renaming variables for clarity, and refactoring code.
It’s tempting to ignore warnings, especially when they don’t seem critical.
However, these warnings have a habit of multiplying over time, leading to a
cluttered solution that’s hard to manage. By addressing them immediately,
you prevent potential issues from snowballing. Cleaning them up as they
arise will keep the project organized and reduce technical debt down the
road.
Our team has found that using a linter or configuring the project to treat
warnings as errors helps to keep the codebase clean and warning-free. This
practice encourages everyone to pay attention to the details and not just the
high-level architecture. It also fosters a culture of continuous improvement,
where small optimizations lead to a more maintainable and professional
project.
170
Up until now, we’ve largely overlooked the accessibility features in our app,
particularly on the SettingsPage where we declared automation properties.
In .NET MAUI, several of the old automation properties are now obsolete,
including
• AutomationProperties.Name
• AutomationProperties.HelpText
• AutomationProperties.LabeledBy
Depending on the IDE you use, you might not receive warnings for these
obsolete properties, so it’s important to be proactive in replacing them with
their modern equivalents.
171
For example, a Label next to a Picker can explain the purpose of the Picker
by linking the two elements through accessibility properties. This provides
extra clarity for users who rely on screen readers to understand the context
of different UI elements.
Additional Considerations
• Don’t set the Description on Labels: The screen reader will automatically
read the text of a label.
172
• On iOS: If you set the Description property on a control that has child
elements, it may prevent the screen reader from accessing the child
elements. This can lead to incomplete information being presented to users
relying on assistive technologies.
SettingsPage with SemanticProperties:
FontSize="Large"
TextColor="{StaticResource AccentColor}"
SemanticProperties.HeadingLevel="Level1" />
TextColor="{StaticResource PrimaryColor3}"
SemanticProperties.HeadingLevel="Level1"/>
x:Name="RepMaxFormulasPicker"
TitleColor="{StaticResource PrimaryColor2}"
BackgroundColor="{StaticResource BackgroundColor2}"
SemanticProperties.Description="{Binding
SemanticProperties.HeadingLevel="Level1"
SelectedIndexChanged="OnFormulasSelectedIndexChanged"/> 173
Chapter 9 Cleaning Up, testing, and OptimizatiOn AppTheme: Dynamic
Theme Switching in
.NET MAUI
In modern apps, users expect the ability to toggle between different themes,
such as light mode, dark mode, or even accessibility-friendly themes. .NET
MAUI has enhanced its support for dynamic theme switching, enabling us to
build applications that adapt visually based on the user’s preferences.
To enable dynamic theme switching in your .NET MAUI app, you can
follow these steps:
<ResourceDictionary xmlns="http://schemas.microsoft.
com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.
com/winfx/2009/xaml"
x:Class="AppForFitnessCore.
Resources.Themes.ProtanopiaStyle">
<Color x:Key="LabelTextColor">#006400</Color>
<Color x:Key="LabelBackgroundColor">#FFB6C1</Color>
<!-- Light Pink -->
</ResourceDictionary>
174
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/
Themes/DefaultStyle.xaml" />
the styles:
TextColor="{DynamicResource LabelTextColor}"
FontSize="Large"
BackgroundColor="{DynamicResource
LabelBackgroundColor}"
deficiencies."
SemanticProperties.HeadingLevel="Level1"/>
<Picker x:Name="ColorThemePicker" Grid.Row="4"
Title="Color theme"
TitleColor="{DynamicResource LabelTextColor}"
BackgroundColor="{DynamicResource
LabelBackgroundColor}"
SemanticProperties.Description="{Binding
SemanticProperties.HeadingLevel="Level1"
SelectedIndexChanged="OnThemeChanged"/> 175
Chapter 9 Cleaning Up, testing, and OptimizatiOn 4. Load theme when user
changes theme by removing and adding:
EventArgs e)
ToString();
if (string.IsNullOrEmpty(selectedTheme)
MergedDictionaries: { } mergedDictionaries
}) return;
mergedDictionaries.Clear();
switch (selectedTheme)
case "Protanopia":
mergedDictionaries.Add(new
ProtanopiaStyle());
break;
case "Tritanopia":
mergedDictionaries.Add(new
TritanopiaStyle());
break;
case "None":
mergedDictionaries.Add(new DefaultStyle());
break;
176
mergedDictionaries.Add(new DefaultStyle());
break;
}
In earlier versions of the app, we used the StaticResource markup extension
because the app didn’t need to change themes dynamically at runtime.
However, when we want to enable theme switching, we need to use
DynamicResource instead. The DynamicResource markup extension allows
resources to be updated at runtime, which is essential if we anticipate
changing themes while the app is running.
177
Chapter 9 Cleaning Up, testing, and OptimizatiOn
.NET MAUI also supports automatic theme switching based on the system’s
light or dark mode with AppThemeBinding, allowing us to adjust UI
elements according to the system’s theme settings. I’ve added a small
example to the SettingsPage, which consists of XAML only and no extra
code-behind. It really is that easy.
178
<Label Grid.Row="5"
FontSize="Large"/>
MAUI are extensive, and the official documentation provides many more
examples.
Testing
AppForFitness currently doesn’t have any tests, but we’re about to change
that. I’ll walk you through the process of creating tests, with a particular
focus on unit testing. When integrated into your development workflow, unit
tests have a significant impact on improving code quality. They also serve as
both design documentation and functional specifications, clearly defining
how your app should behave.
Writing unit tests for platform-specific code is not common or practical for
all solutions, because unit tests are meant to isolate and test logic, not
platform-specific behaviors like UI rendering or OS-specific APIs (e.g.,
camera, GPS, or file system interactions). Platform-specific code is often
tied directly to hardware or OS features, which makes unit testing less
meaningful since those features typically need to be tested in their respective
environments which can be done with UI tests, explorative testing, and other
types of tests.
Unit tests are designed to test business logic in isolation without
dependencies on the environment. For platform-specific code (such as
checking DeviceInfo.Platform in .NET MAUI), the focus should be on
abstracting this code into interfaces or services, which you can mock in tests.
179
MAUI project, you might get a message that it’s not compatible (Figure 9-3).
Figure 9-3. You might need to add conditions to your tags in the csproj file
when adding a reference to a .NET MAUI project 2. Update the .NET MAUI
project to target .NET 8 as well, and add a condition to OutputType to only
180
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios; net8.0</TargetFrameworks>
<OutputType Condition="'$(TargetFramework)'
!= 'net8.0'">Exe</OutputType>
3. Make sure the class you are testing has abstracted dependencies that need
to be mocked. For example, testing our SecureStorageService requires us to
use the abstractions for Preferences, SecureStorage,
Figure 9-4. Extract Interface is a nice feature that can help us quickly add a
layer of abstraction
181
IDeviceInfo deviceInfo,
IDebugger debugger,
IPreferences preferences,
try
preferences.Set(key, value);
else
Console.WriteLine(ex.ToString());
182
}
public class DebuggerWrapper : IDebugger
Dependency registrations:
builder.Services.AddSingleton(DeviceInfo.Current);
builder.Services.AddSingleton<IDebugger, DebuggerWrapper>();
builder.Services.AddSingleton(SecureStorage.Default);
builder.Services.AddSingleton(Preferences.Default);
builder.Services.AddTransient<ISecureStorageService,
SecureStorageService>();
4. Write tests:
= new();
= new();
_mockDeviceInfo.Object,
_mockDebugger.Object,
183
Chapter 9 Cleaning Up, testing, and OptimizatiOn
_mockPreferences.Object,
_mockSecureStorage.Object);
Android,
};
[Theory]
[InlineData(nameof(DevicePlatform.iOS), true)]
[InlineData(nameof(DevicePlatform.iOS), false)]
[InlineData(nameof(DevicePlatform.Android), true)]
[InlineData(nameof(DevicePlatform.Android), false)]
// Arrange
var platform = GetDevicePlatform(platformName);
Returns(platform);
Returns(isDebuggerAttached);
// Act
// Assert
isDebuggerAttached)
_mockSecureStorage.Verify(s =>
else
{
_mockSecureStorage.Verify(s =>
IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>()), Times.Never);
// Code omitted
Integration Tests
Integration tests are designed to test how different parts of your system work
together. Unlike unit tests, which focus on isolated components, integration
tests verify that multiple components (such as classes, services, or modules)
collaborate correctly.
Integration tests for a .NET MAUI application are no different from other
integration tests. Here is one of the integration tests for the
FitnessApiService:
185
[Fact]
public async Task Get_Exercises_ReturnsSuccessAndCorrectData()
// Arrange
// Act
response.EnsureSuccessStatusCode();
ReadAsStringAsync();
// Assert
Assert.NotNull(responseData);
Assert.NotNull(exerciseCatalogue);
Assert.NotEmpty(exerciseCatalogue.Exercises);
That said, it’s important to note that this app remains a sample project
designed to highlight specific features and challenges. The code is kept as
186
Chapter 9 Cleaning Up, testing, and OptimizatiOn concise and readable as
possible to showcase concepts clearly. If you’re interested in the details of
these changes, you can find all the updates in the related commit.
UI Tests
• Appium: This is still a robust, widely used solution that fully supports
.NET MAUI apps across Android and
187
Chapter 9 Cleaning Up, testing, and OptimizatiOn Even though we have unit
and integration tests and UI tests, they always find edge cases that we’ve
missed. Exploratory testing is a manual testing technique where testers
actively explore the application without following a predefined set of test
cases. The focus is on discovery, investigation, and learning as testers
interact with the application in real time, finding issues that might not be
uncovered through traditional scripted testing.
As the name implies, scenario-based testing is when tests are created based
on specific cases or scenarios. It is also commonly referred to as use case
testing. It’s similar to regression testing which is the process of running a
suite of tests, including previously defined scenarios, after changes are made
to the application to ensure no new defects are introduced.
Performance
There’s no denial that there is limited tooling for performance and memory
profiling due to the complexity of building cross-platform applications, but
there are some. While this performance profiling is out of scope for this
book, I do want to discuss how we can improve performance.
If you’d like to learn more about profiling .NET MAUI applications, have a
read here: https://github.com/dotnet/maui/wiki/Profiling-.NET-
MAUI-Apps.
188
• Optimize layout: Use efficient layouts like Grid and StackLayout, and
avoid deeply nested
AppForFitnessCore/Views/ProgressPage.xaml to
xaml, you could use compiled bindings for frequently updated properties.
Compiled bindings are enabled by default; you might have seen this on some
of the pages:
[XamlCompilation(XamlCompilationOptions.Compile)]
The line can be removed as its default, but you can set the option to Skip.
That would also mean that errors will be reported at runtime instead of
compile time. Skip is for obvious reasons not recommended.
While the code above was declared for the type, you can also set it for the
assembly, forcing the option:
[assembly: XamlCompilation(XamlCompilationOptions.
Compile)]
• Image optimization: Use appropriate image sizes and formats. Review the
images in your Resources/Images folder to ensure they’re optimized for
mobile devices.
189
performed at startup.
• Optimize data access: If you’re using a local database, ensure queries are
optimized. Review any database operations in your services.
https://learn.microsoft.com/en-us/dotnet/maui/deployment/
performance
Memory
MemoryToolkit.Maui.
2. Add to builder:
if DEBUG
// Configure logging
builder.Logging.AddDebug();
builder.UseLeakDetection(collectionTarget =>
detected.
Application.Current?.MainPage?.DisplayAlert
$"
{collectionTarget.Name} is a
zombie!", "OK");
});
#endif
191
<ContentPage xmlns="http://schemas.microsoft.com/
dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/
winfx/2009/xaml"
xmlns:mtk="clr-namespace:MemoryToolkit.
Maui;assembly=MemoryToolkit.Maui"
x:Class="ShellSample.MainPage"
mtk:LeakMonitorBehavior.Cascade="True">
leaks. -->
</ContentPage>
Adding this to the pages in AppForFitness yielded a lot of memory leaks, all
of them when navigating away from a page. And I suspect that the memory
leak detection library is reporting false positives due to the way AppShell
manages page instances. This is a common misunderstanding when working
with memory profiling tools in the context of navigation frameworks like
Shell.
Instead, it caches them for faster navigation in the future, which is nice and
what we often (but not always) want.
Maui you’re using, often work by tracking object allocations and checking
192
False positives: In the case of Shell, pages that are kept in memory
intentionally might be flagged as leaks because they’re not being garbage
collected when the detection tool expects them to be.
To demonstrate a memory leak, I’ve added an extra page in the app with a
memory leak, for educational purposes.
event. This means that every time the page appears, we’re adding a new
event handler without removing the old ones.
193
allocated because
• The event handler holds a reference to the large byte array through its
closure.
• Accumulation over time: In a real app, if this page is navigated to and from
repeatedly, the memory
It’s primarily designed to detect leaks in UI elements and views, and it may
not catch all types of leaks, especially those related to non-UI objects or
complex scenarios.
It can produce false positives (indicating a leak where there isn’t one) and
false negatives (missing actual leaks). The WorkoutPage is a perfect example
of a leak that might not be caught by the MemoryLeak Toolkit.
194
Chapter 9 Cleaning Up, testing, and OptimizatiOn This is because the leak is
occurring due to event handler subscriptions, which are not directly tied to
the UI elements that the toolkit primarily monitors.
False negatives: In our case, the toolkit gave warnings for controls on other
pages but missed the actual leak in the WorkoutPage. This highlights the
importance of not relying solely on automated tools for leak detection.
• Profiling tools
It’s crucial to understand what each tool can and cannot do. The
MemoryLeak Toolkit is excellent for catching certain types of UI-related
leaks, but it’s not a comprehensive solution for all memory issues.
Trimming and AOT for Performance Optimization Beyond the
optimizations we discussed earlier, there is also AOT and trimming. Mobile
platforms like iOS and Android have constrained memory and resources, so
shaving a couple of megabytes makes a big difference. There are two key
techniques we can add to our already optimized apps that we’ve only briefly
mentioned so far, Ahead-of-Time compilation (AOT) and trimming. These
two can improve startup times and responsiveness and reduce app size. Let’s
have a closer look, starting with trimming.
195
Trimming is a .NET SDK feature that removes unused assemblies and code
from your app, which helps reduce the final size of the app. When you build
your app, in particular for release, there might be many parts of the libraries
you use, such as the .NET Base Class library, that aren’t used by the app.
Trimming removes what isn’t being used, keeping only the essential bits. By
analyzing the code and determining what is used and what isn’t used, the
trimming tool is able to remove what isn’t referenced by your code. We
covered trimming incompatibilities earlier in the book, but as a refresher
here they are:
Emit usage)
• Dynamic assembly loading (the trimmer won’t know what to use and what
to trim away if you dynamically load assemblies)
• C++/CLI
• COM marshaling
In your .csproj file, you can enable or adjust trimming by setting the
PublishTrimmed property to true. Here’s a basic example:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
196
Chapter 9 Cleaning Up, testing, and OptimizatiOn You can also specify
different trimming granularity using the TrimMode property, such as
• Full (default)
<ItemGroup>
</ItemGroup>
While trimming is powerful, it’s important to test your app thoroughly after
enabling it, as aggressive trimming can sometimes remove parts of the
framework or libraries that your app needs at runtime.
https://learn.microsoft.com/en-us/dotnet/core/deploying/
trimming/trimming-options.
compilation. This means all iOS apps built with .NET MAUI are fully AOT
compiled into native ARM assembly code. However, AOT can also be used
on Android to improve performance, though it is not mandatory.
197
To enable AOT compilation for your Android app, you can add the following
configuration to your .csproj file:
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
<UseLLVM>true</UseLLVM>
</PropertyGroup>
However, AOT compilation does come with trade-offs. While it can greatly
improve startup time and overall performance, it can also increase build
times and, in rare cases, the size of the app. Therefore, test and test some
more. It’s important to configure and test these optimizations carefully to
strike the right balance between app size and performance and functionality
if your app relies on dynamic assembly loading or reflection-based
serializers.
Summary
That said, the journey doesn’t end here. As .NET MAUI continues to evolve,
we have to stay informed about new updates, improvements, and best
practices. Ongoing maintenance is key to ensuring our app remains robust,
scalable, and ready to embrace the future of mobile development.
Staying current is just as important as the initial migration itself, and our
goal is to keep refining our app as .NET MAUI matures.
In the appendix, you’ll find a detailed list of resources to help you stay up to
date, along with a section on common migration problems and known issues.
If anything is missing, don’t hesitate to reach out to Apress or me. We’re
always here to help you on your .NET MAUI journey.
199
OceanofPDF.com