Introducing .NET MAUI - 2nd - 2025
Introducing .NET MAUI - 2nd - 2025
.NET MAUI
Build and Deploy Cross-Platform
Applications Using C# and .NET 9.0
Multi-Platform App UI
—
Second Edition
—
Shaun Lawrence
Introducing .NET MAUI
Build and Deploy
Cross-Platform Applications
Using C# and .NET 9.0
Multi-Platform App UI
Second Edition
Shaun Lawrence
Introducing .NET MAUI: Build and Deploy Cross-Platform Applications
Using C# and .NET 9.0 Multi-Platform App UI, Second Edition
Shaun Lawrence
St Ives, UK
v
TABLE OF CONTENTS
vi
TABLE OF CONTENTS
vii
TABLE OF CONTENTS
viii
TABLE OF CONTENTS
HorizontalStackLayout .................................................................................125
VerticalStackLayout .....................................................................................126
Data Binding......................................................................................................129
Binding ........................................................................................................129
Applying the Remaining Bindings................................................................134
MultiBinding ................................................................................................135
Command ....................................................................................................138
Compiled Bindings.......................................................................................141
Make Use of the BoardDetailsPage ...................................................................142
Taking Your Application for a Spin.....................................................................143
Summary...........................................................................................................144
Source Code ................................................................................................144
ix
TABLE OF CONTENTS
Summary...........................................................................................................187
Source Code ................................................................................................188
Extra Assignment ..............................................................................................189
Source Code ................................................................................................189
x
TABLE OF CONTENTS
xi
TABLE OF CONTENTS
xii
TABLE OF CONTENTS
xiii
TABLE OF CONTENTS
Summary...........................................................................................................343
Source Code ................................................................................................344
Extra Assignment ..............................................................................................345
Source Code ................................................................................................345
xiv
TABLE OF CONTENTS
xv
TABLE OF CONTENTS
xvi
TABLE OF CONTENTS
xvii
TABLE OF CONTENTS
Trimming .....................................................................................................524
Ahead-of-Time Compilation.........................................................................531
When Libraries Don’t Support Trimming or AOT ..........................................536
Results.........................................................................................................537
Crashes/Analytics ........................................................................................537
Obfuscation .................................................................................................538
Distributing Test Versions ..................................................................................541
Summary...........................................................................................................541
Index .................................................................................................551
xviii
About the Author
Shaun Lawrence is an experienced software
engineer who has been specializing in building
mobile and desktop applications for the past
20 years. He is a recognized Microsoft MVP
in Development Technologies for his work
helping the community learn and build with
Xamarin.Forms and .NET MAUI. His recent
discovery of the value he can add by sharing
his experience with others has thrust him on to
the path of wanting to find any way possible to
continue it.
Shaun actively maintains several open source projects within the .NET
community. A key project for the scope of this book is the .NET MAUI
Community Toolkit where he predominantly focuses on building good
quality documentation for developers to consume. Shaun lives in the UK
with his wife, two children, and their dog.
xix
About the Technical Reviewer
Gerald Versluis is a Senior Software Engineer
at Microsoft working on .NET MAUI. Since
2009, Gerald has been working on a variety of
projects, ranging from front end to back end
and anything in between that involve C#, .NET,
Azure, ASP.NET, and all kinds of other .NET
technologies. At some point, he fell in love
with cross-platform and mobile development
with Xamarin, now .NET MAUI. Since that
time, he has become an active community member, producing content
online and presenting about all things tech on conferences all around the
world.
xxi
Acknowledgments
I have a few people that I would like to thank for their assistance.
Firstly, Gerald: Not only have you reviewed this book, but you have
been there to help me overcome some tricky obstacles with either one of
your many YouTube videos or just general experience to guide me to a
sensible solution.
Secondly, Bailey, our family cockerpoo: Forcing me out on those
lengthy walks come rain or shine really helped me to clear my head and
provide some time for my brain to catch up. I can’t tell you how many
solutions we came up with and had to rush home to jot them down!
Thirdly, the team at Apress: You have all helped me to keep on track
and provide great support whenever I got stuck.
Finally, my family – my wife Levinia and daughters Zoey and Hollie:
Without your encouragement I would not have been brave enough to
take on the challenge to write this book. I am so grateful for the help and
sacrifices you have each made to help me get this book finished and even
the little slots of time waiting for you to finish dance class.
xxiii
Introduction
Welcome to Introducing .NET MAUI.
This book has been written for developers who are new to .NET MAUI
and cross-platform development. You should have basic knowledge of C#
but require no prior knowledge of using .NET MAUI. The content ranges
from beginner through to more advanced topics and is therefore tailored
to meet a wide range of experiences. In fact, my intention is to allow you
to learn different levels of content upon multiple reads of this book; you
can feel free to skip past the more complex scenarios and just apply the
results at the end of the chapter if you do not feel ready for it. Then upon
subsequent read-throughs, I expect more and more to make sense.
This book provides an in-depth explanation of each key concept in
.NET MAUI, and you will then use these concepts in practical examples
while building a cross-platform application. The content has been
designed to primarily flow with the building of this application; however,
there is a secondary theme that involves grouping as many related
concepts as possible. The idea behind this is to both learn as you go and to
have content that closely resembles reference information, which makes
returning to this book as easy as possible.
All code examples in this book, unless otherwise stated, are applied
directly to the application you are building. Once key concepts have been
established, the book will offer improvements or alternatives to simplify
your future experiences as you build production-worthy applications.
This book does not rely upon these simplifications for all the practical
examples, and the reason for this is simple: I strongly believe that you need
to understand the concepts before you start to use them or use libraries
that do it for you.
xxv
INTRODUCTION
xxvi
CHAPTER 1
Introduction to
.NET MAUI
Abstract
In this chapter, you will gain an understanding of what exactly .NET
MAUI is, how it differs from other frameworks, and what it offers you as a
developer wishing to build a cross-platform application that can run on
both mobile and desktop environments. I will also cover the reasons why
you should consider it for your next project by weighing the possibilities
and limitations of the framework as well as the rich array of tooling
options.
.NET MAUI provides a single API that allows developers to write code
once and run it anywhere. When building a .NET MAUI application, you
write code that interacts with this single cross-platform API, and .NET MAUI
provides the bridge between your code and the platform-specific layer.
If you take a look inside the prism in Figure 1-1, you can start to
understand the components that .NET MAUI both uses and offers.
Figure 1-2 shows how an Android application is compiled. We can make
the statement that when compiling our application for Android, Your code
is compiled against .NET MAUI and in turn .NET for Android.
Figure 1-2 shows how our code only directly makes use of the .NET
MAUI APIs, but then under the hood, .NET MAUI makes use of the .NET
for Android APIs. It is through this approach that we as developers can
make the most of code sharing – by making the most of the API surface that
is provided to us by .NET MAUI.
2
CHAPTER 1 INTRODUCTION TO .NET MAUI
There will be times when the API surface of .NET MAUI does not
provide everything that you need; for this, you will need to directly access
a platform feature. .NET MAUI provides enough flexibility that you can
achieve this by interacting directly with the platform-specific APIs:
• .NET for Android
• .NET for iOS
3
CHAPTER 1 INTRODUCTION TO .NET MAUI
4
CHAPTER 1 INTRODUCTION TO .NET MAUI
5
CHAPTER 1 INTRODUCTION TO .NET MAUI
performance, and other topics that I will touch on throughout this book.
This makes me believe that this mature cross-platform framework has
finally become a first-class citizen of the .NET and Microsoft ecosystems. I
guess the clue is in the first part of its new name.
On the topic of its name, .NET MAUI implies that it is a UI framework,
and while this is true, this is not all that the framework has to offer.
Through the .NET and the .NET MAUI platform APIs, we are provided with
ways of achieving common application tasks such as file access, accessing
media from the device gallery, using the accelerometer, and more. The
.NET MAUI platform APIs were previously known as Xamarin Essentials,
so if you are coming in with some Xamarin Forms experience, they should
feel familiar but note that they have evolved to fit within .NET MAUI. I will
touch on much more of this functionality as you progress through this
book with the key chapters being Chapters 10 and 12.
6
CHAPTER 1 INTRODUCTION TO .NET MAUI
Supported Platforms
.NET MAUI provides official support for all of the following platforms:
• Android 5.0 (API level 21) and above
• iOS 12.2 and above
7
CHAPTER 1 INTRODUCTION TO .NET MAUI
Code Sharing
A fundamental goal of all cross-platform frameworks is to enable
developers to focus on achieving their main goals by reducing the effort
required to support multiple platforms. This is achieved by sharing
common code across all platforms. Where I believe .NET MAUI excels over
alternative frameworks is in the first four characters of its name; Microsoft
has pushed hard to produce a single .NET that can run anywhere.
Being a full stack developer myself, I typically need to work on web-
based back ends as well as mobile applications; .NET allows me to write
code that can be compiled into a single library. This library can then be
shared between the web and client applications, further increasing the
code sharing possibilities and ultimately reducing the maintenance effort.
8
CHAPTER 1 INTRODUCTION TO .NET MAUI
Developer Freedom
.NET MAUI offers many ways to build the same thing. Where Xamarin.
Forms was largely designed to support a specific application architecture
(such as MVVM, which I will talk all about in Chapter 4), .NET MAUI is
different. One key benefit of the rewrite by the team at Microsoft is it now
allows the use of other architectures such as MVU (Chapter 4). This allows
us as developers to build applications that suit our preferences, from
different architectural styles to different ways of building UIs and even
different ways of styling an application.
Community
Xamarin has always had a wonderful community. From bloggers to open
source maintainers, there is a vast amount of information and useful
packages available to help any developer build a great mobile application.
One thing that has really struck me is the number of Microsoft employees
9
CHAPTER 1 INTRODUCTION TO .NET MAUI
who are part of this community; they are clearly passionate about the
technology and dedicate their own free time to contributing to this
community. The evolution to .NET MAUI brings this community with it;
Chapter 17 includes a set of resources that make discovering members
within the community and guidance on how to get involved.
Performance
.NET MAUI applications are compiled into native packages for each of the
supported platforms, which means that they can be built to perform well.
Android has always been the slowest platform when dealing with
Xamarin.Forms, and the team at Microsoft has been working hard and
showing off the improvements. The team has provided some really great
10
CHAPTER 1 INTRODUCTION TO .NET MAUI
resources in the form of blog posts covering the progress that has been
made to bring the startup times of Android applications to well below
one second. These posts cover metrics plus tips on how to make your
applications really fly.(https://devblogs.microsoft.com/dotnet/
dotnet-9-performance-improvements-in-dotnet-maui/)
Android apps built using .NET MAUI compile from C# into
intermediate language (IL), which is then just-in-time (JIT) compiled to a
native assembly when the app launches.
iOS and macOS apps built using .NET MAUI are fully ahead-of-time
(AOT) compiled from C# into native ARM assembly code.
Windows apps built using .NET MAUI use Windows UI Library
(WinUI) 3 to create native apps that target the Windows desktop.
• Syncfusion
“The feature-rich/flexible/fast .NET MAUI controls for
building cross-platform mobile and desktop apps with
C# and XAML”
www.syncfusion.com/maui-controls
www.telerik.com/maui-ui
11
CHAPTER 1 INTRODUCTION TO .NET MAUI
• DevExpress
“Our .NET Multi-platform App UI Component Library
ships with high-performance UI components for
Android and iOS mobile development (WinUI desktop
support is coming in 2022). The library includes a Data
Grid, Chart, Scheduler, Data Editors, CollectionView,
Tabs, and Drawer components.”
www.devexpress.com/maui/
• Grial UI Kit
“Grial offers a set of beautiful XAML UI pages, templates,
controls and helpers made for Xamarin.Forms and .NET
MAUI. These cover the most typical Mobile UI patterns
and are crafted by developers, for developers.”
https://grialkit.com/
Note that while these are commercial products, several of them
provide free licenses for smaller companies or independent developers so
I recommend checking out their products.
12
CHAPTER 1 INTRODUCTION TO .NET MAUI
No Camera API
This has been a pain point for a lot of developers throughout the life
of Xamarin.Forms and continues to be an initial pain point for .NET
MAUI. There are some good arguments as to why it hasn’t happened.
Building a camera API against the Android Camera offering has not been
an easy task, as I am sure most developers who have embarked on that
journey can attest to. The sheer fact that Google has recently rewritten the
entire API for a third time shows the inherent challenges.
13
CHAPTER 1 INTRODUCTION TO .NET MAUI
There are investigations into providing a way to avoid this and have
controls render exactly the same on all platforms, but this is still at an
early stage.
14
CHAPTER 1 INTRODUCTION TO .NET MAUI
Visual Studio
Visual Studio is a comprehensive integrated development environment or IDE
that provides a great development experience. I have been using this tool for
years and I can happily say that it continues to improve with each new version.
To build .NET MAUI apps, you must use at least Visual Studio 2022.
15
CHAPTER 1 INTRODUCTION TO .NET MAUI
• Android
• iOS*
• Windows
*A networked Mac with Xcode 13.0 or above is required for iOS
development and deployment. This is due to limitations in place by Apple.
Note that Visual Studio comes with three different pricing options, but
I would like to draw your attention to the Community edition, which is free
for use by small teams and for educational purposes. In fact, everything in
this book can be achieved using the free Community edition.
Rider
JetBrains Rider is an impressive cross-platform IDE that can run on
Windows, macOS, and Linux. JetBrains has a history of producing great
tools to help developers achieve their goals. One highlight is ReSharper,
which assists with inspecting and analyzing code. With Rider, the
functionality provided by ReSharper is built in.
JetBrains offers Rider for free but only for educational use and open
source projects.
I will be using Rider as I build the application alongside this book.
16
CHAPTER 1 INTRODUCTION TO .NET MAUI
improve with each release, I find it leaves me feeling like I am missing out
some of the great pieces of functionality that come from a fully fledged
IDE. If you are new to development or new to .NET MAUI, I would
thoroughly recommend using Visual Studio or Rider.
You may find references to Visual Studio for Mac online; sadly this
tool has been recently discontinued; therefore, it doesn’t make it into
this section officially.
Summary
Throughout the course of this book, you will primarily be using Visual
Studio as the tool to build your application. I will refer to Rider and Visual
Studio Code in the later parts when I cover how to deploy and test macOS
applications.
In this chapter, you have learned the following:
• What .NET MAUI is
17
CHAPTER 2
macOS
There are several tools that you must install on macOS to allow support for
building Mac Catalyst applications and to provide the ability to build iOS
applications from a Windows environment.
This is required if you wish to develop on macOS or deploy to a Mac
or iOS device (even from a Windows machine). If you are happy with only
deploying to Windows or Android from a Windows machine, then you can
skip this part or just read it for reference.
Xcode
Xcode is Apple’s IDE for building applications for iOS and macOS. You
don’t need to use Xcode directly, but Visual Studio needs it in order to
compile your iOS and macOS applications.
20
CHAPTER 2 BUILDING OUR FIRST APPLICATION
21
CHAPTER 2 BUILDING OUR FIRST APPLICATION
Remote Access
The final step to set up the macOS environment is to enable remote login
so that Visual Studio (Windows) can communicate to the Mac to build and
run iOS and macOS applications.
1. Open System Settings (macOS Ventura 13.0+) or
System Preferences on older macOS versions.
2. Select General on the left-hand panel and then
Sharing, as highlighted in Figure 2-2. This image
shows the macOS System Settings dialog with the
Sharing menu option highlighted.
22
CHAPTER 2 BUILDING OUR FIRST APPLICATION
23
CHAPTER 2 BUILDING OUR FIRST APPLICATION
24
CHAPTER 2 BUILDING OUR FIRST APPLICATION
Windows
Visual Studio
First, you must install Visual Studio 2022. These steps will guide you
through getting it ready to build .NET MAUI applications:
1. Download and install Visual Studio 2022. This
can be accessed from https://visualstudio.
microsoft.com/downloads/.
25
CHAPTER 2 BUILDING OUR FIRST APPLICATION
26
CHAPTER 2 BUILDING OUR FIRST APPLICATION
27
CHAPTER 2 BUILDING OUR FIRST APPLICATION
28
CHAPTER 2 BUILDING OUR FIRST APPLICATION
29
CHAPTER 2 BUILDING OUR FIRST APPLICATION
30
CHAPTER 2 BUILDING OUR FIRST APPLICATION
Command Line
The command that we wish to run has the benefit of working on both
Windows and macOS, but opening a command prompt or terminal session
is different on each operating system. Let’s take a look at each in turn.
31
CHAPTER 2 BUILDING OUR FIRST APPLICATION
macOS
Windows
You should verify that the results include maui and that they are of the
expected version. For example, the current version is .NET MAUI 9.0 so we
are looking for the Manifest and Version to start with 9.0. If you are working
with a different major version, then confirm that it is installed.
If the version is not installed, you can then enter the following
command in your Command Prompt or Terminal application and
press return:
32
CHAPTER 2 BUILDING OUR FIRST APPLICATION
33
CHAPTER 2 BUILDING OUR FIRST APPLICATION
34
CHAPTER 2 BUILDING OUR FIRST APPLICATION
35
CHAPTER 2 BUILDING OUR FIRST APPLICATION
36
CHAPTER 2 BUILDING OUR FIRST APPLICATION
cd c:\work\
cd WidgetBoard
dotnet restore
You now have a .NET MAUI application. Let’s proceed to learning how
to build and ultimately run it.
37
CHAPTER 2 BUILDING OUR FIRST APPLICATION
You may also notice the drop-down in the above image that currently
says WidgetBoard (net9.0-android). This allows you to show in the visible
file what applies to that specific target, but it does not affect what you are
currently compiling. Figure 2-16 shows this a little clearer.
38
CHAPTER 2 BUILDING OUR FIRST APPLICATION
Figure 2-16 highlights items 1 and 2 from the above list to highlight
what is compiled vs. what is targeted in Visual Studio.
I want to try something a little bit different from the normal types of
apps that are built as part of a book or course. Something that requires a
fair amount of functionality that a lot of real-world applications also need.
Something that can help to make use of potentially older hardware so we
can give them a new lease on life.
WidgetBoard
The application that we will be building together will allow users to turn
old tablets or computers into their own unique digital board. Figure 2-17
shows a sketch of how it could look once a user has configured it.
40
CHAPTER 2 BUILDING OUR FIRST APPLICATION
Summary
In this chapter, you have
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch02.
41
CHAPTER 3
The Fundamentals
of .NET MAUI
Abstract
In this chapter, you will dissect the project you created in Chapter 2
and dive into the details of each key area. The focus is to provide a good
overview of what a .NET MAUI single project looks like, where each of the
key components are located, and some common ways of enhancing them.
Project Structure
.NET MAUI provides support for multiple platforms from within a single
project. The focus is to allow us as developers to share as much code and
as many resources as possible.
You will likely hear the term single project a lot during your time
working with .NET MAUI. It is a concept that is relatively new to the .NET
world, introduced as part of .NET MAUI. Its key feature is that you can
build applications for multiple different targets from, you guessed it, a
single project. If you have ever built .NET applications that aim to share
code, you will have noticed that each application you wanted to build and
deploy required its own project. The same was true with Xamarin.Forms in
that you would have at least one project with your common code and then
one project per platform. The single project now houses both the shared
code and the platform-specific bits of code.
Figure 3-1 shows a comparison between the old separate project
approach in Xamarin.Forms and the new .NET MAUI project format. The
squares represent a project file.
Let’s inspect the project you created in Chapter 2 so that you can
start to get an understanding of how .NET MAUI supports the multiple
platforms and how they relate to shared code.
The new project has the following structure:
44
Chapter 3 the Fundamentals oF .net mauI
builder.UseMauiApp<App>();
Note that wherever you see a .xaml file, there will typically be an
associated .xaml.cs file. This is due to limitations in what XAML can
provide; it requires an associated C# file to cover the parts that XAML does
not support. I will cover XAML much more extensively in Chapter 5.
45
Chapter 3 the Fundamentals oF .net mauI
It is also worth noting that you do not have to write any XAML. Sure,
.NET MAUI and its predecessor, Xamarin.Forms, have a deep connection
to XAML, but because the XAML is ultimately compiled down to C#,
anything that is possible to create in XAML is also possible in C#. The next
chapter (Chapter 4) will take you through the different possibilities for
architecting your applications.
/Platforms/ Folder
I mentioned that the platform-specific code lives in the Platforms folder.
While cross-platform applications provide a nice abstraction from the
platforms we wish to support, I still believe it is extremely valuable to know
how these platforms behave. Let’s dive in and look at each of the platform
folders to understand what is happening.
Android
Inside the Android platform folder, you will see the following files:
• Resources/values/colors.xml: This contains color
information used for the Android platform. If you
wish to change some of the colors used within your
application, you will need to update this file.
• MainApplication.cs: This is the main entry point for
the Android platform. Initially you should note that
it does very little. The bit it does is rather important,
though; it is responsible for creating the MauiApp using
the MauiProgram class. This is the bridge between the
Android application and your cross-platform .NET
MAUI code.
46
Chapter 3 the Fundamentals oF .net mauI
iOS
Inside the iOS platform folder, you will see the following files:
• AppDelegate.cs: This class allows you to respond to all
platform-specific parts of the application lifecycle.
47
Chapter 3 the Fundamentals oF .net mauI
MacCatalyst
Inside the MacCatalyst platform folder, you will see the following files.
It is worth noting that this section is nearly identical to the previous iOS
section. It’s been kept separate to provide an easy reference to what the
platform folder consists of for MacCatalyst.
Tizen
Inside the Tizen platform folder, you will see the following files:
• Main.cs: This is the main entry point for your Tizen
application.
48
Chapter 3 the Fundamentals oF .net mauI
Windows
Inside the Windows platform folder, you will see the following files:
Summary
Phew! That felt like a lot to take in! I think I need to take a tea break! Don’t
worry, though; while this gives an overview of what each of the files is
responsible for, you will be modifying most of them throughout this book
with some practical examples, so if there are any points that aren’t clear, or
you feel you will need to revisit them, you certainly will be.
/Resources/ Folder
The Resources folder is where you store anything you want to include in
your application that is not strictly code. Let’s look through each of the
subfolders and key types of resource.
49
Chapter 3 the Fundamentals oF .net mauI
AppIcon
This aptly named folder is responsible for housing the icon image files
used to generate our application’s icon. The default project that is created
provides us with two images in this folder. In Chapter 5, you will learn how
to replace the defaults and how the app icons are structured. This type of
resource is called a MauiIcon.
Fonts
.NET MAUI allows you to embed your own custom fonts. This is especially
handy if you are building an app for a specific brand, or you want to make
sure that you render the same font on each platform. You can embed either
True Type Fonts (.ttf files) or Open Type Fonts (.otf files).
A word of warning around fonts. I strongly recommend that you check
the licensing rules around fonts before including them in your application.
While there are sites that make it possible to download fonts freely, a very
large percentage of those fonts usually require paying to use them.
There are two parts to embedding a font so that it can be used within
your application.
1. The font file should be placed in this folder
(Resources/Fonts).
By default, the font will be automatically included
as a font file based on the following line that can be
found inside the project file (WidgetBoard.csproj):
.ConfigureFonts(fonts =>
{
fonts.AddFont("Lobster-Regular.ttf", "Lobster");
});
Images
Practically every application you build will include some images. Each
platform that you wish to support has its own rules on the image sizes that
you need to supply to make the image render as sharp and clear on the
many devices they run. Take iOS, for example. In order to supply a 24×24
pixel image in your app, you must provide three different image sizes:
24×24, 48×48, and 72×72. This is due to the different DPIs for the devices
Apple builds. Android devices follow a similar pattern, but the DPIs are not
the same. This is similar for Windows.
Figure 3-2 shows an example image that would be rendered at 24×24
pixels. Note that while Windows shows the three sizes, this is just based on
recommendations for trying to cover the most common settings. In truth,
Windows devices can have their DPIs vary much more. Figure 3-2 shows
the required image sizes needed for all supported platforms in order to
render a 24×24 pixel image.
51
Chapter 3 the Fundamentals oF .net mauI
You can see from the figure above that it can become painful very
quickly if you have a lot of images in your application each requiring at
least five different sizes to be maintained. Thankfully .NET MAUI gives
us the ability to provide a single Scalable Vector Graphics (SVG) image,
52
Chapter 3 the Fundamentals oF .net mauI
and it will generate the required images for all the platforms when the
application is compiled. I cannot tell you how happy all of us Xamarin.
Forms old timers are at this new piece of functionality!
As it currently stands, if the SVG image is of the correct original size,
you can simply drop the image into the /Resources/Images/ folder and it
will just begin to work in your application. In a similar way to how the fonts
are automatically picked up, you can see how the images are also handled
by looking inside your project file and observing the line <MauiImage
Include="Resources\Images\*" />.
.NET MAUI doesn’t render SVGs directly but generates PNG images
from the SVGs at compile time. This means that when you are referring
to the image you wish, it needs to have the .png extension. For example,
when embedding an image called image.svg, in code, you refer to it as
image.png.
If the contents of the SVG are not of the desired size, then you can add
some configuration to tell the tooling what size the image should be. For
this, the image should not be added to the /Resources/Images/ folder as
the tooling will end up generating duplicates and there is no telling which
one will win. Instead, you can simply add the image to the /Resources/
folder and then add the following line to your project file:
The above code will treat the contents of the image.svg file as being
24×24 pixels and then scale for each platform based on that size.
Raw
The next type of resource to embed is raw files. This essentially means that
what is embedded can be loaded at runtime. A typical example of this is to
provide some data to preload into the application when first starting. This
type of resource is called a MauiAsset.
53
Chapter 3 the Fundamentals oF .net mauI
Splash
This folder is created by default to show how a splash screen can be added
to a .NET MAUI application. In Chapter 5, you will learn how to customize
the defaults and provide your own splash screen along with the many
ways a splash screen can be customized. This type of resource is called a
MauiSplashScreen.
Styles
The Styles folder is where developers are encouraged to create style-
related resources; these could be control styles, color palettes, or even CSS
styles. We will cover these items throughout the book with the main focus
being in Chapter 5. There isn’t a single type of resource for the contents
of this folder but the two defaults created for us; Colors.xaml and Styles.
xaml are of type MauiXaml, and these will be the most common type of
resources that you will create here.
This concludes the section on the /Resources/ folder. Let’s proceed to
covering where an application begins its life.
Where to Begin?
.NET MAUI applications have a single main entry point that is common
across all platforms. This provides us with a way to centralize much of the
initialization process for our applications and therefore only write it once.
You will have noticed that in each of the platform-specific main
entry points covered in the previous section, they all call MauiProgram.
CreateMauiApp();. This is the main entry point into your .NET MAUI and
shared application.
54
Chapter 3 the Fundamentals oF .net mauI
55
Chapter 3 the Fundamentals oF .net mauI
56
Chapter 3 the Fundamentals oF .net mauI
We can see that for the Baker to do their job, they need to know
about all these different pieces of equipment. Now imagine that the
WeighingScale breaks, and a replacement is provided. The Baker will
still need to weigh the ingredients but won’t care how that weighing
is performed. Imagine that the new WeighingScale is digital and now
requires batteries. There are a few reasons why we want to move away from
having hard-coded dependencies as in our Baker example.
• If we did replace the WeighingScale with a different
implementation, we would have to modify the
Baker class.
• If the WeighingScale has dependencies (e.g., batteries
in our new digital scale), they must also be configured
in the Baker class.
• This becomes more difficult to unit test because the
Baker is creating dependencies and therefore a unit test
would result in having to test far more than a unit test is
designed to.
57
Chapter 3 the Fundamentals oF .net mauI
Now our Baker knows about an interface for something that can weigh
their ingredients but not the actual thing that does the weighing. This
means that in the scenario where the weighing scale breaks and a new
one is supplied, there is no change to the Baker class in order to handle
this new scale. Instead, it is registered as part of the application startup or
bootstrapping process. Of course, we could and should follow the same
approach for our other dependencies.
One additional concept I have introduced here is the use of constructor
injection. Constructor injection is the process of providing the registered
dependencies when creating an instance of our Baker. So, when our Baker
is created, it is passed an instance of WeighingScale.
If you have a background with Xamarin.Forms, you will have come
across the DependencyService. This provided a mechanism for managing
dependency injection within an application; however, it received criticism
in the past for not supporting constructor injection. This doesn’t mean
it wasn’t possible to achieve constructor injection in Xamarin.Forms
applications, but it required the use of a third-party package and there are
a lot of great packages out there! Now it is all baked into .NET MAUI.
58
Chapter 3 the Fundamentals oF .net mauI
Registering Dependencies
In the previous section, I discussed how to minimize concrete
dependencies in your code base. Now let’s look through how to configure
those dependencies so that the dependents are given the correct
implementations.
Implementations that you register in the generic host builder are
referred to as services, and the work of providing the implementations out
to dependents is referred to as the ServiceProvider. You can register your
services using the following.
AddSingleton
A singleton registration means that there will only ever be one instance
of the object. So, based on the example of our Baker needing to use an
IWeighingScale, we register it as follows:
builder.Services.AddSingleton<IWeighingScale, WeighingScale>();
59
Chapter 3 the Fundamentals oF .net mauI
AddTransient
A transient registration is the opposite of a singleton. Every time an
implementation is resolved, a new instance is created and provided. So
based on the example of our Baker needing to use an IWeighingScale, we
register it as follows:
builder.Services.AddTransient<IWeighingScale, WeighingScale>();
AddScoped
A scoped registration is somewhere in the middle of a singleton and
transient. A single instance will be provided for a “scope,” and then when
a new scope is created, a new instance will be provided for the life of
that scope.
builder.Services.AddScoped<IWeighingScale, WeighingScale>();
60
Chapter 3 the Fundamentals oF .net mauI
Application Lifecycle
Sadly, no two platforms provide the same set of behaviors or lifecycle
events such as when an application is started, backgrounded, or closed.
This is where cross-platform frameworks provide us with a solid set
of encapsulated events to cover most scenarios. There are four main
application states in a .NET MAUI application.
Application States
These are the application states:
• Not running: This means that the application has not
been started and is not loaded into memory. This is
typically when the application has been installed,
the device has been powered on, the application
was closed by the user, or the operating system has
terminated the application to free up some resources.
• Running: This means that the application is visible and
is focused.
61
Chapter 3 the Fundamentals oF .net mauI
Before we dive into the details of each of the events that are fired
between the state transitions, I need to give you some background on
how they can be accessed and why. In order to access these events, you
must access the Window class. It certainly isn’t a common concept to have
a window in a mobile application, but you must appreciate that you are
dealing with a cross-platform framework and therefore an approach that
fits desktop as well as mobile. I see it as follows: a mobile application is a
single window application, and a desktop is likely to be multi-window.
Lifecycle Events
Now on to the events that move an application between states. These are
the annotations on the arrows from Figure 3-3:
62
Chapter 3 the Fundamentals oF .net mauI
63
Chapter 3 the Fundamentals oF .net mauI
Open Visual Studio. You need to add a new class to your project and
call it StateAwareWindow. Your new class will need to be modified so it
looks as follows:
Inside of your application, you can override all methods that will
be executed when the specific event occurs. Each override method
follows the naming of the events, as described previously, with a prefix
of On. Therefore, to handle the Activated event, you override the
OnActivated method.
The final step is to make use of the new class, so inside your App.xaml.
cs file, add the following:
64
Chapter 3 the Fundamentals oF .net mauI
65
Chapter 3 the Fundamentals oF .net mauI
This list may not provide too much meaning right now, and I wouldn’t
worry yourself with needing to know this. The aim here is to provide you
with a quick look-up to be able to then research if any lifecycle events are
going wrong or possibly not the right fit for your solution. I can safely say
that a large number of the issues I have helped clients with in the past
are around how the lifecycle of an application differs on each platform
supported by .NET MAUI.
66
Chapter 3 the Fundamentals oF .net mauI
Android
To receive a notification for an Android lifecycle event, you call the
ConfigureLifecycleEvents method on the MauiAppBuilder object. You
can then make use of the AddAndroid method and specify the events you
wish to handle and how you wish to handle them.
using Microsoft.Maui.LifecycleEvents;
namespace WidgetBoard;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureLifecycleEvents(events =>
{
#if ANDROID
events.AddAndroid(lifecycle=>
lifecycle.OnStart((activity) =>
OnStart(activity)));
static void OnStart(Activity activity)
{
// Perform your OnStart logic
}
#endif
});
return builder.Build();
}
}
67
Chapter 3 the Fundamentals oF .net mauI
using Microsoft.Maui.LifecycleEvents;
namespace WidgetBoard;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureLifecycleEvents(events =>
{
#if IOS || MACCATALYST
events.AddiOS(lifecycle =>
lifecycle.OnActivated((app) =>
OnActivated(app)));
static void OnActivated(UIKit.UIApplication
application)
68
Chapter 3 the Fundamentals oF .net mauI
{
// Perform your OnActivated logic
}
#endif
});
return builder.Build();
}
}
Windows
To receive a notification for a Windows lifecycle event, you call the
ConfigureLifecycleEvents method on the MauiAppBuilder object. You
can then make use of the AddWindows method and specify the events you
wish to handle and how you wish to handle them.
using Microsoft.Maui.LifecycleEvents;
namespace WidgetBoard;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureLifecycleEvents(events =>
69
Chapter 3 the Fundamentals oF .net mauI
{
#if WINDOWS
events.AddWindows(lifecycle =>
lifecycle.OnActivated((window, args) =>
OnActivated(window, args)));
static void OnActivated(Microsoft.
UI.Xaml.Window window, Microsoft.UI.Xaml.
WindowActivatedEventArgs args)
{
// Perform your OnActivated logic
}
#endif
});
return builder.Build();
}
}
70
Chapter 3 the Fundamentals oF .net mauI
Summary
In this chapter, you have
• Walked through the main components of a .NET MAUI
application
• Earned a tea break
• Learned about the startup process
• Learned about the life of a .NET MAUI application
In the next chapter, you will
• Learn about the different possibilities you have to
architect your applications
• Decide on what architecture to use
• Walk through a concrete example by creating your
ClockWidget
• Learn how to further optimize your implementation
using NuGet packages
71
CHAPTER 4
An Architecture
to Suit You
Abstract
In this chapter, you will look through some possible architectural patterns
that can be used to build .NET MAUI applications. The objective is to
provide you with enough detail to help you find the architecture that best
fits you. I want to point out that there are no right answers concerning
which architecture to choose. The best option is to go with one that you
feel will benefit you and your team.
I aim to quash the following myths throughout the course of this
chapter:
A Measuring Stick
You will build the same control with each of the options to provide a way to
compare the differences. The control you will be building is a ClockWidget.
The purpose of this control is to do the following:
• Display the current time in your app.
• Update the time every minute.
Figure 4-1 shows a very rough layout of the control with the current date
and time. You will tidy this up later with the ability to format the date and time
information in Chapter 5, but for now, let’s just focus on a limited example to
highlight the differences in options. Figure 4-1 shows how the ClockWidget
will render in your application when you have finished with this chapter.
Prerequisites
Before you get started with each of the architectures you will be reviewing
in this chapter, you need to do a little bit of background setup to prepare.
You need to add a single new class. This implementation will allow
your widgets to schedule an action of work to be performed after a specific
period of time. In your scenario of the ClockWidget, you can schedule an
update of the UI. Let’s add this Scheduler class into your project.
• Right-click the WidgetBoard project.
• Select Add ➤ Class.
• Give it the name of Scheduler.
• Click Add.
74
Chapter 4 an arChiteCture to Suit You
namespace WidgetBoard;
75
Chapter 4 an arChiteCture to Suit You
Model
The Model is where you keep your business logic. It is typically loaded
from a database/web service among many other things.
For your business logic, you are going to rely on the Scheduler class
that you created earlier in the “Prerequisites” section of this chapter.
76
Chapter 4 an arChiteCture to Suit You
View
The View defines the layout and appearance of the application. It is what
the user will see and interact with. In .NET MAUI, a View is typically
written in XAML where possible, but there will be occasions when logic
in the code-behind will need to be written. You will learn this later in this
chapter; you don’t have to use XAML at all, so if you don’t feel XAML is
right for you, fear not.
A View in .NET MAUI is typically a ContentPage or an implementation
that will inherit from ContentPage or ContentView. You use a ContentPage
if you want to render a full page in your application (basically a view that
will fill the application). You use a ContentView for something smaller
(like a widget!). For your implementation, you will be inheriting from a
ContentView.
I discussed in Chapter 2 that the majority of XAML files come with an
associated C# file. A XAML-based view is no exception to this rule. With
this in mind, let’s take a look at the contents you need to place in each of
the files.
XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentView
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:WidgetBoard.ViewModels"
x:Class="WidgetBoard.ClockWidget">
<ContentView.BindingContext>
<viewmodels:ClockWidgetViewModel />
</ContentView.BindingContext>
<Label Text="{Binding Time}"
77
Chapter 4 an arChiteCture to Suit You
FontSize="80"
VerticalOptions="Center"
HorizontalOptions="Center" />
</ContentView>
C# (Code-Behind)
The following code will have already been created for you by the .NET
MAUI template. It is included for reference.
namespace WidgetBoard;
public partial class ClockWidget : ContentView
{
public ClockWidget()
{
InitializeComponent();
}
}
ViewModel
The ViewModel acts as the bridge between the View and the Model. You
expose properties and commands on the ViewModel that the View will
bind to. To make a comparison to building applications with just code-
behind, we could state that properties basically map to references of
controls and commands are events. A binding provides a mechanism for
both the View and ViewModel to send and receive updates.
78
Chapter 4 an arChiteCture to Suit You
For your ViewModel to notify the View that a property has changed
and therefore the View will refresh the value displayed on screen, you
need to make use of the INotifyPropertyChanged interface. This offers
a single PropertyChanged event that you must implement and ultimately
raise when your data-bound value has changed. This is all handled by the
XAML binding engine, which you will look at in much more detail in the
next chapter. Let’s create your ViewModel class and then break down what
is going on.
79
Chapter 4 an arChiteCture to Suit You
public ClockWigetViewModel()
{
SetTime(DateTime.Now);
}
You have
• Created a class called ClockWidgetViewModel
• Implemented the INotifyPropertyChanged interface
• Added a property that when set will check whether
its value really has changed, and if it has, raise the
PropertyChanged event with the name of the property
that has changed
• Added a method to set the Time property and repeat
every second so that the widget looks like a clock
counting
80
Chapter 4 an arChiteCture to Suit You
81
Chapter 4 an arChiteCture to Suit You
macOS
1. Open the Terminal application.
2. Enter the following command and then press return:
Windows
1. Open the Command Prompt application.
2. Enter the following command and then press return:
This will install the template so that you can create a new project.
Sadly, this is different enough to the WidgetBoard project that you have
been working with so far.
Next, you need to create the project. This is again done via the terminal
for now:
This will create a new project that you can start modifying.
82
Chapter 4 an arChiteCture to Suit You
the structure looks similar to a standard .NET MAUI project. One key
difference is that there are very few (only two) XAML files; the bulk of the
applications written in MVU will be through using C#.
The next key detail is how a View is represented. Views in MauiReactor
are referred to as Components; the aim of building components is to create
small reusable components that can make up the building blocks on an
application or multiple applications. The Views are considered immutable
in MVU, which means they will never update; instead, when the state
(model) is updated, the view will be redrawn in order to visually represent
the changes to the state. With this detail in mind, it is essential to build
small components in order to limit the amount of the application that
needs to be redrawn when some state changes.
Let’s proceed to making some changes in order to see MVU in action in
.NET MAUI. The template will have created a single MainPage.cs file under
the /Pages folder. This is the file that we are going to want to modify for the
purpose of creating a ClockWidget.
The result of the above change will be to present a page with a single
component inside.
83
Chapter 4 an arChiteCture to Suit You
Now that you have added a load of code, let’s summarize what you
have done.
• You have created your state (model) class ClockState.
• You have created a new component named
ClockWidget.
• You have defined your state type as ClockState.
84
Chapter 4 an arChiteCture to Suit You
85
Chapter 4 an arChiteCture to Suit You
Let’s work through how you can build your ClockWidget in C# in all its
verbosity, and then I will show how you can simplify it using C# Markup. (I
must add this is an open source package that you need to bring in.) Also,
these examples are still built using MVVM.
Plain C#
As mentioned, anything you can build in XAML can also be built in C#.
The following code shows how the exact same XAML definition of your
ClockWidget can be built using just C#:
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
86
Chapter 4 an arChiteCture to Suit You
C# Markup
I have recently come to appreciate the value of being able to fluently
build UIs. I don’t tend to do it often because I personally feel comfortable
building with XAML or perhaps it is Stockholm syndrome kicking in ☺
(I’ve been working with XAML for well over ten years now). When I do, it
needs to be as easy to read and build as possible given it is not something I
do often.
As a maintainer on the .NET MAUI Community Toolkit, one of the
packages we provide is CommunityToolkit.Maui.Markup. It provides a set
of extension methods and helpers to build UIs fluently.
using CommunityToolkit.Maui.Markup;
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
87
Chapter 4 an arChiteCture to Suit You
.Font(size: 80)
.CenterHorizontal()
.CenterVertical()
.Bind(Label.TextProperty, getter: static
(ClockWidgetViewModel viewModel) =>
viewModel.Time);
}
}
This code performs the same steps as the plain C# example; however,
the code is much easier to read. I am sure you can imagine that when the
complexity of the UI increases, this fluent approach can really start to
benefit you.
88
Chapter 4 an arChiteCture to Suit You
Now that I have covered the various architecture options and decided
on using MVVM, let’s proceed to adding in the specific Views and
ViewModels so that they can be used inside the application. Then I will
show how to start simplifying the implementation so that the code really
only needs to include the core logic by avoiding having to add a lot of the
boilerplate code.
Adding IWidgetViewModel
The first item you need to add is an interface. It will represent all widget
view models that you create in your application.
• Right-click the ViewModels folder.
• Select Add ➤ New Item.
• Click Add.
89
Chapter 4 an arChiteCture to Suit You
namespace WidgetBoard.ViewModels;
public interface IWidgetViewModel
{
int Position { get; set; }
string Type { get; }
}
Adding BaseViewModel
This will serve as the base class for all of your view models so that you only
have to write some boilerplate code once. Don’t worry; you will see how to
optimize this even further!
• Right-click the ViewModels folder.
You can replace the contents of the class file with the following code:
namespace WidgetBoard.ViewModels;
90
Chapter 4 an arChiteCture to Suit You
You should be familiar with the first line inside the class:
This is the event definition that you must add as part of implementing
the INotifyPropertyChanged interface, and it serves as the mechanism for
your view model to update the view.
The next method provides a mechanism to easily raise the
PropertyChanged event:
91
Chapter 4 an arChiteCture to Suit You
92
Chapter 4 an arChiteCture to Suit You
Adding ClockWidgetViewModel
Let’s add a new class file into your ViewModels folder as you did for the
BaseViewModel.cs file. Call this file ClockWidgetViewModel and modify
the contents to the following:
using System;
using System.ComponentModel;
namespace WidgetBoard.ViewModels;
public ClockWidgetViewModel()
{
SetTime(DateTime.Now);
}
93
Chapter 4 an arChiteCture to Suit You
Adding Views
First, add a new folder to your project.
• Right-click the WidgetBoard project.
• Select Add ➤ New Folder.
• Enter the name Views.
• Click Add.
This folder will house your application’s views. Let’s proceed to adding
your first one.
Adding IWidgetView
The first item you need to add is an interface to represent all widget view
models that you create in your application.
• Right-click the Views folder.
94
Chapter 4 an arChiteCture to Suit You
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
Adding ClockWidgetView
The next item you need to add is a ContentView. This is the first time you
are doing this, so use the following steps:
• Right-click the Views folder.
• Select Add ➤ New Item.
95
Chapter 4 an arChiteCture to Suit You
Observe that two new files have been added to your project:
ClockWidgetView.xaml and ClockWidgetView.xaml.cs. You may
notice that the ClockWidgetView.xaml.cs file is hidden in the Solution
Explorer panel and that you need to expand the arrow to the left of the
ClockWidgetView.xaml file.
Let’s update both files to match what was in the original examples.
Open the ClockWidgetView.xaml file and modify the contents to the
following:
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
96
Chapter 4 an arChiteCture to Suit You
This completes the work to add the ClockWidget into your code base.
Now you need to modify your application so that you can see this widget
in action!
Modifying MainPage.xaml
Simply replace the contents of the file with the following:
The original file had a basic example that ships with the .NET MAUI
template, but it wasn’t of much use in this application.
97
Chapter 4 an arChiteCture to Suit You
Modifying MainPage.xaml.cs
You need to modify the contents of this file because you deleted some
controls from the MainPage.xaml file. If you don’t update this file, Visual
Studio will report compilation errors. You can replace the entire contents
of the MainPage.xaml.cs file with the following to remove references to the
controls you deleted from the XAML file:
namespace WidgetBoard;
This concludes the changes that you need to make in your application.
Let’s see what your application looks like now!
98
Chapter 4 an arChiteCture to Suit You
You have looked at ways to optimize your code base when using MVVM,
but I would like to provide some further details on how you can leverage the
power of the community in order to further improve your experience.
MVVM Enhancements
There are two key parts I will cover regarding how you can utilize existing
packages to reduce the amount of code you are required to write.
MVVM Frameworks
There are several MVVM frameworks that can expand on this by providing
a base class implementation for you with varying levels of other extra
features. To list a few:
• CommunityToolkit.Mvvm
• FreshMVVM
• Prism
• ReactiveUI
99
Chapter 4 an arChiteCture to Suit You
These packages will ultimately provide you with a base class very
similar to the BaseViewModel class that you created earlier. For example,
the Prism library provides the BindableBase class that you could use. It
offers yet another optimization in terms of less code that you need to write
and ultimately maintain.
You can go a step further, but you need to believe.
Magic
Yes, that’s right: magic is real! These approaches involve auto-generating
the required boilerplate code so that we as developers do not have to do it.
There are two main packages that offer this functionality. They provide it
through different mechanisms, but they work equally well.
In the past, I was skeptical of using such packages. I felt like I was
losing control of parts that I needed to hold on to. Now I can appreciate
that I was naïve, and this is impressive.
Let’s look at how these packages can help to further reduce the
code. This example uses CommunityToolkit.Mvvm, which provides the
ObservableObject base class and a wonderful way of adding attributes
([ObservableProperty]) to the fields you wish to trigger PropertyChanged
events when their value changes. This will then generate a property with
the same name as the field but with a capitalized first character, so time
becomes Time.
100
Chapter 4 an arChiteCture to Suit You
public ClockWigetViewModel()
{
SetTime(DateTime.Now);
}
That’s 17 lines down to 2 from the original example! The part that I
really like is that it reduces all the noise of the boilerplate code so there is a
bigger emphasis on the code that we need to write as developers.
You may have noticed that you are still referring to the Time property
in the code but you haven’t supplied the definition for this property. This
is where the magic comes in! If you right-click the Time property and select
Go to Definition…, it will open the following source code so you can view
what the toolkit has created for you:
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace WidgetBoard.ViewModels
{
partial class ClockWidgetViewModel
{
/// <inheritdoc cref="time"/>
101
Chapter 4 an arChiteCture to Suit You
[global::System.CodeDom.Compiler.GeneratedCode
("CommunityToolkit.Mvvm.SourceGenerators.
ObservablePropertyGenerator", "8.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.
ExcludeFromCodeCoverage]
public global::System.DateTime Time
{
get => time;
set
{
if (!global::System.Collections.Generic.
EqualityComparer<global::System.DateTime>.
Default.Equals(time, value))
{
OnTimeChanging(value);
OnPropertyChanging(global::Community
Toolkit.Mvvm.ComponentModel.__Internals.__
KnownINotifyPropertyChangingArgs.Time);
time = value;
OnTimeChanged(value);
OnPropertyChanged(global::CommunityTool
kit.Mvvm.ComponentModel.__Internals.__
KnownINotifyPropertyChangedArgs.Time);
}
}
}
/// <summary>Executes the logic for when <see
cref="Time"/> is changing.</summary>
[global::System.CodeDom.Compiler.GeneratedCode
("CommunityToolkit.Mvvm.SourceGenerators.
ObservablePropertyGenerator", "8.0.0.0")]
102
Chapter 4 an arChiteCture to Suit You
You can see that the generated source code looks a little noisy, but it
does in fact generate the property you need. View the section highlighted
in bold above.
I have only really scratched the surface regarding the functionality
that the CommunityToolkit.Mvvm offers. I strongly urge you to refer
to the documentation at https://learn.microsoft.com/dotnet/
communitytoolkit/mvvm/ to learn how it can further aid your application
development because this will not be looked into any deeper in this book
so we can focus on the fundamentals.
Summary
I hope I have made it clear that there is no single right way to do things or
build applications. You should pick and choose what approaches will best
suit your environment. With this point in mind, the goal of this chapter was
to give you a good overview of several different approaches to architecting
your application. There are always a lot of opinions floating around to
indicate which architectures people prefer, but I strongly urge you to
evaluate which will help you to achieve your goals best.
103
Chapter 4 an arChiteCture to Suit You
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch04.
104
CHAPTER 5
User Interface
Essentials
Abstract
In this chapter, you are going to investigate the fundamental parts of
building a .NET MAUI application. You are going to apply an icon and
splash screen, add in some pages and their associated view models, and
configure some bindings between your page and the view model. You will
also gain an understanding of what XAML is and what it has to offer as you
build the pages of your application.
Prerequisites
You need to do some setup before you can jump into using Shell. If Shell is
still feeling like an unknown concept, fear not. I will be covering it a little
bit later in this chapter under the “Shell” section.
Let’s go ahead and add the following folders to your project.
Models
This will house all of your Model classes. If you recall from Chapter 4, these
are where some of your business logic is located. In your Models folder, you
need to create one class.
• Right-click the Models folder.
Board.cs
This will serve as a base class for the layout options you provide. In
this book, you will only be building fixed layout boards, but I wanted to
lay some groundwork so if you are feeling adventurous, you can go off
and build alternative layout options without having to restructure the
application. In fact, I would love to hear where you take it!
Your fixed layout will offer the user of the app the ability to choose a
number of rows and columns and then position their widgets in them.
namespace WidgetBoard.Models;
This is the first time that we have used the init keyword in this book.
I wanted to explain its use in case you are not familiar with it; the init
keyword allows us to define a property that can be set only when a new
instance is initialized. This means that the following is allowed:
106
CHAPTER 5 USER INTERFACE ESSENTIALS
To highlight the value of the init keyword, the following code will
generate three compiler errors, one compiler per property that hasn’t been
assigned a value.
Pages
This will house the pages in your application. I am distinguishing between
a page and a view because they do behave differently in .NET MAUI. You
can think of a page as a screen that you are seeing whereas a view is a
smaller component. A page can contain multiple views.
Let’s go ahead and create the following files under the Pages folder.
The following steps show how to add the new pages.
107
CHAPTER 5 USER INTERFACE ESSENTIALS
BoardDetailsPage
This is the page that lets you both create and edit your boards. For now,
you will not touch the contents of this file. Note that you should see
BoardDetailsPage.xaml and BoardDetailsPage.xaml.cs files created.
You also need to jump over to the MauiProgram.cs file and register this
page with the Services inside the CreateMauiApp method just before the
return builder.Build(); line.
builder.Services.AddTransient<BoardDetailsPage>();
ViewModels
This houses your ViewModels that are the backing for both your Pages and
Views. You created this folder in the previous chapter, but you need to add
a number of classes. The following steps show how to add the new pages:
• Right-click the ViewModels folder.
• Select Add ➤ New Class.
• Click Add.
BoardDetailsPageViewModel
This serves as the view model for the BoardDetailsPage file you created.
namespace WidgetBoard.ViewModels;
You also need to jump over to the MauiProgram.cs file and register this
page with the Services inside the CreateMauiApp method as you did above.
builder.Services.AddTransient<BoardDetailsPageViewModel>();
108
CHAPTER 5 USER INTERFACE ESSENTIALS
You should start to notice a common pattern with the creation of these
files and the need to add them to the MauiProgram.cs file. This is to allow
you to fully utilize the dependency injection provided by the framework,
which you learned about in Chapter 3.
This concludes the prerequisite work required for this chapter, so let’s
proceed to covering the user interface essentials.
App Icons
Every application needs an icon, and for many people, this will be how
they obtain their first impression. Thankfully these days device screens
allow for bigger icon sizes and therefore more detail to be included
in them.
As with general image resources, each platform requires different sizes
and many more combinations to be provided. For example, iOS expects
the following:
• Five different sizes of the app icon
• Three different sizes for the Spotlight feature
109
CHAPTER 5 USER INTERFACE ESSENTIALS
If you look in the contents of your project file, you will see the
following entry:
This tells the tooling to use the file appicon.svg and convert it into all
the required sizes for each platform when building. Note you only want
one MauiIcon in your project file. If you have multiple, the first one will
be used.
110
CHAPTER 5 USER INTERFACE ESSENTIALS
You do not need to replace the above entry as the file you should
have downloaded should have the name appicon.svg. If the file name is
different, either rename it or update the name in the project file.
Platform Differences
It is worth noting that some platforms apply different rules to app icons
and also can provide rather different outputs.
Android
App icons on Android can take many different shapes due to the different
device manufacturers and their own flavor of the Android operating
system. To cater for this, Google introduced the adaptive icon. This allows
a developer to define two layers in their icon:
• The background: This is typically a single color or
consistent pattern. It is the appicon.svg file that you
downloaded.
• The foreground: This includes the main detail. It is the
appiconfig.svg file that you downloaded.
.NET MAUI allows you to support the adaptive icon using the
IncludeFile and the ForegroundFile properties on the MauiIcon
element. You can see the IncludeFile is already defined in your project.
This represents the background. You can split your application icon into
two parts and then provide the detail to the ForegroundFile. Note that this
can be applied to all platforms and is my recommended way to ship an
application icon.
111
CHAPTER 5 USER INTERFACE ESSENTIALS
Splash Screen
A splash screen is the first thing a user sees when they start your
application. It gives you as a developer a way of showing the user
something while the application is launching. Once everything has
finished loading, the splash screen will be hidden and your main page will
be shown.
In a similar manner to how the app icon is managed, the splash screen
also has an entry in the project file and can generate a screen based on an
SVG file. In fact, you will be using the same image to save effort.
<MauiSplashScreen Include="Resources\Splash\splash.svg"
Color="#512BD4" BaseSize="128,128" />
Note that splash screens built in this manner must be static. You can’t
have any animations running to show progress.
The Color property enables you to define a background color for the
splash screen.
I have designed a splash screen image that you are free to use in your
application, you can find a copy at https://github.com/bijington/
introducing-dotnet-maui/tree/main/chapter05/splash and place
them in the Resources/Splash folder.
112
CHAPTER 5 USER INTERFACE ESSENTIALS
XAML
As a .NET MAUI developer, you will hear XAML mentioned many times;
XAML stands for eXtensible Application Markup Language. It is an XML-
based language used for defining user interfaces. It originates from WPF
and Silverlight, but the .NET MAUI version has its differences.
There are two different types of XAML files that you will encounter
when building your application:
• A ResourceDictionary: This is a single file that
contains resources that can easily be used throughout
your application. Resources/Styles/Styles.xaml
is a perfect example of this. The Styles.xaml file is a
default set of styles that is provided when you create
a new .NET MAUI application. If you wish to modify
some built-in styling, this is a very good place to do so.
• A View-based file: This contains both a .xaml and
.xaml.cs file. They are paired together using the
partial class keyword.
113
CHAPTER 5 USER INTERFACE ESSENTIALS
When dealing with this second item, you have to make sure that the
InitializeComponent line is called inside the constructor; otherwise,
the XAML will not be interpreted correctly, and you will see an
exception thrown.
It is worth noting that XAML does not provide a rich set of features
like C# does, and for this reason, there is almost always a xaml.cs file that
goes alongside the XAML file. This C# file provides the ability to use the
rich feature set of the C# language when XAML does not. For example,
handling a button interaction event would have to be done within the C#
code file.
114
CHAPTER 5 USER INTERFACE ESSENTIALS
If you break this down into small chunks, you can start to understand
not only what makes up the UI of your application but also some of the
fundamentals of how XAML represents it.
The root element is a ContentPage. As mentioned, a typical view in
.NET MAUI is either a ContentPage or ContentView. As the name implies,
it is a page that presents its content, and this will be a single view as its
content.
As mentioned, XAML is an XML-based language, and there are the
following key parts to understanding XAML:
1. Properties are set by attributes on your element, so
new Label
{
Text = "Welcome to .NET MAUI!"
};
115
CHAPTER 5 USER INTERFACE ESSENTIALS
116
CHAPTER 5 USER INTERFACE ESSENTIALS
Now that you know what needs to be achieved, let’s go ahead and do
it. You need to delete the existing contents of the page and replace them
with a Border. A Border is similar to a ContentView in that it can only have
a single child, but it offers you some extra properties that allow you to
provide a nice looking UI. In particular, you care about the StrokeShape
and Stroke properties. You may notice that you are not actually setting
these properties in the XAML and you would be correct! There are two
main reasons for this:
• You have suitable defaults defined in the Resources/
Styles/Styles.xaml file that was created for you. Note
that if you want to override these, it’s perfectly fine. I
will be covering this a little bit later in this chapter in
the “Styling” section.
• It is considered good practice to only define the
properties that you need to supply, which is basically
anything that changes from the defaults. While the
XAML compiler does a decent job of generating a
UI that is defined at compile time, some bits are still
potentially interpreted at runtime and this has a
performance impact.
117
CHAPTER 5 USER INTERFACE ESSENTIALS
HorizontalOptions="Center"
VerticalOptions="Center"
Padding="0">
</Border>
</ContentPage>
The most important parts of the properties that you are setting are the
HorizontalOptions and VerticalOptions. They allow you to define where
in the parent this view will be displayed. By default, a view will fill its
parent’s content, but you are going to make it float in the center. The main
reason is so it will stay there regardless of the screen size it is running on.
Of course, there are more in-depth ways of handling different screen sizes
and you will explore them in the coming chapters.
While you have much more content to add to this XAML file, you are going
to do so in the context of the following topics. Your next step is to add multiple
child views. For this, you are going to need to choose a suitable Layout.
Layouts
.NET MAUI provides you with a set of prebuilt layout classes that allow you
to group and arrange views in your application. The aim of this section is to
explore each layout control and how it might be used for your application.
I strongly recommend playing around with each of the layouts to see what
will fit best for each individual use case and always remember to keep the
visual tree as simple as possible.
AbsoluteLayout
As the name suggests, the AbsoluteLayout allows the positioning of its
children with absolute values. The x, y, width, and height of a child are
controlled through the LayoutBounds attached property. This means you
use as follows
118
CHAPTER 5 USER INTERFACE ESSENTIALS
<AbsoluteLayout>
<Label
AbsoluteLayout.LayoutBounds="0,0,600,200"/>
</AbsoluteLayout>
<AbsoluteLayout>
<Label
AbsoluteLayout.LayoutBounds="0,0,0.5,0.2"
AbsoluteLayout.LayoutFlags="All"/>
</AbsoluteLayout>
This will result in the Label being positioned at 0,0, but the width will
be 50% of the AbsoluteLayout and the height will be 20%. This provides
a lot of power when defining a user interface that can grow as the size of a
device also increases.
119
CHAPTER 5 USER INTERFACE ESSENTIALS
The LayoutFlags option provides you with a lot of power. You can
choose which part of the LayoutBounds is applied absolutely and which is
applied proportionally. Here are the possible values for LayoutFlags and
what they impact:
Value Description
120
CHAPTER 5 USER INTERFACE ESSENTIALS
FlexLayout
The FlexLayout comes with a large number of properties to configure
how its children are positioned. If you want your controls to wrap, this is
the control for you! A good example for using the FlexLayout is a media
gallery.
Figure 5-4 shows how controls can be positioned inside a FlexLayout.
The above layout can be achieved with the following code example:
<FlexLayout
AlignItems="Start"
Wrap="Wrap"
Margin="30"
JustifyContent="SpaceEvenly">
<Border
BackgroundColor="LightGray"
WidthRequest="100"
HeightRequest="100" />
<Border
BackgroundColor="LightGray"
121
CHAPTER 5 USER INTERFACE ESSENTIALS
WidthRequest="100"
HeightRequest="100" />
<Border
BackgroundColor="LightGray"
WidthRequest="100"
HeightRequest="100" />
<Border
BackgroundColor="LightGray"
WidthRequest="100"
HeightRequest="100" />
</FlexLayout>
Each of the properties you are using allows you to customize where
each item is positioned during the rendering process and how it will
move around in the application if it is resized. For further information
on the possible ways of configuring the FlexLayout, read the Microsoft
documentation at https://learn.microsoft.com/dotnet/maui/user-
interface/layouts/flexlayout.
Your BoardDetailsPage only needs controls positioned vertically so a
FlexLayout feels like an overly complicated layout for this purpose.
Grid
I love Grids. They are usually my go-to layout option, mainly because
I have become used to thinking about how they lay out controls and
because they tend to allow you to keep your visual tree depth shallow.
The layout essentially works by allowing you to define a set of rows and
columns and then define which control should be displayed in which row/
column combination.
Figure 5-5 shows how controls can be positioned inside a Grid.
122
CHAPTER 5 USER INTERFACE ESSENTIALS
Controls inside a Grid are allowed to overlay each other, which can
provide an extra tool in a developer’s toolbelt when needing to show/
hide controls. Controls in the Grid are arranged by first defining the
ColumnDefinitions and RowDefinitions. Let’s take a look at how to create
the above layout with a Grid.
<Grid
ColumnDefinitions ="*,2*,250,Auto"
ColumnSpacing="20"
Margin="30"
RowDefinitions="*,*"
RowSpacing="20">
<Border
BackgroundColor="LightGray"
Grid.Column="0"
Grid.Row="0" />
<Border
BackgroundColor="LightGray"
Grid.Column="1"
123
CHAPTER 5 USER INTERFACE ESSENTIALS
Grid.Row="1" />
<Border
BackgroundColor="LightGray"
Grid.Column="2"
Grid.Row="0" />
<Border
BackgroundColor="LightGray"
Grid.Column="3"
Grid.Row="1"
WidthRequest="30"
HeightRequest="30" />
</Grid>
You can see that you have created columns using a variety of different
options:
124
CHAPTER 5 USER INTERFACE ESSENTIALS
HorizontalStackLayout
The name really gives this away. It positions its children horizontally.
The HorizontalStackLayout is not responsible for providing sizing
information to its children, so the children are responsible for calculating
their own size.
Figure 5-6 shows how controls can be positioned inside a
HorizontalStackLayout.
The above layout can be achieved with the following code example:
<HorizontalStackLayout
Spacing="20"
Margin="30">
<Border
BackgroundColor="LightGray"
WidthRequest="100" />
<Border
BackgroundColor="LightGray"
WidthRequest="100" />
<Border
125
CHAPTER 5 USER INTERFACE ESSENTIALS
BackgroundColor="LightGray"
WidthRequest="100" />
</HorizontalStackLayout>
You wish to layout your controls vertically so you can guess where this
is going, although you will actually use one to group some of your inner
controls.
VerticalStackLayout
The name really gives this away. It positions its children vertically.
The VerticalStackLayout follows the same sizing rules as the
HorizontalStackLayout, so the children are responsible for calculating
their own size.
And there you have it: something that arranges its children vertically,
which is exactly what you need!
Figure 5-7 shows how controls can be positioned inside a
VerticalStackLayout.
126
CHAPTER 5 USER INTERFACE ESSENTIALS
The above layout can be achieved with the following code example:
<VerticalStackLayout
Spacing="20"
Margin="30">
<Border
BackgroundColor="LightGray"
HeightRequest="100" />
<Border
BackgroundColor="LightGray"
HeightRequest="100" />
<Border
BackgroundColor="LightGray"
HeightRequest="100" />
</VerticalStackLayout>
We mentioned that this is the layout that you will want to use in your
page; let’s go ahead and use it. Inside the Border you added earlier, add the
following to your BoardDetailsPage.xaml file.
<VerticalStackLayout>
<VerticalStackLayout
Padding="20">
<Label
Text="Name"
FontAttributes="Bold" />
<Entry />
<Label
Text="Layout"
FontAttributes="Bold" />
<HorizontalStackLayout>
<RadioButton
x:Name="FixedRadioButton"
127
CHAPTER 5 USER INTERFACE ESSENTIALS
Content="Fixed" />
</HorizontalStackLayout>
<VerticalStackLayout>
<Label
Text="Number of Columns"
FontAttributes="Bold" />
<Entry Keyboard="Numeric" />
<Label
Text="Number of Rows"
FontAttributes="Bold" />
<Entry Keyboard="Numeric" />
</VerticalStackLayout>
</VerticalStackLayout>
<Button
Text="Save"
HorizontalOptions="End" />
</VerticalStackLayout>
Yes, I know! I spoke about keeping the visual tree simple and here
you are nesting quite a few layouts. I find there is typically some level of
pragmatism that needs to be applied. This page is still relatively simple in
terms of what is being rendered on screen so I will argue that it is fine. If
you were to repeat this layout multiple times, you would need to be a little
more strict and find the best way to lay it all out. Quite often you will find
that there can be a balancing act between defining something to give the
best performance and making it easier to maintain as a developer.
So you have now built your UI, but you will notice that it doesn’t do
anything other than let the user type in the entry fields. You need to bind
the view up to your view model.
128
CHAPTER 5 USER INTERFACE ESSENTIALS
This is not strictly part of layouts, but it is worth noting how you apply
the Keyboard property to your Entry controls. This allows you to inform
the operating system what soft keyboard to display and therefore limit
the type of data the user can enter. Note that this only applies to mobile
applications and it only really helps if a hardware keyboard is not used; if a
user does connect a hardware keyboard, they will be able to enter invalid
characters; therefore, it will still be up to us as developers to validate that
the correct data has been entered. We will cover how to validate data in a
reusable way in Chapter 9.
Data Binding
UI-based applications, as their name suggests, involve presenting
an interface to the users. This UI is rarely ever just a static view and
therefore needs to be updated, drive updates into the application, or
both. This process is typically an event-driven one as either side of this
synchronization needs to be notified when the other side changes. .NET
MAUI wraps this process up for you through a concept called data binding.
Data binding provides the ability to link the properties from two objects so
that changes in one property are automatically updated in the second.
Binding
The most common type of bindings that you create is between a single
value at the source and a single value at the target. The target is the owner
of the bindable property. I use the terms target and source because you
do not have to solely bind between a view and a view model. There are
scenarios where you may wish to bind one control to another.
Before you jump into creating your first binding, you need to first
create something to bind to. Open your BoardDetailsPageViewModel
class, which is the view model for your view, and add the following:
129
CHAPTER 5 USER INTERFACE ESSENTIALS
BindingContext
And finally the crucial step is to set the BindingContext of your page to this
view model. In Chapter 4, you did this by setting it in the XAML directly,
but because you have registered your view model with the dependency
injection layer, you can make the most of that and have it create the
view model and whatever dependencies it has for you. Open your
BoardDetailsPage.xaml.cs file and change the constructor to
public BoardDetailsPage(BoardDetailsPageViewModel
boardDetailsPageViewModel)
{
InitializeComponent();
BindingContext = boardDetailsPageViewModel;
}
130
CHAPTER 5 USER INTERFACE ESSENTIALS
Now if you jump into the BoardDetailsPage.xaml file, you can apply
the binding to your new BoardName property in your view model. You want
to modify the first Entry that you added to look like
This is a relatively small change and will look like the bindings you
created back in Chapter 4 when exploring the MVVM pattern. There isn’t
much detail to this, but there is a fair amount of implicit behavior that I feel
I must highlight. Let’s cover what it tells you first and then what it doesn’t.
You are creating a binding between the BoardName property (which
exists on your BoardDetailsPageViewModel) and the Text property on the
Entry control.
Now on to what this code doesn’t tell you.
Path
The binding could also be written as
Text="{Binding Path=BoardName}"
Mode
I mentioned that bindings keep two properties in sync with each other.
When you create a binding, you can define which direction the updates
flow. In your example, you have not provided one, which then relies on
131
CHAPTER 5 USER INTERFACE ESSENTIALS
the default Mode for the bindable property that you are binding to. In this
case, it is the Text property of the Entry, which has a default binding
mode of TwoWay. I strongly urge you to make sure you are aware of both
these defaults and your expectation when creating a binding. Choosing
the correct Mode can also boost performance. For example, the OneTime
binding mode means that no updates need to be monitored for. In your
scenario, you don’t currently need to allow the view model to update the
Entry Text property; however, as you progress, this page will also allow
for the editing of a board so you will leave it alone. If you didn’t need
to edit, you could in theory modify your binding to be Text="{Binding
Path=BoardName, Mode=OneWay}".
There are several variations for binding modes:
• Default: As the name suggests, it uses the default,
which is defined in the target property.
• TwoWay: It allows for updates to flow both ways
between source and target. A typical example is
binding to the Text property of an Entry where you
want to both receive input from the user and update
the UI, such as your scenario that you just added with
the Entry and its Text property as Text="{Binding
Path=BoardName}".
• OneWay: It allows for updates to flow from the source
to the target. An example of this is your ClockWidget
where you only want updates to flow from your source
to your target.
132
CHAPTER 5 USER INTERFACE ESSENTIALS
Source
As mentioned, a binding does not have to be created against something
defined in your code (e.g., a property on a view model). It can, in fact, be
created against another control. If you look back at the XAML you created
for this page, you will notice that you gave the RadioButton the name of
FixedRadioButton. This was actually setting you up for this moment: you
can now bind your innermost VerticalStackLayouts visibility to the value
of this RadioButton.
<VerticalStackLayout
IsVisible="{Binding IsChecked, Source={x:Reference
FixedRadioButton}}">
133
CHAPTER 5 USER INTERFACE ESSENTIALS
<RadioButton
Content="Fixed"
134
CHAPTER 5 USER INTERFACE ESSENTIALS
x:Name="FixedRadioButton"
IsChecked="{Binding IsFixed}" />
<Entry
Text="{Binding NumberOfColumns}"
Keyboard="Numeric" />
And finally change the Entry that follows that to be
<Entry
Text="{Binding NumberOfRows}"
Keyboard="Numeric" />
MultiBinding
There can be occasions when you wish to bind multiple source properties
to a single target property in a view. To take a minor detour, let’s rework
your ClockWidgetViewModel to have two properties: one with the date and
one with the time. You should end up with the following code (the bold
highlights the new parts):
namespace WidgetBoard.ViewModels;
public class ClockWidgetViewModel : ViewModelBase
{
private readonly Scheduler scheduler = new();
private DateOnly date;
private TimeOnly time;
public ClockWidgetViewModel()
{
SetTime(DateTime.Now);
}
public DateOnly Date
{
135
CHAPTER 5 USER INTERFACE ESSENTIALS
136
CHAPTER 5 USER INTERFACE ESSENTIALS
<Label
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:WidgetBoard.ViewModels"
x:Class="WidgetBoard.Views.ClockWidgetView"
FontSize="80"
VerticalOptions="Center"
HorizontalOptions="Center">
<Label.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="Date" />
<Binding Path="Time" />
</MultiBinding>
</Label.Text>
</Label>
<Label.Text>
<Binding Path="Time" />
</Label.Text>
137
CHAPTER 5 USER INTERFACE ESSENTIALS
Command
Very often you will need your applications to respond to user interaction.
This can be by tapping or clicking on a button or selecting something
in a list. This interaction is recorded in your view, but you usually
require that the logic to handle this interaction be performed in the view
model. This comes in the form of a Command and an optional associated
CommandParameter set of properties. A command works in a similar way to
an event; you can provide a method that will be executed when an event
happens; commands are suited to the MVVM architecture because it
enables you to bind the command to an instance in the view model, which
is where you want your business logic to reside. The Command property
itself can be bound from the view to the view model and allows the view
model to not only handle the interaction but also to determine whether
the interaction can be performed in the first place. You already added a
Button to your BoardDetailsPage.xaml file but you didn’t hook it, so let’s
do exactly that!
You just need to modify your button to be (changes in bold)
<Button
Text="Save"
HorizontalOptions="End"
Command="{Binding SaveCommand}" />
Based on the binding content that you have explored, you can say that
this Buttons Command property is now bound to a property on your view
model called SaveCommand. You haven’t actually created this property
yet. If you are thinking it would be great if the tooling could know this
138
CHAPTER 5 USER INTERFACE ESSENTIALS
and report it to me, then the next section has got you covered. “Compiled
Bindings” will show you how to inform the tooling of how to report it to
you. First, though, open your BoardDetailsPageViewModel.cs file and add
your command implementation.
Your implementation comes in multiple parts.
public BoardDetailsPageViewModel()
{
SaveCommand = new Command(
() => Save(),
() => !string.IsNullOrWhiteSpace(BoardName));
}
private void Save()
{
var board = new Board
{
139
CHAPTER 5 USER INTERFACE ESSENTIALS
Name = BoardName,
NumberOfColumns = NumberOfColumns,
NumberOfRows = NumberOfRows
};
}
140
CHAPTER 5 USER INTERFACE ESSENTIALS
This change means that every time the BoardName property changes
(and this will be done via the binding from the view), the Button that is
bound to the SaveCommand will re-query to check whether the command
can be executed. If it can, the Button will be enabled and the user can
interact with it; if not, it will be disabled.
Compiled Bindings
Compiled bindings are a great feature that you should in almost all cases
turn on! They help to speed up your applications because they help the
compiler know what the bindings will be set to and reduce the amount of
reflection that is required. Reflection is notoriously bad for performance
so wherever possible it is highly recommended to avoid using it. Bindings
by default do use an amount of reflection in order to handle the value
changes between source and target. Compiled bindings, as just discussed,
help to reduce this, so let’s learn how to turn them on.
Compiled bindings also provide design-time validation. If you set a
binding to a property on your view model that doesn’t exist (imagine you
made a typo, which I do a lot!), without compiled bindings, the application
would still build but your binding won’t do anything. With a compiled
binding, the application will fail to build and the tooling will report that the
property you mistyped doesn’t exist.
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:WidgetBoard.ViewModels"
x:Class="WidgetBoard.Pages.BoardDetailsPage"
x:DataType="viewModels:BoardDetailsPageViewModel">
Now that you have set up your BoardDetailsPage to allow user entry
and even perform an action when the Save button is interacted with, you
need to structure your application so that you can see this happen.
141
CHAPTER 5 USER INTERFACE ESSENTIALS
Note that since .NET 9.0, you will see warnings reported if you do not
use compiled bindings; this was implemented by the team at Microsoft in
an effort to make sure developers are making the most of the performance
and compile time safety that they offer.
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate
pages:BoardDetailsPage}" />
</Shell>
142
CHAPTER 5 USER INTERFACE ESSENTIALS
It is worth noting that the Save button will not do anything just yet.
Adding the handling of this button will be the topic of the next chapter
when we dig deep into Shell and how to allow users to navigate around our
applications.
143
CHAPTER 5 USER INTERFACE ESSENTIALS
Summary
In this chapter, you have
• Created and applied an icon for your application
• Added some placeholder pages and view models
• Filled your first page with some UI and setup bindings
to the view model
• Covered data binding and its many uses
• Gained an understanding of XAML
• Learned about the possible layouts you can use to
group other controls
In the next chapter, you will
• Gain an understanding of Shell and apply this to
building your application’s structure
• Apply the Shell navigation to allow you to navigate to
your next page and the next chapter
• Make use of Shell tabs and search functionality
• Build your flyout menu using all the learnings in
this chapter
• Add tabs into the application
• Add the ability to search for tabs
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch05.
144
CHAPTER 6
Shell
Abstract
In this chapter, you are going to learn how to define the visual hierarchy of
your .NET MAUI application and handle common concepts like navigation
and search functionality – all through a concept called Shell.
Prerequisites
You need to do some setup before you can jump into using Shell. If Shell
is still feeling like an unknown concept, fear not; we will be covering it in
depth within this chapter.
Let’s go ahead and add the following folders to your project.
Pages
Let’s go ahead and create the following files under the Pages folder. The
following steps show how to add the new pages:
BoardListPage
This is the page that will render a list of boards that users will create within
your application. For now, you will not touch the contents of this file. Note
that you should see BoardListPage.xaml and BoardListPage.xaml.cs
files created.
You will also need to jump over to the MauiProgram.cs file and register
this page with the Services inside the CreateMauiApp method.
builder.Services.AddTransient<BoardListPage>();
FixedBoardPage
This is the page that will render the boards you create in the page created
in the previous chapter. For now, you will not touch the contents of this
file. Note that you should see FixedBoardPage.xaml and FixedBoardPage.
xaml.cs files created.
You will also need to jump over to the MauiProgram.cs file and register
this page with the Services inside the CreateMauiApp method.
builder.Services.AddTransient<FixedBoardPage>();
SettingsPage
This is the page that will render any settings that the user can modify,
for example, how frequently to refresh the widgets. For now, you will not
touch the contents of this file. Note that you should see SettingsPage.
xaml and SettingsPage.xaml.cs files created.
146
Chapter 6 Shell
You will also need to jump over to the MauiProgram.cs file and register
this page with the Services inside the CreateMauiApp method.
builder.Services.AddTransient<SettingsPage>();
ViewModels
This houses your ViewModels that are the backing for both your Pages and
Views. You created this folder in the previous chapter, but you need to add
a number of classes. The following steps show how to add the new pages:
• Right-click the ViewModels folder.
AppShellViewModel
This serves as the view model for the AppShell file that is created for you
by the tooling.
namespace WidgetBoard.ViewModels;
You also need to jump over to the MauiProgram.cs file and register this
page with the Services inside the CreateMauiApp method.
builder.Services.AddTransient<AppShellViewModel>();
147
Chapter 6 Shell
BoardListPageViewModel
This serves as the view model for the BoardListPage file that will be
responsible for displaying all available boards within the application to
the user.
namespace WidgetBoard.ViewModels;
You also need to jump over to the MauiProgram.cs file and register this
page with the Services inside the CreateMauiApp method.
builder.Services.AddTransient<BoardListPageViewModel>();
FixedBoardPageViewModel
This serves as the view model for the FixedBoardPage file you created.
namespace WidgetBoard.ViewModels;
You also need to jump over to the MauiProgram.cs file and register this
page with the Services inside the CreateMauiApp method.
builder.Services.AddTransient<FixedBoardPageViewModel>();
You should have noticed a common pattern with the creation of these
files and the need to add them to the MauiProgram.cs file. This is to allow
you to fully utilize the dependency injection provided by the framework,
which you learned about in Chapter 3.
148
Chapter 6 Shell
SettingsPageViewModel
This serves as the view model for the SettingsPage file you created.
namespace WidgetBoard.ViewModels;
You also need to jump over to the MauiProgram.cs file and register this
page with the Services inside the CreateMauiApp method.
builder.Services.AddTransient<SettingsPageViewModel>();
With that concluding the prerequisites required for this chapter, let’s
proceed onto learning all about Shell and how we can define the structure
of .NET MAUI applications.
Shell
Shell in .NET MAUI enables you to define how your application will be
laid out, not in terms of actual visuals but by defining things like whether
you want your pages viewed in tabs or just a single page at a time. It also
enables you to define a flyout, which is a side menu in your application.
You can choose to have it always visible or toggle it to slide in/out, and this
can also vary based on the type of device you are running on. Typically
a desktop has more visual real estate, so you may wish to keep the flyout
always open then.
For your application, you are going to make use of the flyout to allow
you to define multiple boards that you can configure and load. I really
like the idea of having one board for when I work and then swapping to
something else when working on a side project or even for gaming.
149
Chapter 6 Shell
To save having to return to this area and change bits, you are going to
jump straight into the more in-depth option and feature-rich outcome.
Don’t worry, though; as you discover each new concept, you will dive into
some detail to cover what it is and why you are using it along with then
applying that concept to your application.
ShellContent
If you take a look at your AppShell.xaml file, you should see very little
inside. Currently it has the following line:
<ShellContent
ContentTemplate="{DataTemplate pages:BoardDetailsPage}" />
You will recall that in the previous chapter we modified the contents to
the above in order to show our progress when running the application. We
didn’t dig into the details of the change in order to keep that detail within
the Shell chapter, so let’s explore what it means.
Your application’s main content will now be an instance of your
recently created BoardDetailsPage. You don’t need the Title or Route
options anymore as you will be controlling them in different ways.
The Title property will be set based on the page that is shown, so you
will learn about this a little later on.
The Route property you will control as part of the next section,
“Navigation.”
Finally, you added xmlns:pages="clr-namespace:WidgetBoard.
Pages" to the top of the file in order to be able to refer to the
BoardDetailsPage.
150
Chapter 6 Shell
Navigation
I am personally a fan of simplifying the code I write so long as it continues
to make it easy to read. With this in mind, I would like to suggest you
improve on the registration of your pages and their view models already.
151
Chapter 6 Shell
services.AddTransient<BoardDetailsPage>()
services.AddTransient<BoardDetailsPageViewModel>()
Routing.RegisterRoute(route, typeof(TPage));
AddPage<BoardDetailsPage, BoardDetailsPageViewModel>(builder.
Services, "boarddetails");
with the added change that you now define this route. So let’s go and
delete your old registrations and replace with
AddPage<BoardDetailsPage, BoardDetailsPageViewModel>(
builder.Services, RouteNames.BoardDetails);
AddPage<BoardListPage, BoardListPageViewModel>(
builder.Services, RouteNames.BoardList);
AddPage<FixedBoardPage, FixedBoardPageViewModel>(
builder.Services, RouteNames.FixedBoard);
AddPage<SettingsPage, SettingsPageViewModel>(
builder.Services, RouteNames.Settings);
namespace WidgetBoard;
152
Chapter 6 Shell
This means you can save one line of code per page and view model
pair that you had registered as well as the code to register the route for
navigation. The added benefit of introducing the RouteNames class means
that you reduce the risk of a typo being introduced because the string only
needs to be defined in a single place. In fact, this means that even if there is
a typo the code will still likely work because the typo will apply everywhere
it is used within the app.
Now that you have registered your pages, let’s take a look at how you
can actually perform navigation.
Performing Navigation
There are multiple ways to specify the route for navigation, but they all use
the Shell.Current.GoToAsync method.
So, for example, you could navigate to your FixedBoardPage with the
following:
await Shell.Current.GoToAsync(RouteNames.FixedBoard);
153
Chapter 6 Shell
This will result in a FixedBoardPage being created and pushed onto the
navigation stack. This is precisely the behavior that you need at the end of
your SaveCommand execution in your BoardDetailsPagesViewModel class.
Navigating Backward
You can also pop pages off the navigation stack by navigating backward.
This can be achieved by the following:
await Shell.Current.GoToAsync("..");
await Shell.Current.GoToAsync($"../{RouteNames.BoardList}");
await Shell.Current.GoToAsync($"{RouteNames.
FixedBoard}?boardid=1234");
154
Chapter 6 Shell
await Shell.Current.GoToAsync(
RouteNames.FixedBoard,
new Dictionary<string, object>
{
{ "Board", board }
});
You can also send a complex object like the above, which means the
originating page (or page view model) is responsible for retrieving or
constructing the board and you send the whole thing to the receiving page.
There are two main ways to handle sending complex data when
navigating with Shell. Let’s take a look at each in turn.
IQueryAttributable
To receive data, you can implement the IQueryAttributable interface
provided with .NET MAUI. Shell will either call this on the page you are
navigating to, or if the BindingContext (your view model) implements the
interface, it will call it there. Add this to your FixedBoardPageViewModel
class because you are going to need to process the data. You will be going
with the complex object option because you have already loaded the Board
in your AppShellViewModel class.
155
Chapter 6 Shell
You aren’t going to do anything with this data just yet, but it is ready
for when you start to build your board layout view in the next chapter.
For now, you will continue on with the theme of Shell and define your
flyout menu.
You will also need to make your FixedBoardPageViewModel implement
the IQueryAttributable interface. Change the class definition from
Note that you will also need to add the following using statement to the
top of your FixedBoardPageViewModel.cs file:
using WidgetBoard.Models;
QueryProperty
An alternative to using the IQueryAttributable interface is to make use
of the QueryProperty in your receiving class. Making use of the same
example from the “Passing Data When Navigating” section, we could
(but we won’t so don’t worry to apply any of these changes) change the
FixedBoardPageViewModel class to the following:
using WidgetBoard.Models;
namespace WidgetBoard.ViewModels;
[QueryProperty(nameof(CurrentBoard), "Board")]
public class FixedBoardPageViewModel : BaseViewModel
{
public Board CurrentBoard { get; set; }
}
156
Chapter 6 Shell
You can see from the changes above in bold that we have created a
property called CurrentBoard and then added the QueryProperty attribute
to the class. This attribute instructs Shell to set the CurrentBoard property
(first parameter) when a value is received in the query string with the key
of “Board”.
The main reason why I prefer IQueryAttributable over
QueryProperty is that .NET MAUI will call the method for us during
navigation; if we wanted to handle the navigation in our view model
without this interface implementation, we would have to add additional
boilerplate code to do so.
Let’s proceed to learning about the next Shell feature in order to
connect all the dots and have a working application with navigation by the
end of this chapter.
Flyout
A flyout is a menu for a Shell application that is accessible through an
icon or by swiping from the side of the screen. The flyout can consist of an
optional header, flyout items, optional menu items, and an optional footer.
For your application, you are going to provide a basic header, and then
the main content will be a dynamic list of all the boards your user creates.
This means that you are going to have to override the main content, but
thankfully Shell makes this an easy task.
The first thing I like to do when working on a new XAML file is to turn
on compiled bindings, which I covered earlier. If you recall, this is by
specifying the x:DataType attribute to tell the compiler the type that your
view will be binding to. Let’s do that now; first, open up the AppShell.xaml
file and make the following changes (in bold):
157
Chapter 6 Shell
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:DataType="viewmodels:AppShellViewModel"
Shell.FlyoutBehavior="Flyout">
This helps you as you build the view to see what doesn’t exist in your
view model. Of course, if you prefer to build the view model first, then this
also helps.
Finally, you need to add xmlns:viewModels="clr-
namespace:WidgetBoard.ViewModels" to the top of the file.
Now we want to proceed to defining how our Flyout menu will
be presented. This can be customized with the FlyoutHeader and
FlyoutContent, so let’s take a look at each one in turn.
FlyoutHeader
The FlyoutHeader can be given any control or layout, and therefore, you
can build a really good-looking header option. For your application, you
are just going to add a title Label.
Below your ShellContent element, you want to add the following:
<Shell.FlyoutHeader>
<Label
Text="My boards"
FontSize="20"
HorizontalTextAlignment="Center" />
</Shell.FlyoutHeader>
Hopefully the above is self-explanatory, but to cover the parts I
haven’t already covered, you have the ability to specify different layout
information in a Label so you can make the text centered. It is usually
recommended that you use the HorizontalOptions property over the
HorizontalTextAlignment property for performance reasons; however, if
you try that here, you will see that it doesn’t center the Label.
Now let’s add in the main part of your menu.
158
Chapter 6 Shell
FlyoutContent
First, if you want to use a static set of items in your menu, you can simply
add FlyoutItems to the content. This can work well when you have a fixed
set of pages such as Settings, Home, and so on. You will be showing the
boards that the user creates, so you will need something dynamic. For this,
you need to supply the FlyoutContent. More importantly, it’s your first
introduction to the CollectionView control.
The CollectionView allows you to define how an item will look and
then have it repeated for each item in a collection that is bound to it.
Additionally, the CollectionView provides the ability to allow the user
to select items in the collection, and you can define behavior that will
be performed when that selection happens. Let’s add the following to
your Shell:
<Shell.FlyoutContent>
<CollectionView
ItemsSource="{Binding Boards}"
SelectionMode="Single"
SelectedItem="{Binding CurrentBoard}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Board">
<Label
Text="{Binding Name}"
FontSize="20"
Padding="10,0,0,0" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Shell.FlyoutContent>
159
Chapter 6 Shell
If we deconstruct the XAML that was just added, we can make the
following statements. Your FlyoutContent will display a collection of
items; each item will be presented as a Label set to the Name of each item.
The items will be Board instances in the collection of Boards in your view
model. Additionally, the CurrentBoard property on your view model will
be updated when the user selects one of the Labels in this collection.
If you have added all of the parts I have discussed, you will likely
notice that the tooling is reporting that you haven’t added the Boards or
CurrentBoard properties that you are binding to over in your view model.
Let’s jump over to your AppShellViewModel.cs file and add the following.
Collection of Boards
public ObservableCollection<Board> Boards { get; } = [];
public AppShellViewModel()
{
Boards.Add(
new Board
{
Name = "My first board",
160
Chapter 6 Shell
NumberOfColumns = 3,
NumberOfRows = 2
});
}
Note that you will also need to add the following using statements to
the top of your file:
using System.Collections.ObjectModel;
using WidgetBoard.Models;
Selected Board
You bound the SelectedItem property from the CollectionView to your
CurrentBoard property. When your property changes, you can navigate to
the board that was selected.
161
Chapter 6 Shell
Before your bindings will work, you need to make some further
changes.
Note that you will also need to add the following using statement to
the top of your file:
using WidgetBoard.ViewModels;
162
Chapter 6 Shell
builder.Services.AddTransient<AppShell>();
namespace WidgetBoard;
163
Chapter 6 Shell
All of the above changes allow you to use AppShell just like any other
page and not have to create an instance manually.
Or you can slide out the menu from the left-hand side. Figure 6-2
shows the flyout menu in your application.
164
Chapter 6 Shell
165
Chapter 6 Shell
Tabs
Shell offers many different ways to build the structure of your application;
if a Flyout menu doesn’t fit your application, then you might opt to use
tabs instead, or even in combination. We are going to do the latter to show
how you can also make use of tabs.
You will have noticed that when the application was first run, we saw
the BoardDetailsPage which lets a user create a new board. While this
might be useful on the first ever use of the application, it is not likely to
be a common place where a user will want to land in our application. For
this, we are going to make two key changes: introduce tabs and change the
landing page for our users.
Let’s first open up the AppShell.xaml file and make the
following changes
166
Chapter 6 Shell
<ShellContent
ContentTemplate="{DataTemplate pages:BoardDetailsPage}" />
<TabBar>
<Tab Title="Boards">
<ShellContent ContentTemplate="{DataTemplate pages:
BoardListPage}" />
</Tab>
<Tab Title="Settings">
<ShellContent ContentTemplate="{DataTemplate pages:
SettingsPage}" />
</Tab>
</TabBar>
This now means that we will see a tab bar at the bottom of the
application; the first tab is labelled Boards and will present the
BoardListPage, and the second tab will be called Settings and present the
SettingsPage. We won’t add any content to the SettingsPage yet as that
will be the subject of future chapters; we added two tabs now to highlight
that Shell will only present the tab bar if there is more than one tab within
the bar.
We will now apply the same approach that was added to displaying the
user’s boards in the flyout menu. First, let’s open up the BoardListPage.
xaml file and make the following changes.
Modify the ContentPage element to look as follows (changes in bold):
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:WidgetBoard.ViewModels"
167
Chapter 6 Shell
xmlns:models="clr-namespace:WidgetBoard.Models"
x:Class="WidgetBoard.Pages.BoardListPage"
x:DataType="viewModels:BoardListPageViewModel"
Title="My boards">
<CollectionView
ItemsSource="{Binding Boards}"
SelectionMode="Single"
SelectedItem="{Binding CurrentBoard}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Board">
<Label
Text="{Binding Name}"
FontSize="20"
Padding="10,0,0,0" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Collection of Boards
Add a collection of boards and populate it.
public BoardListPageViewModel()
{
Boards.Add(
168
Chapter 6 Shell
new Board
{
Name = "My first board",
NumberOfColumns = 3,
NumberOfRows = 2
});
}
Note that you will also need to add the following using statements to
the top of your file:
using System.Collections.ObjectModel;
using WidgetBoard.Models;
Selected Board
You bound the SelectedItem property from the CollectionView to your
CurrentBoard property. When your property changes, you can navigate to
the board that was selected.
169
Chapter 6 Shell
Before your bindings will work, you need to make some further
changes.
public BoardListPage(BoardListPageViewModel
boardListPageViewModel)
{
InitializeComponent();
BindingContext = boardListPageViewModel;
}
Note that you will also need to add the following using statement to
the top of your file:
using WidgetBoard.ViewModels;
170
Chapter 6 Shell
This concludes the changes to add a set of tabs to our application. Let’s
have a look at how it presents.
One final feature to cover in this chapter is the ability to provide the
user the ability to search for boards.
171
Chapter 6 Shell
Search
Shell allows you to create your own SearchHandler, which means you can
define how the results are met with the values entered in the search box
that is automatically provided. If we imagine that the user has created a
lot of boards, they will need a quick way to find the board that they wish to
display.
Let’s give the users the ability to search for their boards in the
application. First, we need to create our SearchHandler implementation.
Let’s do this by adding a new class to the root of the project.
• Right-click the WidgetBoard project.
• Select Add ➤ New Class.
172
Chapter 6 Shell
new Board
{
Name = "My third board",
}
];
This gives us a list of three boards that we will be able to search against.
if (string.IsNullOrWhiteSpace(newValue))
{
ItemsSource = null;
}
else
{
ItemsSource = boards
173
Chapter 6 Shell
await Shell.Current.GoToAsync(
RouteNames.FixedBoard,
new Dictionary<string, object>
{
{ "Board", (Board)item}
});
}
174
Chapter 6 Shell
We will wait for one second to allow for the Shell navigation to finish
before we then navigate to the FixedBoardPage. The navigation code
should look very similar to the other navigation code that we added
throughout this chapter.
<Shell.SearchHandler>
<widgetBoard:BoardSearchHandler
Placeholder="Enter board name"
ShowsResults="True">
<SearchHandler.ItemTemplate>
<DataTemplate x:DataType="models:Board">
<Label
Text="{Binding Name}"
FontSize="20"
Padding="10,0,0,0" />
</DataTemplate>
</SearchHandler.ItemTemplate>
</widgetBoard:BoardSearchHandler>
</Shell.SearchHandler>
175
Chapter 6 Shell
Note that you will also need to add the namespace xmlns:widget
Board="clr-namespace:WidgetBoard" into the ContentPage element.
This concludes the changes for searching and also this chapter; let’s
proceed to running the application for a final time.
176
Chapter 6 Shell
The user will be presented with search results based on the text
entered within the search box. Figure 6-6 shows the application matching
the entered text of “ir” to both “My first board” and “My third board”.
ToolbarItems
This feature might not strictly belong to Shell, but it fits into the shell of
the application. We have already added a search bar to the title bar of
our application; we can also add buttons to that bar in order to provide
the user with quick ways of achieving tasks. We have the perfect scenario
in our application – there is currently no way to add a board, which will
become a little frustrating for users if we don’t fix that.
177
Chapter 6 Shell
The changes in this section will actually teach us three new concepts:
how to add buttons onto the title bar, how to present a page without
navigating to it, and how to show a page and wait for a result to be
returned. Let’s proceed to doing this.
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Command="{Binding AddBoardCommand}" />
</ContentPage.ToolbarItems>
You can see that we have added a single ToolbarItem into the
ToolbarItems collection. Our item has the Text of Add; you could also add
an image icon if you wanted to. Finally, we set the Command property by
binding it to a property called AddBoardCommand on the view model behind
this page. Based on that last part, we now need to add that property to the
view model; let’s do that now.
Open up the BoardListPageViewModel.cs file and make the following
changes.
Introduce the AddBoardCommand property.
178
Chapter 6 Shell
This means that the OnAddBoard method will be executed when the
button is interacted with.
Add the method that will be executed when the AddBoardCommand is
executed.
This doesn’t do anything new just yet; it will navigate the user to the
BoardDetailsPage because we registered the BoardDetails route to that
page in our MauiProgram.cs file earlier on in the book. One thing I would
like to highlight is that we are awaiting the call to GoToAsync; this means
that the application will only wait for the page to be navigated to and then
continue executing. This behavior is not quite what we want – we want to
show a page and have it return the board that was created so we can add it
to the Boards property and have it presented to the user. This leads us onto
the next new concept.
179
Chapter 6 Shell
our scenario of showing a page to create something and then have it close.
Shell provides us with the PresentationMode property for just these types
of scenarios.
Let’s proceed to making use of this PresentationMode property and
customize our BoardDetailsPage.xaml file. Open the file and make the
following changes.
Add the PresentationMode property.
Modify the ContentPage element to look as follows (changes in bold):
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:WidgetBoard.ViewModels"
x:Class="WidgetBoard.Pages.BoardDetailsPage"
Shell.PresentationMode="ModalAnimated"
x:DataType="viewModels:BoardDetailsPageViewModel">
<Button
Text="Save"
HorizontalOptions="End"
Command="{Binding SaveCommand}" />
180
Chapter 6 Shell
<Grid ColumnDefinitions="*,*,*">
<Button
Text="Cancel"
Command="{Binding CancelCommand}" />
<Button
Text="Save"
Grid.Column="2"
Command="{Binding SaveCommand}" />
</Grid>
The above layout means that we will have three equal spaced columns
in our Grid with the cancel Button filling the first column and the save
Button filling the third column. We won’t add in the CancelCommand to the
view model just yet; we will save it for the next section.
Show a page and wait for a result.
This can be a common scenario in applications with multiple screens,
and in our application, it is perfect! We want to show a page to allow a user
to create a new board and then return to the screen that shows the list
of boards.
The previous sections all led up to this point! We have two final
changes to make in order to complete the ability to add a new board into
the application.
Wait for a result to be returned from a ContentPage.
In order to do this, you need to open the BoardListPageViewModel.
cs file that you modified earlier and update the OnAddBoard method to the
following (with changes in bold):
181
Chapter 6 Shell
182
Chapter 6 Shell
This concludes the first change; now let’s proceed to providing a result
back from the BoardDetailsPage.
[QueryProperty(nameof(BoardCreatedCompletionSource), "Created")]
public class BoardDetailsPageViewModel : BaseViewModel
BoardCreatedCompletionSource?.SetResult(null);
});
183
Chapter 6 Shell
Shell.Current.GoToAsync("..");
BoardCreatedCompletionSource?.SetResult(board);
}
The first new line means that the current page will be hidden, and the
second line means that the newly created board will be returned to the
OnAddBoard method.
When you click the Add button, the application will present the
BoardDetailsPage. Figure 6-8 shows the application presenting the ability
to create a board by supplying a name, number of columns, and number
of rows.
185
Chapter 6 Shell
Finally, when the Save button is pressed, the user is then returned to
the list of boards with the new board added to the list. Figure 6-9 shows
the application presenting a list of boards including the new board named
“Result”.
186
Chapter 6 Shell
This concludes our chapter on Shell. I really hope each of the carefully
crafted examples shows how you can achieve a variety of different
scenarios.
Summary
It is worth stating that anything you do with Shell is built out of
components in the .NET MAUI box. Shell puts them together in an
opinionated way, but you can use all of those things separately, outside of
Shell as well if that’s what you want.
187
Chapter 6 Shell
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch06.
188
Chapter 6 Shell
Extra Assignment
This extra assignment is a culmination of the last two chapters combined. I
would like you to consider how you might add a second layout type (e.g., a
board where widgets could be placed anywhere) given that you
• Have a single layout type on your BoardDetailsPage
• Have options displayed when this type is selected
• Pass a FixedLayout instance over as data to your
FixedBoardPage
I would love to see what concepts you come up with.
Source Code
I would love for you to have an attempt at this extra assignment, but I have
also provided the source code. The source code for this extra assignment
can be found on the GitHub repository at https://github.com/Apress/
Introducing-.NET-MAUI-2nd-ed/tree/main/ch06-extra.
189
CHAPTER 7
At the end of the last chapter, I discussed the idea of having a second
type of layout in the “Extra Assignment” section. To continue with this
theme, I have structured the architecture of the layout to aid in this journey.
I am a fan of taking an approach like this because it allows you to potentially
replace one part of the implementation without impacting the others.
BoardLayout will be responsible for displaying the widgets. It will be
assigned an ILayoutManager implementation, which will decide where
to place the widgets. You will be adding a FixedLayoutManager to decide
this part.
Placeholder
The first item that you need to create is the placeholder to show where a
widget will be placed. There isn’t too much to this control, but creating it
allows you to group all of the related bits and pieces together. Figure 7-2
shows what your Placeholder control will look like when rendered inside
the application.
192
CHAPTER 7 CREATING OUR OWN LAYOUT
In order to achieve the above look, you are going to make use of the
Border control. This is a really useful control. It allows you to provide
borders, custom corner radius, shadows, and other styling options. It also
behaves much like the ContentView in that it can contain a single child
control.
Create a folder called Controls in your main project. It will house the
Placeholder control and potentially more as you build your application.
Next, add a new class to the folder and call it Placeholder. Note that
you are opting to create the control purely in C# without XAML; the main
reason is that it results in less code. I always find there is never a single
way to build things, and even if you like XAML, at times it doesn’t add any
value, just like in this scenario. Of course, if you prefer to build your UI with
XAML, you can do so.
namespace WidgetBoard.Controls;
193
CHAPTER 7 CREATING OUR OWN LAYOUT
{
Text = "Tap to add widget",
FontAttributes = FontAttributes.Italic,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center
};
}
As discussed, there isn’t too much to this implementation, but let’s still
break it down. Here you have
• Created a control that inherits from Border
• Set the content of your control to be a Label showing
fixed text in an italic font and the text is centered both
horizontally and vertically
• Added a Position property to know where in the layout
it will be positioned
Now you can start building the layout that will display the placeholders
and ultimately your widgets.
ILayoutManager
You have a slight chicken-and-egg scenario here. You need to create a
board and a layout manager, both of which need to know about the other;
therefore, let’s add in the LayoutManager parts first.
The purpose of the ILayoutManager interface is to define how the
BoardLayout will interact with a layout manager implementation.
Create a folder called Layouts in your main project. It will house the
ILayoutManager interface and more as you build your application.
194
CHAPTER 7 CREATING OUR OWN LAYOUT
namespace WidgetBoard.Layouts;
Let’s break it down so you have a clear definition of what you just
created:
BoardLayout
Your BoardLayout will be the parent of your widgets. Create the layout
inside your Layouts folder.
195
CHAPTER 7 CREATING OUR OWN LAYOUT
BoardLayout.xaml
Modify the existing contents to the following:
<Grid
x:Name="PlaceholderGrid" />
<Grid
x:Name="WidgetGrid"
ChildAdded="OnWidgetsChildAdded"
BindableLayout.ItemsSource="{Binding ItemsSource,
Source={x:Reference Self}}"
BindableLayout.ItemTemplateSelector="{Binding
ItemTemplateSelector, Source={x:Reference Self}}"
InputTransparent="True"
CascadeInputTransparent="False" />
</Grid>
You have added quite a bit to this that might not feel familiar, so again
let’s break it down.
196
CHAPTER 7 CREATING OUR OWN LAYOUT
Your main layout is a Grid, and inside of it are two more Grids.
The first inner Grid (PlaceholderGrid) is where you add the
Placeholder control you created earlier in this chapter.
The second inner Grid (WidgetGrid) is where you add widgets. The
reason you have built the control this way is mainly so you can utilize a
really impressive piece of functionality that drastically reduces the amount
of code you have to write: BindableLayout.
You have not supplied a Grid.Row or Grid.Column to either of your
inner Grids. This results in both controls filling the space of the parent
Grid and the second one overlapping the first. This behavior can provide
some real power when building rather complex UIs.
BindableLayout
BindableLayout allows you to turn a layout control into a control that
can be populated by a collection of data. BindableLayout is not a control
itself, but it provides the ability to enhance layout controls by adding an
ItemsSource property for bindings. This means that all of the layouts
you learned about in the previous chapter (e.g., Grid, AbsoluteLayout,
FlexLayout, HorizontalStackLayout, VerticalStackLayout) can be
turned into a layout that can show a specific set of controls for each item
that is provided. For this, you need to set two properties:
• BindableLayout.ItemsSource: This is the collection of
items that you wish to represent in the UI.
• BindableLayout.ItemTemplate or BindableLayout.
ItemTemplateSelector: This allows you to define
how the item will be represented. In most scenarios,
ItemTemplate is enough, but this only works when you
have one type of item to display in your collection. If
you have multiple types, each widget will be a separate
type in your application, so you need to use the
ItemTemplateSelector.
197
CHAPTER 7 CREATING OUR OWN LAYOUT
I won’t actually be providing the source for these bindings just yet; this
will be done in Chapter 8. For now, you just need to make it possible to
bind them.
BoardLayout.xaml.cs
Now that you have created your XAML representation, you need to add in
the code-behind, which will work with it. We are going to follow a slightly
different approach for this and the next section; you have a lot of code
to add now so you will add it in stages and we will talk around what you
are adding.
The initial code should look as follows:
namespace WidgetBoard.Layouts;
198
CHAPTER 7 CREATING OUR OWN LAYOUT
{
layoutManager = value;
199
CHAPTER 7 CREATING OUR OWN LAYOUT
using System.Collections;
200
CHAPTER 7 CREATING OUR OWN LAYOUT
201
CHAPTER 7 CREATING OUR OWN LAYOUT
This handler checks to see if the new child being added is of the
IWidgetView type, and if it is, it delegates out to the LayoutManager
implementation to set the widget’s position.
using WidgetBoard.Controls;
This method allows the caller to pass a placeholder that will be added
to PlaceholderGrid. This is useful when first loading a board or when
dealing with a widget being removed from a specific position.
202
CHAPTER 7 CREATING OUR OWN LAYOUT
This method allows for the board’s columns to be defined on both the
PlaceholderGrid and WidgetGrid.
This method allows for the board’s rows to be defined on both the
PlaceholderGrid and WidgetGrid.
This property provides all children from the PlaceholderGrid that are
of type Placeholder. This is to allow for determining which placeholder
needs to be removed when adding a widget.
FixedLayoutManager
The final part for you to create is the FixedLayoutManager class. This will
provide the logic to
• Accept the number of rows and columns for a board
• Provide tap/click support through a command
203
CHAPTER 7 CREATING OUR OWN LAYOUT
namespace WidgetBoard.Layouts;
To start, you are going to want to add the following using statements:
using System.Windows.Input;
using WidgetBoard.Controls;
And also make your class inherit from BindableObject and implement
your ILayoutManager interface. Your class should now look as follows:
using System.Windows.Input;
using WidgetBoard.Controls;
namespace WidgetBoard.Layouts;
204
CHAPTER 7 CREATING OUR OWN LAYOUT
205
CHAPTER 7 CREATING OUR OWN LAYOUT
206
CHAPTER 7 CREATING OUR OWN LAYOUT
207
CHAPTER 7 CREATING OUR OWN LAYOUT
Next, you need to add the code that will execute the command. You
will be relying on the use of a TapGestureRecognizer by adding one to
your Placeholder control inside your InitializeGrid method that you
will be adding in the next section. For now, you can add the method that
will be used so that you can focus on how to execute the command. Let’s
add the code and then look over the details.
You can see from the implementation that there are three main parts to
the command execution logic:
• First, you make sure that command has a value.
• Second, you check that you can execute the command.
If you recall back in Chapter 5, you provided a method
to prevent the command from executing if the user
hadn’t entered a BoardName.
208
CHAPTER 7 CREATING OUR OWN LAYOUT
Your method to build the grid layout has several parts, so let’s add
them as you go and discuss their value. You initially need to make sure that
you have valid values for the Board, NumberOfRows, and NumberOfColumns
properties plus you haven’t already built the UI.
209
CHAPTER 7 CREATING OUR OWN LAYOUT
{
return;
}
isInitialized = true;
}
The next step is to use the NumberOfColumns value and add it to your
Board. Let’s add this to the end of the InitializeGrid method.
210
CHAPTER 7 CREATING OUR OWN LAYOUT
211
CHAPTER 7 CREATING OUR OWN LAYOUT
Grid.SetColumn(bindableObject, column);
Grid.SetRow(bindableObject, row);
212
CHAPTER 7 CREATING OUR OWN LAYOUT
using WidgetBoard.ViewModels;
using WidgetBoard.Views;
namespace WidgetBoard;
213
CHAPTER 7 CREATING OUR OWN LAYOUT
The above may look a little complicated, but if you break it down,
hopefully it should become clear. You have added two fields that will store
the type and name information needed for when you create the instances
of widgets.
The RegisterWidget method takes a display name parameter and
two types:
214
CHAPTER 7 CREATING OUR OWN LAYOUT
You then store a mapping between the view model type and the view
type (widgetRegistrations). This allows you to create a view when you
pass in a view model. This really helps you to keep a clean separation
between your view and view model.
You also store a mapping between the display name and the view
model type (widgetNameRegistrations). This will allow you to present an
option on screen to the user. Once they choose the name of the widget they
would like to add, the factory will create an instance of it.
215
CHAPTER 7 CREATING OUR OWN LAYOUT
216
CHAPTER 7 CREATING OUR OWN LAYOUT
}
return null;
}
builder.Services.AddSingleton<WidgetFactory>();
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
217
CHAPTER 7 CREATING OUR OWN LAYOUT
{
InitializeComponent();
WidgetViewModel = clockWidgetViewModel;
BindingContext = clockWidgetViewModel;
}
WidgetFactory.RegisterWidget<ClockWidgetView, ClockWidgetView
Model>("Clock");
builder.Services.AddTransient<ClockWidgetView>();
builder.Services.AddTransient<ClockWidgetViewModel>();
WidgetTemplateSelector
The main purpose of this implementation is to provide a conversion
between the widget view models that you will be storing on your
FixedBoardPageViewModel and something that can actually be rendered
on the screen. You are going to depend on the WidgetFactory you have
just created. Create the class under the root project folder and modify its
contents to the following:
using WidgetBoard.ViewModels;
218
CHAPTER 7 CREATING OUR OWN LAYOUT
namespace WidgetBoard;
219
CHAPTER 7 CREATING OUR OWN LAYOUT
builder.Services.AddSingleton<WidgetTemplateSelector>();
Updating FixedBoardPageViewModel
You need to add in the properties that you can bind to in your view.
220
CHAPTER 7 CREATING OUR OWN LAYOUT
BoardName = board.Name;
NumberOfColumns = board.NumberOfColumns;
NumberOfRows = board.NumberOfRows;
}
221
CHAPTER 7 CREATING OUR OWN LAYOUT
public FixedBoardPageViewModel(
WidgetTemplateSelector widgetTemplateSelector
)
{
WidgetTemplateSelector = widgetTemplateSelector;
Widgets = [];
}
222
CHAPTER 7 CREATING OUR OWN LAYOUT
<layouts:FixedLayoutManager
NumberOfColumns="{Binding NumberOfColumns}"
NumberOfRows="{Binding NumberOfRows}" />
</layouts:BoardLayout.LayoutManager>
</layouts:BoardLayout>
</ContentPage>
This now includes your shiny new BoardLayout complete with all the
bindings you have created to make it functional.
One additional change you will need to make is to link the
FixedBoardPage to the FixedBoardPageViewModel; to do this, you can
open the FixedBoardPage.xaml.cs file and modify the contents to the
following; the changes are in bold:
using WidgetBoard.ViewModels;
namespace WidgetBoard.Pages;
This sets us up nicely for when we start to use the page and display it in
our application.
223
CHAPTER 7 CREATING OUR OWN LAYOUT
Summary
In this chapter, you have
• Created your own layout
• Made use of a variety of options when adding bindable
properties
• Provided command support from your layout
• Used your layout in your application
In the next chapter, you will
• Gain an understanding of what accessibility is
• Learn why it is important to build inclusive
applications
• Look at how you can make use of .NET MAUI
functionality
• Consider other scenarios and how to support them
• Look over some testing options to support your journey
to building accessible applications
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch07.
224
CHAPTER 7 CREATING OUR OWN LAYOUT
Extra Assignment
You will have noticed how a lot of the naming includes the word Fixed.
Let’s continue the extra assignment from the previous chapter and build a
board that is a variation of this approach. I really like the idea of a freeform
board where the user can position their widgets wherever they like. This
is a little more involved, but if you consider how the BoardLayout can
use AbsoluteLayouts rather than Grids, then a new ILayoutManager
implementation should hopefully be where the alternative logic will need
to be applied. If you do embark on this journey, please feel free to share
your experience and findings.
Source Code
I would love for you to have an attempt at this extra assignment, but I have
also provided the source code. The source code for this extra assignment
can be found on the GitHub repository at https://github.com/Apress/
Introducing-.NET-MAUI-2nd-ed/tree/main/ch07-extra.
225
CHAPTER 8
Accessibility
Abstract
In this chapter, you will be taking a break from adding new parts to the
user interface in order to gain an understanding of what accessibility is,
why you should make your applications accessible, and how .NET MAUI
makes this easier. You will also cover some testing options to support your
journey to building accessible applications.
I wanted this chapter to appear earlier on in this book. I feel it is such
an important topic and one that you really do need to consider early on
in your projects. It has come to settle nicely in the middle of the book now
because you needed some UI to apply the concepts to.
What Is Accessibility?
The definition of accessibility according to the Cambridge Dictionary
(https://dictionary.cambridge.org/dictionary/english/
accessibility) is
“the quality of being easy to understand”
By considering the scenarios where your application might be less easy
to understand for a large percentage of the world’s population that have
some form of disability, you can learn to provide ways to break down the
complexities in understanding the content. This might be through the use
228
CHAPTER 8 ACCESSIBILITY
229
CHAPTER 8 ACCESSIBILITY
230
CHAPTER 8 ACCESSIBILITY
SemanticProperties
The SemanticProperties class offers a set of attached properties that can
be applied to any visual element. .NET MAUI applies these property values
on the platform-specific APIs that provide accessibility.
Let’s look through each of the properties and apply them to your
BoardDetailsPage.
SemanticProperties.Description
The SemanticProperties.Description property allows you to define a
short string that will be used by the screen reader to announce the element
to the user when it gains focus. This should be a name that implies the
intent of the element if the user were to interact with it.
As I type this chapter, I am testing the application. The first Entry
added on the BoardDetailsPage currently results in the macOS VoiceOver
assistant announcing “edit text, is editing, blank”.
231
CHAPTER 8 ACCESSIBILITY
<Entry
Text="{Binding BoardName}"
SemanticProperties.Description="Enter the board name"/>
This now results in “Enter the board name, is editing, blank” being
announced, which is much more useful to the user.
You can take this a step further. You have a label above that just has the
Text of “Name.” If you change this to use your new descriptive text, then
you can set the SemanticProperties.Description value to its text. Let’s
do that now; the changes are highlighted in bold:
<Label
Text="Enter the board name"
x:Name="EnterBoardNameLabel"
FontAttributes="Bold" />
<Entry
Text="{Binding BoardName}"
SemanticProperties.Description="{Binding Text,
Source={x:Reference EnterBoardNameLabel}}" />
The resulting code may look less appealing, but it provides a number of
benefits:
232
CHAPTER 8 ACCESSIBILITY
SemanticProperties.Hint
The SemanticProperties.Hint property allows you to provide a string
that the screen reader will announce to the user so that they have a better
understanding of the purpose of the control.
Let’s add a hint to Entry with the addition in bold:
<Entry
Text="{Binding BoardName}"
SemanticProperties.Description="{Binding Text,
Source={x:Reference EnterBoardNameLabel}}"
SemanticProperties.Hint="Provides a name that will be
used to identify your widget board. This is a required
field." />
SemanticProperties.HeadingLevel
The SemanticProperties.HeadingLevel property allows you to mark
an element as a heading to help organize the UI and make it easier for
users to navigate. Some screen readers enable users to quickly jump
between headings, thus providing a far more friendly navigation for those
users that rely on screen readers. To give some context on the need for
headings - when using VoiceOver on iOS you can swipe down or up to
navigate between headings and then left or right to navigate between
233
CHAPTER 8 ACCESSIBILITY
the items under the heading, otherwise it could be an arduous task for
a user to navigate between all items in the UI in order to reach the item
they need. Headings have a level from 1 to 9 and are represented by the
SemanticHeadingLevel enumeration.
public Placeholder()
{
Content = new Label
{
Text = "Tap to add widget",
FontAttributes = FontAttributes.Italic,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center
};
SemanticProperties.SetDescription(
Content,
"Tap to add a widget");
SemanticProperties.SetHint(
Content,
"Allows you to choose a widget that can be added to the
board at this location.");
}
234
CHAPTER 8 ACCESSIBILITY
You will notice how this approach looks rather different to setting the
properties in XAML.
SemanticScreenReader
So far we have added helpful property values that the screen reader will
use; this is great for on-screen control, but what if you want to provide
more context for when an action was triggered? .NET MAUI provides the
SemanticScreenReader that enables you to instruct a screen reader to
announce some text to the user. This can work especially well if you wish
to present instructions to a user or to prompt them if they have paused
their interaction.
The SemanticScreenReader provides a static Announce method
to perform the announcements; it also provides a Default instance. I
personally like to make use of the scenarios where .NET MAUI provides
you with a Current or a Default instance and register this with the app
builder to make full use of the dependency injection support. To do this,
write the following line of code in your MauiProgram.cs file:
builder.Services.AddSingleton(SemanticScreenReader.Default);
With the screen reader registered, you can announce that the new
board was created successfully once the user has tapped on the Save
button. You need to open the BoardDetailsPageViewModel.cs file and
make the following changes.
Add the read-only field.
Assign a value in your constructor, just applying the bold code to your
existing content.
235
CHAPTER 8 ACCESSIBILITY
public BoardDetailsPageViewModel(ISemanticScreenReader
semanticScreenReader)
{
this.semanticScreenReader = semanticScreenReader;
SaveCommand = new Command(
() => Save(),
() => !string.IsNullOrWhiteSpace(BoardName));
}
Call Announce in your Save method, just applying the bold code to
your existing content.
await Shell.Current.GoToAsync(
RouteNames.FixedBoard,
new Dictionary<string, object>
{
{ "Board", board }
});
}
236
CHAPTER 8 ACCESSIBILITY
If you run your application and save a new board called “My work
board,” you will observe that the screen reader will announce “A new
board with the name My work board was created successfully.” This gives
the user some valuable audible feedback. If you expect the save process to
take some time, you can also perform an announcement at the start of the
process to keep the user informed.
AutomationProperties
AutomationProperties are the old Xamarin.Forms way of exposing
information to the screen readers on each platform. I won’t cover all of the
options because some have been replaced by the SemanticProperties
section that you just learned about. In fact, I would strongly recommend
that you always look at SemanticProperties before considering using the
AutomationProperties class. The following are the important ones that
provide a different set of functionality.
AutomationProperties.ExcludedWithChildren
The AutomationProperties.ExcludeWithChildren property allows
developers to exclude the element supplied and all its children from the
accessibility tree. Setting this property to true will exclude the element and
all of its children from the accessibility tree.
AutomationProperties.IsInAccessibleTree
The AutomationProperties.IsInAccessibleTree property allows
developers to decide whether the element is visible to screen readers.
A common scenario for this feature is to hide controls such as Label or
Image controls that serve a purely decorative purpose (e.g., a background
image). Setting this property to true will exclude the element from the
accessibility tree.
237
CHAPTER 8 ACCESSIBILITY
Suitable Contrast
WCAG states in guideline 1.4.3 Contrast (Minimum) – Level AA that the
visual presentation of text and images of text has a contrast ratio of at least
4.5:1, except for the following:
• Large Text: Large-scale text and images of large-scale
text have a contrast ratio of at least 3:1.
• Incidental: Text or images of text that are part of an
inactive user interface component, that are pure
decoration, that are not visible to anyone, or that are
part of a picture that contains significant other visual
content have no contrast requirement.
238
CHAPTER 8 ACCESSIBILITY
239
CHAPTER 8 ACCESSIBILITY
If you take a look at the colors you are using for your text controls and
the background colors, you can work out whether you need to improve on
the contrast ratio. You can see by checking in your Styles.xaml file that
your Label control uses Gray900 for the text color. Checking in the Colors.
xaml file, you can see that this Gray900 color has a value of #212121.
Therefore, you can use your methods to calculate the contrast ratio with
GetContrastRatio(Colors.White, Color.FromArgb("#212121");
This gives you a contrast ratio of 16.10:1, which means this is providing
a very good contrast ratio. The best possible contrast is black on white,
which gives a contrast ratio of 21:1. Therefore, you do not need to make
any changes to your color scheme, which shows that .NET MAUI ships
with default color options that are suitable for building accessible
applications. In fact, I have it on good authority that the .NET MAUI
templates undergo audits to ensure that they are accessible; therefore, they
provide an excellent set of examples to follow.
240
CHAPTER 8 ACCESSIBILITY
This guideline mainly focuses on highlighting the fact that there is still
a large percentage of users that do not rely on accessibility features such
as screen readers or screen magnification when they could benefit from
them. The guideline further states that, as a developer, you should provide
the ability to scale the text in your application up to 200% without relying
on the operating system to perform the scaling.
In this section, I am not going to focus on adding that specific feature;
however, I will be discussing some approaches that will aid this feature as
well as using the assistive technology options.
241
CHAPTER 8 ACCESSIBILITY
Figure 8-1. Your application with fixed sizing and a small font size
However, if you up the scaling to 200%, you will see a rather unpleasant
screen. Figure 8-2 shows your application with fixed size controls and a
large font size, highlighting that the text becomes clipped and unreadable.
242
CHAPTER 8 ACCESSIBILITY
Figure 8-2. Your application with fixed sizing and a large font size
243
CHAPTER 8 ACCESSIBILITY
244
CHAPTER 8 ACCESSIBILITY
Android
Google, much like each of the other platform providers, does recommend
that you perform a manual test, such as turning on TalkBack and verifying
that the user experience is as you have designed.
Google also offers some analysis tools to detect whether any
accessibility guidelines are not being met. There is a good list provided
by Google with a breakdown of the functionality provided by each tool at
https://developer.android.com/guide/topics/ui/accessibility/
testing#analysis.
iOS
Apple doesn’t offer as much as Google on this front. There is the
Accessibility Inspector, but it only focuses on allowing you to view
the information that the screen reader will be provided. I don’t
feel this is as good as taking a dry run through your application
245
CHAPTER 8 ACCESSIBILITY
macOS
Apple provides a little extra functionality when testing on macOS. It
does provide the Accessibility Inspector as per iOS and well as the
Accessibility Verifier. This tool allows you to run tests against your
application to verify items like the accessibility description have been
defined on all required elements. Further information on these features
can be found at https://developer.apple.com/library/archive/
documentation/Accessibility/Conceptual/AccessibilityMacOSX/
OSXAXTestingApps.html.
Windows
Microsoft offers the biggest amount of options when it comes to testing the
accessibility of your applications. The Windows Software Development
Kit (SDK) provides several tools such as the ability to inspect an
application and view all related properties plus automation tests that
verify the state of accessibility. All details of the tools can be found at
https://docs.microsoft.com/windows/apps/design/accessibility/
accessibility-testing.
246
CHAPTER 8 ACCESSIBILITY
Useful Resources
Accessibility Checklist
The following checklist is provided by Microsoft on their documentation
site at https://docs.microsoft.com/dotnet/maui/fundamentals/
accessibility#accessibility-checklist. I haven’t added to it or
reworded because I believe it provides an excellent breakdown of the
possible ways to provide accessible support.
Follow these tips to ensure that your .NET MAUI apps are accessible to
the widest audience possible:
• Ensure your app is perceivable, operable,
understandable, and robust for all by following the Web
Content Accessibility Guidelines (WCAG). WCAG is
the global accessibility standard and legal benchmark
for web and mobile. For more information, see Web
Content Accessibility Guidelines (WCAG) Overview.
247
CHAPTER 8 ACCESSIBILITY
248
CHAPTER 8 ACCESSIBILITY
Summary
In this chapter, you have
• Gained an understanding of what accessibility is
• Learned why it is important to build inclusive
applications
• Looked at how you can make use of .NET MAUI
functionality
249
CHAPTER 8 ACCESSIBILITY
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch08.
Extra Assignment
Take one of your favorite applications that you are completely familiar with
because you know the layout and how to use it. Then proceed to
• Turn on the screen reading assistant on your phone.
• Try to navigate your way around this application.
• Better still, try to impact your vision with a blindfold or
remove any glasses if you use them. Try to rely entirely
on the screen reader.
• Perhaps try the same but modify the device font scaling
and see if the application is able to handle increases in
text size, or if it even allows this option.
250
CHAPTER 9
Advanced UI
Concepts
Abstract
In this chapter, you will provide the user of your application with the ability
to add a widget to the boards they create through the use of an overlay. You
will further enhance this overlay by defining common styling techniques
and handling the differences between light and dark mode devices.
You will then take a journey into discovering how you can build an
application that feels natural and organic to your human user base. Finally,
you will look at how you can keep the animations driving the organic look
and feel cleanly separated from your business logic code.
As we covered in Chapter 7, there are two common approaches to
extending functionality: inheritance and composition. Chapter 7 focused
on how to utilize inheritance in how we built our BoardLayout control; this
chapter will focus on the composition approach through highlighting all
of the pieces that .NET MAUI offers to make this an easy task. The two key
offerings that we will be covering are Triggers and Behaviors – these each
have their own sections in this chapter.
252
CHAPTER 9 ADVANCED UI CONCEPTS
that is blocking and will require the user to engage with it to return to the
previous page. This type of page or display is referred to as modal. The
scenario of showing something to the user and requiring them to engage
with it could be a perfect scenario.
In order to enable this functionality in .NET MAUI, you need to set the
Shell.PresentationMode property on the ContentPage that you wish to
display. For example:
<ContentPage ...
Shell.PresentationMode="Modal">
...
</ContentPage>
Overlaying a View
Sometimes the most straightforward way to achieve this approach is to add
another view to your page and programmatically change its visibility to
give the impression you have a modal page displaying.
Pro
253
CHAPTER 9 ADVANCED UI CONCEPTS
Showing a Popup
There is currently no explicit support in .NET MAUI for displaying
popups; however, the functionality does exist on each of the platforms
that .NET MAUI runs on. You can go to the lengths of implementing your
own ability to display a popup, but it would be rather involved. Instead,
the .NET MAUI Community Toolkit provides a Popup class that makes it
straightforward for you to display a popup in your application.
Pros
• Keeps specific code contained
• Provides easy return result handling
Con
• Brings in an extra dependency
For further reading on how to use the toolkit and its Popup class, please
refer to the documentation at https://learn.microsoft.com/dotnet/
communitytoolkit/maui/views/popup.
254
CHAPTER 9 ADVANCED UI CONCEPTS
<BoxView
BackgroundColor="Black"
Opacity="0.5"
IsVisible="{Binding IsAddingWidget}" />
<Border
IsVisible="{Binding IsAddingWidget}"
HorizontalOptions="Center"
VerticalOptions="Center"
Padding="10">
<VerticalStackLayout>
<Label
Text="Add widget"
FontSize="20" />
<Label
Text="Widget" />
<Picker
ItemsSource="{Binding AvailableWidgets}"
SelectedItem="{Binding SelectedWidget}"
SemanticProperties.Description="{Binding Text,
Source={x:Reference SelectTheWidgetLabel}}"
SemanticProperties.Hint="Picker containing the
possible widget types that can be added to the
board. This is a required field." />
255
CHAPTER 9 ADVANCED UI CONCEPTS
<Label
Text="Preview" />
<ContentView
WidthRequest="250"
HeightRequest="250" />
<Button
Text="Add widget"
Command="{Binding AddWidgetCommand}"
SemanticProperties.Hint="Adds the selected widget
to the board. Requires the 'Select the widget'
field to be set." />
</VerticalStackLayout>
</Border>
The code addition results in two new controls added to the parent
Grid’s children collection: a BoxView and a Border. The BoxView is added
to provide a semi-transparent overlay on top of the rest of the application,
and the Border presents the content for selecting a new widget. Adding
them after the BoardLayout means it will be rendered on top of the
BoardLayout. This ordering is referred to as Z-index, and in the majority
of .NET MAUI applications, layouts are determined by the order in which
the children are added to their parent. This means that the later the
controls are added, the higher they will appear visually. You can modify
this default behavior by using the ZIndex property where the higher the
value, the higher they will appear visually. With this knowledge, you can
add a binding between the IsVisible property of your new controls and a
property on your view model, so your view model can control whether you
are adding a widget to the board.
Let’s update your view model.
256
CHAPTER 9 ADVANCED UI CONCEPTS
257
CHAPTER 9 ADVANCED UI CONCEPTS
public FixedBoardPageViewModel(
WidgetTemplateSelector widgetTemplateSelector,
WidgetFactory widgetFactory)
{
WidgetTemplateSelector = widgetTemplateSelector;
this.widgetFactory = widgetFactory;
Widgets = new ObservableCollection<IWidgetViewModel>();
AddWidgetCommand = new Command(OnAddWidget);
AddNewWidgetCommand = new Command<int>(index =>
{
IsAddingWidget = true;
addingPosition = index;
});
}
258
CHAPTER 9 ADVANCED UI CONCEPTS
IsAddingWidget = false;
}
Hopefully the majority of what you just added should feel familiar. The
part that most likely doesn’t is the final OnAddWidget method. Let’s take a
deeper look at this implementation.
The SelectedWidget property is bound to your Picker in the view. You
do some initial input validation to make sure that the user has chosen a
type of widget to add; otherwise, you return out of the method.
Next, you use the new dependency (widgetFactory) to create a view
model for you.
Then you set its Position based on which placeholder was tapped
initially.
Then you add your newly created widgetViewModel to the collection of
Widgets so that it can update the UI.
Finally, you set the IsAddingWidget property to false in order to hide
the overlay view again.
259
CHAPTER 9 ADVANCED UI CONCEPTS
<layouts:BoardLayout
ItemsSource="{Binding Widgets}"
ItemTemplateSelector="{Binding WidgetTemplateSelector}">
<layouts:BoardLayout.LayoutManager>
<layouts:FixedLayoutManager
NumberOfColumns="{Binding NumberOfColumns}"
NumberOfRows="{Binding NumberOfRows}"
PlaceholderTappedCommand="{Binding AddNewWidget
Command}" />
</layouts:BoardLayout.LayoutManager>
</layouts:BoardLayout>
If you build and run your application, you can see that once you have
created a board, you can now tap or click on the Placeholder and observe
that your overlay displays. You will notice that there is no background
to your overlay, though, so it is really difficult for a user to understand
what to do. You can just set the BackgroundColor of your Border control;
however, this can lead to a number of issues. For example, if you fixed
the BackgroundColor to white and a user switches on dark mode on their
device, they would have a rather unpleasant experience. Figure 9-1 shows
how the application currently looks and highlights the issue.
260
CHAPTER 9 ADVANCED UI CONCEPTS
Figure 9-1. The application showing the overlay with a poor user
experience
Let’s look at how .NET MAUI provides the ability to style your
applications, which includes supporting light and dark modes.
Styling
.NET MAUI provides the ability to style your applications. Styling in .NET
MAUI offers many advantages:
• Central definition of look and feel
261
CHAPTER 9 ADVANCED UI CONCEPTS
• Style inheritance
Styles in .NET MAUI can be defined at many different
levels, and where they are defined is extremely
important when understanding what impact they will
have. The two key distinctions between where they are
defined can be considered as
• Globally: These styles are added to the application’s
resources. You can see an example of this if you open
the App.xaml file. The line in bold shows that another
file (Styles.xaml) containing the styles is loaded into
the Application.Resources property. These styles
apply to all controls in the application unless otherwise
explicitly overridden.
<Application
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/
winfx/2009/xaml"
xmlns:local="clr-namespace:WidgetBoard"
x:Class="WidgetBoard.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/
Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/
Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
262
CHAPTER 9 ADVANCED UI CONCEPTS
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding
Light={StaticResource Gray200}, Dark={StaticResource
Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>
TargetType
To start, when defining a Style, you must define the TargetType. This
property defines which type of control the style definition targets and
therefore applies to. Defining a Style with only the TargetType property
set will apply to all controls of that type within the scope it is defined. This
is referred to as implicit styling.
263
CHAPTER 9 ADVANCED UI CONCEPTS
If you wish to explicitly style a control, you can also add the x:Key
property. This is referred to as explicit styling. You are then required to set
the Style property on any control that wishes to use this explicit style that
you have created. You will be creating an explicit style in the “Creating a
Style” section following shortly.
ApplyToDerivedTypes
By default, styles created explicitly apply to the type defined in the
TargetType property I just covered. If you wish to allow derived classes to
also inherit this style, you need to set the ApplyToDerivedTypes property
to true. If you have a CustomBorder that inherits from Border and you had
the following Style defined:
Setter
This is the part that looks and feels quite a bit different to the previous XAML
you have written. Since you are not creating controls but defining how they
will look, you must follow this syntax. Let’s look at the following example:
<Style TargetType="Label">
<Setter Property="TextColor" Value="Black" />
</Style>
264
CHAPTER 9 ADVANCED UI CONCEPTS
Creating a Style
Let’s view this in action by adding the following to the Styles.xaml file.
Add this just below the existing <Style TargetType="Border"> entry.
The above looks very similar to the default Border style already defined
with the addition of the BackgroundColor setter.
It is also worth noting that you only need to set the values that you
wish to change from the implicit style. Therefore, your explicit style can be
reduced down to
265
CHAPTER 9 ADVANCED UI CONCEPTS
Now you can use this style in your application. Open the
FixedBoardPage.xaml file and add the following line to your Border
element (change in bold):
<Border
IsVisible="{Binding IsAddingWidget}"
HorizontalOptions="Center"
VerticalOptions="Center"
Padding="10"
Style="{StaticResource OverlayBorderStyle}">
This will result in your overlay looking far better to the user
now because it is no longer transparent. Also, consider moving the
HorizontalOptions, VerticalOptions, and Padding properties over to the
style definition. Figure 9-2 shows how much better the overlay now looks.
266
CHAPTER 9 ADVANCED UI CONCEPTS
What you have done here is considered bad practice, though! You
have hard-coded the BackgroundColor of your Border control in the style
definition so your application will look great on a device running in light
mode. However, as soon as the user switches to dark mode, they will have a
glaring white border showing.
The repercussions of using fixed values can include text or content
disappearing entirely from the application. Imagine that the text color
switches to white in dark mode, with you having hard-coded to a white
background of the overlay view, so the user would see no text on screen.
This would result in a terrible user experience.
.NET MAUI provides the ability to handle the different modes that a
device can run under.
AppThemeBinding
This is an extremely valuable concept. It allows you to define different
values based on whether the device your application is running on is set
to light or dark mode. Taking the example of the OverlayBorderStyle you
previously created, you can modify the Setter for BackgroundColor to
Now if a user is running in dark mode, the border overlay will be black
and the text will be visible.
You only need to apply AppThemeBinding to properties that require
a visual distinction between light and dark modes. This typically applies
to all Brush/Color properties; however, you could conceivably decide to
change the StrokeThickness of your Border control, for example.
267
CHAPTER 9 ADVANCED UI CONCEPTS
Further Reading
It is worth noting that this book is limited to covering the styling options
in XAML. However, .NET MAUI does provide support for CSS-based
style sheets. Go to https://docs.microsoft.com/dotnet/maui/user-
interface/styles/css.
Triggers
.NET MAUI provides a concept called triggers. They enable you to further
enhance how your views react to changes in the view model. You are given
the ability to define actions that can modify the appearance of the UI
based on event or data changes. Triggers provide us with another way of
changing the visibility of our border overlay for adding a new widget. The
initial work will appear more verbose in the short term, but do bear with
me – it will result in a much better outcome!
There are a number of different types of triggers that can be attached
to a control, each with a varying level of functionality. You will take a brief
look at them and then dig into the one that you need for your scenario.
• Trigger: A Trigger represents a trigger that applies
property values, or performs actions, when the
specified property meets a specified condition.
• DataTrigger: A DataTrigger represents a trigger that
applies property values, or performs actions, when the
bound data meets a specified condition. The Binding
markup extension is used to monitor for the specified
condition.
• EventTrigger: An EventTrigger represents a trigger that
applies a set of actions in response to an event. Unlike
Trigger, EventTrigger has no concept of termination
of state, so the actions will not be undone once the
condition that raised the event is no longer true.
268
CHAPTER 9 ADVANCED UI CONCEPTS
Creating a DataTrigger
In this chapter, you have added your overlay Border control and are
currently changing its visibility through a binding direct to the IsVisible
property. You can write this differently with a DataTrigger. Let’s open the
FixedBoardPage.xaml file and modify the Border control to the following:
<Border
IsVisible="False"
HorizontalOptions="Center"
VerticalOptions="Center"
Padding="10"
Style="{StaticResource OverlayBorderStyle}">
<Border.Triggers>
<DataTrigger
TargetType="Border"
Binding="{Binding IsAddingWidget}"
Value="True">
<Setter
Property="IsVisible"
Value="True" />
</DataTrigger>
</Border.Triggers>
Notice that the syntax for a Trigger is very similar to a Style. You
will also notice that it looks a lot more verbose than your original simple
binding approach. If you simply want to control the IsVisible property of
269
CHAPTER 9 ADVANCED UI CONCEPTS
IsVisible="False"
<DataTrigger
TargetType="Border"
Binding="{Binding IsAddingWidget}"
Value="True">
Much like with styles, you define the type of control the DataTrigger
applies to. You also set the Binding property to bind to the IsAddingWidget
property on your view model. Finally, you set the Value property to true.
This all means that when the IsAddingWidget property value is set to true,
the contents of the DataTrigger will be applied.
This leads you onto the final change, which is the setter.
<Setter
Property="IsVisible"
Value="True" />
270
CHAPTER 9 ADVANCED UI CONCEPTS
enters or exits a specific state. What exactly does this mean? Let’s take a
look at an example. You can rewrite the trigger usage from the previous
example as
<DataTrigger
TargetType="Border"
Binding="{Binding IsAddingWidget}"
Value="True">
<DataTrigger.EnterActions>
<!—-action to perform-->
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<!—-action to perform-->
</DataTrigger.ExitActions>
</DataTrigger>
Creating a TriggerAction
.NET MAUI provides the TriggerAction<T> base class that allows you to
define an action that will be performed in the enter or exit scenario. This
enables you to build a more complex behavior that can be performed
when a value changes. When creating a trigger action, you can use the base
class TriggerAction<T> provided by .NET MAUI, and then you need to
override the Invoke method. It is this method that defines what action will
be performed when the value changes. Let’s create your own action that
you can use.
271
CHAPTER 9 ADVANCED UI CONCEPTS
Creating ShowOverlayTriggerAction
First, you need to find a place to locate this action. Create a new folder
in the root project called Triggers and then add a new class file called
ShowOverlayTriggerAction.cs. Then you can add the following code:
namespace WidgetBoard.Triggers;
This code doesn’t do too much right now. It will just change the
IsVisible property of the control it is attached to when the value changes.
Now you need to attach it to your AddWidgetFrame control.
Using ShowOverlayTriggerAction
You can now add in the action to perform sections that you left when
first adding a DataTrigger to your control. Modify your code in the
FixedBoardPage.xaml file, with the changes in bold.
<DataTrigger
TargetType="Border"
Binding="{Binding IsAddingWidget}"
Value="True">
<DataTrigger.EnterActions>
<triggers:ShowOverlayTriggerAction
ShowOverlay="True" />
272
CHAPTER 9 ADVANCED UI CONCEPTS
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<triggers:ShowOverlayTriggerAction
ShowOverlay="False" />
</DataTrigger.ExitActions>
</DataTrigger>
Further Reading
You have only scratched the surface on the functionality that can
be achieved with triggers. I recommend checking out the Microsoft
documentation to see more ways triggers can be useful: https://learn.
microsoft.com/dotnet/maui/fundamentals/triggers.
This feels like it could be a challenging topic to show off in printed
form given the dynamic nature of an animation, but it is one of my favorite
topics so I am going to show it off as best I can. Animations provide you
with the building blocks to make your applications feel much more natural
and organic.
273
CHAPTER 9 ADVANCED UI CONCEPTS
Basic Animations
.NET MAUI ships with a set of prebuilt animations available via extension
methods. These methods provide the ability to rotate, translate, scale, and
fade a VisualElement over a period of time. Each of these methods has a
To suffix, for example, ScaleTo. It is worth noting that each of the methods
for animating is asynchronous and will therefore need to be awaited if you
wish to know when they have finished. The full list of animation methods
is as follows:
Method Description
274
CHAPTER 9 ADVANCED UI CONCEPTS
The overlay view you added in the previous section just shows
immediately and disappears immediately based on the IsVisible binding
you created. What if you animate your overlay to grow from nothing up to
the required size? Don’t worry about adding this code to your application
just yet. You will look over some examples and then add it to Visual Studio
in the “Combining Triggers and Animations” section. The main reason for
not adding it immediately is because the animation’s API relies on direct
access to the view-related information, and this breaks the MVVM pattern.
However, once you look over how to animate, you can take this learning
and add it into your ShowOverlayTriggerAction implementation.
The code to animate a VisualElement is surprisingly small, as you can
see in the following example:
AddWidgetFrame.Scale = 0;
await AddWidgetFrame.ScaleTo(1, 500);
First, you make sure that the AddWidgetFrame has a Scale of 0 and then
you call ScaleTo, telling it to grow to a Scale of 1 (which is 100%) over a
duration of 500 milliseconds.
All of the prebuilt animation methods apart from the ones that start
with Rel perform the animation against the VisualElement's existing
value (e.g., for ScaleTo, it will change from the existing Scale property
value). This means that it is entirely possible that no animation will take
place if both the existing property and the value provided to the method
are the same.
275
CHAPTER 9 ADVANCED UI CONCEPTS
Chaining Animations
You can chain animations together into a sequence. A common example
here is to provide the appearance of a tile being flipped over and giving a
3D effect to the user. The key detail when chaining animations is that you
await each animation method call to make sure that one animation has
finished before the next one begins.
Concurrent Animations
In a similar way to chaining, you can perform multiple animations
concurrently by simply not awaiting each method call or alternatively
awaiting all of the calls.
AddWidgetFrame.Scale = 0;
AddWidgetFrame.IsVisible = true;
AddWidgetFrame.Opacity = 0;
await Task.WhenAll(
AddWidgetFrame.FadeTo(1),
AddWidgetFrame.ScaleTo(1, 500));
In fact, this animation looks like a very good contender for your actual
implementation in the ShowOverlayTriggerAction implementation.
Cancelling Animations
Providing the ability to cancel an animation can be an extremely valuable
feature for a user. Quite often in applications, and predominantly games,
an animation will show when an action completes. Animations like this if
276
CHAPTER 9 ADVANCED UI CONCEPTS
blocking can become tiresome for users especially if the same animation
repeats frequently. Therefore, a common pattern to follow is when the user
taps on the control being animated, it cancels the animation.
If you wish to cancel an animation, you can call the CancelAnimations
extension method on the VisualElement that you are animating.
AddWidgetFrame.CancelAnimations();
Easings
Animations in general will move mechanically as a computer changes a
value over time. Easings allow you to move away from a linear update of
those values in order to provide a much more organic and natural motion.
.NET MAUI offers a whole host of prebuilt easings, plus there is even the
ability to build your own if you really wish to do so. Let’s take a look at the
options that .NET MAUI provides out of the box:
277
CHAPTER 9 ADVANCED UI CONCEPTS
As a general guide, an easing ending with the In suffix will start the
animation slowly and speed up as it comes to a finish. An easing ending
with the Out suffix will start off quickly and slow down toward the end.
Complex Animations
.NET MAUI provides the Animation class. This enables you to define
complex animation sequences. In fact, the prebuilt animations that you
covered in the “Basic Animations” section are built using this class inside
the .NET MAUI code. Using this class, it is possible to animate any visual
property of a VisualElement; for example, you can animate a change in
BackgroundColor or TextColor.
The Animation class provides the ability to define simple animations
through to really quite complex animations. Take a quick look at how
the ScaleTo animation can be implemented to understand what the
class offers.
278
CHAPTER 9 ADVANCED UI CONCEPTS
v => AddWidgetFrame.Scale = v
This code shows the simplest type of animation you can create
within .NET MAUI. It is entirely possible to create much more complex
animations. To achieve this, you need to create an animation and then
add child animations in order to define the changes for each property and
different sequences in the animation.
279
CHAPTER 9 ADVANCED UI CONCEPTS
Figure 9-3. The distinguishing frames from the animation you will
be building
Let’s build the animation with the Animation class using the
understanding you gained in the previous section.
280
CHAPTER 9 ADVANCED UI CONCEPTS
Yes, I know this looks quite different to the previous animation you
built. Let’s deconstruct the parts that feel unfamiliar.
281
CHAPTER 9 ADVANCED UI CONCEPTS
The two lines above define the first transition in your animation. You
see that the ScaleX property will change from 1.00 (100%) to 1.25 (125%)
and the ScaleY property will change from 1.00 (100%) to 0.75% (75%) of
the control’s current size. This provides the appearance that the view is
being stretched. The key new part for you is the use of the Add method and
the first two parameters. This allows you to add the animation defined
as the third parameter as a child of the animation it is being added to.
The result is that when you Commit the main animation, all of the child
animations will be executed based on the sequence you defined in these
two first parameters. Let’s cover what these parameters mean.
The first parameter is the beginAt parameter. This determines when
the child animation being added will begin during the overall animation
sequence. So in the example of your first line, you define 0.00, meaning it
will begin as soon as the animation starts.
The second parameter is the finishAt parameter. This determines
when the child being added will finish during the overall animation
sequence. So in the example of your first line, you define 0.30, meaning it
will end 30% into the animation sequence.
Both the beginAt and finishAt parameters should be supplied
as a value between 0 and 1 and considered a percentage in the overall
animation sequence. You will also notice that I tend to include the
decimal places even when they are 0; this really makes it easier to read the
animation sequence as it ensures that all of the code is indented in the
same way.
Finally, you call the Commit method as before to begin the animation
sequence.
Now that you have covered building animations and some possible
examples of using them, let’s combine them with your trigger knowledge
to really make your AddWidgetFrame look great when it becomes visible.
282
CHAPTER 9 ADVANCED UI CONCEPTS
namespace WidgetBoard.Triggers;
283
CHAPTER 9 ADVANCED UI CONCEPTS
else
{
await sender.ScaleTo(0, 500, Easing.SpringIn);
sender.Opacity = 0;
sender.IsVisible = false;
}
}
}
The trigger action now provides two key visual changes when the
ShowOverlay property value changes. When the property becomes true,
the AddWidgetFrame control will both fade in over 250 milliseconds and
scale up from 0 to 1 over 500 milliseconds. You also make use of the
Easing.SpringOut option to give a slightly more fluid feel to the changes
in the animation.
When ShowOverlay becomes false, you just reverse the scale
animation to show it shrink. Once the animation has completed, you then
make sure that the control is no longer visible.
This concludes the sections on triggers and animations. You have seen
how they can help to both simplify the views and view models you create
while at the same time provide some really great functionality to make
your applications feel alive. I would recommend taking the application for
a spin and observing the animations in action; sadly we can’t show that
functionality off in printed form.
Behaviors
Quite often as developers we need to extend functionality of controls;
there are typically two approaches for this when you consider doing this to
control:
284
CHAPTER 9 ADVANCED UI CONCEPTS
285
CHAPTER 9 ADVANCED UI CONCEPTS
First, we make our class inherit from the Behavior base class provided
by .NET MAUI; see the changes in bold:
Next we can add two properties to our class; these will allow
developers to define a valid and invalid Style.
bindable.TextChanged += BindableOnTextChanged;
}
bindable.TextChanged -= BindableOnTextChanged;
}
Finally, we can handle the TextChanged event and set the Style
property on the Entry that this behavior is attached to, to either the
InvalidStyle or the ValidStyle based on whether the user has entered
any text.
286
CHAPTER 9 ADVANCED UI CONCEPTS
<ContentPage.Resources>
<Style TargetType="Entry" x:Key="ValidEntryStyle">
<Setter Property="BackgroundColor"
Value="Transparent" />
</Style>
287
CHAPTER 9 ADVANCED UI CONCEPTS
We covered styles earlier; here we are adding a local Style which will
only apply in the current page. Next we need to attach the behavior and
assign the styles to it.
We can delete the following:
288
CHAPTER 9 ADVANCED UI CONCEPTS
lot of red/warnings on the screen before they have started entering data.
You usually only show them validation once they have started typing.
Figure 9-4 shows the name entry with a red background after the user has
deleted all text.
Figure 9-4. The name entry with a red background after the user has
deleted all text
The user can then enter text into the name field. Figure 9-5 shows the
name entry with a white background after the user has entered some text.
289
CHAPTER 9 ADVANCED UI CONCEPTS
Figure 9-5. The name entry with a red background after the user has
deleted all text
Fonts
We covered the topic of fonts in Chapter 3, but in this chapter, we are
going to put it into action and apply a custom font to the clock widget. I
always like the old digital display to show a clock. Sadly due to licensing
issues, I haven’t found a free font that exactly matches the digital display.
The best I could find is the VT323 font. For the purpose of including it in
your application, you can download it from https://fonts.google.com/
specimen/VT323 or feel free to choose any other font that you prefer. Once
you have a font downloaded, let’s proceed to using it in the application.
290
CHAPTER 9 ADVANCED UI CONCEPTS
fonts.AddFont("VT323-Regular.ttf", "VT323");
This makes the font from the VT323-Regular.ttf file available for use
under the alias of “VT323”.
FontFamily="VT323"
291
CHAPTER 9 ADVANCED UI CONCEPTS
Figure 9-6. The clock widget rendering the new VT323 font
You should notice that the font chosen is a monospace font, which
means all characters take up the same space; this is especially useful in
this scenario because it will prevent the text from moving from side to side
when the time changes.
Summary
In this chapter, you have
• Provided the ability to add a widget to a board
• Covered the different options available when showing
an overlay
292
CHAPTER 9 ADVANCED UI CONCEPTS
• Embedded a font
In the next chapter, you will
• Learn about the different types of local data
• Discover what .NET MAUI offers in terms of local file
storage locations and when to use each one
• Gain an understanding of database technologies and
apply two different options
• Modify your application to save and load the boards
your users create
• Gain an understanding of the options for storing small
bits of data or preferences
293
CHAPTER 9 ADVANCED UI CONCEPTS
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-ed/
tree/main/ch09.
Extra Assignment
I think you can take these animations to another level and really make your
application feel alive! Try the following possible extensions!
Source Code
I would love for you to have an attempt at this extra assignment, but I have
also provided the source code. The source code for this extra assignment
can be found on the GitHub repository at https://github.com/Apress/
Introducing-.NET-MAUI-2nd-ed/tree/main/ch09-extra.
294
CHAPTER 10
Local Data
Abstract
In this chapter, you will learn about the different types of local data, what
they are best used for, and how to apply them in your application. The
options will include understanding when and where to store data that
needs to be kept secure.
You will modify your application to store the boards that your user
creates so that they can be displayed in the slide-out menu and also be
opened. You will also record the last opened board so that when returning
to the application, this board will be presented to the user.
.NET MAUI provides multiple options when you want to store data
locally on a device. Each option is better suited to a specific purpose and
size of data. Here is a brief overview of those options:
• File system: Stores loose files directly on the device
through file system access
• Database: Stores data in a file optimized for access
File System
.NET MAUI provides some helpful abstractions over the multiple platforms
that it supports. One such abstraction is the FileSystem helper class. It
comes from the old Xamarin.Essentials library and now is a core part of
.NET MAUI. It allows you to obtain useful bits of information to help with
common tasks involving the file system.
Let’s take a look at the properties the FileSystem class offers you as it
helps to know when they should be used and for what type of data.
Cache Directory
You have no need to cache anything as part of the application we’re
building in this book; however, I feel this is a valuable piece of information
to mention. This property enables you to get the most appropriate location
to store cache data. You can store any type of data in this directory.
Typically you store it when you want to persist it longer than just holding
296
CHAPTER 10 LOCAL DATA
it in memory, but your application must not rely on this data to function
because the operating system can and will purge the contents of this
directory.
builder.Services.AddSingleton(FileSystem.Current);
297
CHAPTER 10 LOCAL DATA
Database
A database is a collection of data that is organized. In a database, data
is organized or structured into tables consisting of rows and columns.
Databases are a much better approach than storing data in files. The
ability to index the data makes it easier to query and manipulate. There
are different kinds of databases, ranging from relational databases to
distributed databases, cloud databases, and NoSQL databases. In this
chapter, you will focus on relational and NoSQL databases.
Every application I have ever built has required some form of database,
and I suspect that most of the applications that you will build will also
require one. In fact, a customer once insisted that we build them an
application without a database until we helped them understand the true
value that a database provides. A database really provides value when you
need to link data together or filter and sort the data in an efficient manner.
In your application, you are going to provide the ability to save a board
and return a list of boards that the user has created. You will also provide
the ability to store where the widgets have been placed so that they will
be remembered when a user loads the board back up. This means that
we will need to store information about a widget and the board that they
belong to. There are many ways to structure this, and if you are not familiar
with database design approaches and how to optimize the design, I would
strongly recommend reading up on the subject along with database
normalization. Figure 10-1 shows the entity relationship diagram for the
database you will be creating.
298
CHAPTER 10 LOCAL DATA
Repository Pattern
The repository pattern allows you to hide all the logic that deals with
creating, reading, updating, and deleting (also known as CRUD) entities
within your application. By using this pattern, it allows you to keep all
the knowledge around how entities are loaded, saved, and more in a
single place. This has the added benefit that if you want to completely
change where your data is loaded from, you only need to change the
implementation inside the repository. It also allows you to provide mock
implementations when wanting to perform things like unit testing and you
don’t want to have to rely on an actual database existing. The repository
pattern will also work well in this application due to the number of
different parts of the application that perform the same or similar things
(e.g., loading a list of boards). Having a single repository that does all of the
database-related activities makes it easier to maintain as the application
grows in size and complexity.
Let’s add a new folder called Data and then add an interface for your
repository to that folder called IBoardRepository. Change the code to look
as follows:
using WidgetBoard.Models;
namespace WidgetBoard.Data;
299
CHAPTER 10 LOCAL DATA
IReadOnlyList<Board> ListBoards();
Now that you have defined your interface, you can update your
application’s code base to use this interface when loading and saving
your boards.
Creating a Board
Thankfully our application only provides one location to create a board; I
would like to argue that makes for a good design practice because it keeps
the creation logic in a single place.
The first place you will update is your BoardDetailsPageViewModel
class, which provides support for creating a new board. Open up the class
and make the following modifications.
Add a new IBoardRepository field.
public BoardDetailsPageViewModel(
ISemanticScreenReader semanticScreenReader,
IBoardRepository boardRepository)
{
this.semanticScreenReader = semanticScreenReader;
this.boardRepository = boardRepository;
SaveCommand = new Command(
() => Save(),
() => !string.IsNullOrWhiteSpace(BoardName));
}
300
CHAPTER 10 LOCAL DATA
this.boardRepository.CreateBoard(board);
semanticScreenReader.Announce($"A new board with the name
{BoardName} was created successfully.");
await Shell.Current.GoToAsync(
RouteNames.FixedBoard,
new Dictionary<string, object>
{
{ "Board", board }
});
}
301
CHAPTER 10 LOCAL DATA
the user creates and you store in the database. Let’s do each one in turn
because they each have a slightly different approach needed to update
them to follow a good practice.
public AppShellViewModel(
IBoardRepository boardRepository)
{
this.boardRepository = boardRepository;
}
There is a further change that you need to make in order to allow your
AppShellViewModel class to actually load the board. You need to hook into
some of the lifecycle events that apply to Pages in .NET MAUI. AppShell
302
CHAPTER 10 LOCAL DATA
inherits from Page, which means you get full access to those lifecycle
events. The specific event you care about now is the OnAppearing event. It
is called when your page is displayed on screen.
The OnAppearing method can be called multiple times during
the lifetime of the page, so it is recommended to make your method
idempotent or check whether it has been called before in order to prevent
odd behavior when called a second time.
OnAppearing is a great choice for your scenario because it will result
in your code being executed every time the view appears; this can be
every time your flyout menu is opened. This provides you with the ability
to refresh your list of boards every time the user opens the flyout menu.
The main reason it is fine for your scenario is because you will be loading
data from a local database with a limited number of boards to load, so it
will be pretty quick. In scenarios where you are loading from an external
web service, it can take much more time to perform it, and therefore, you
may wish to maintain some level of caching and prevent calling the web
service every time the view appears. A better option under this scenario
and probably most typical scenarios in .NET MAUI applications is to use
the OnNavigatedTo method.
Let’s open your AppShell.xaml.cs file and make use of this
lifecycle method.
When the method gets called, you use the newly added LoadBoards
method on your view model. The main reason you hook into this lifecycle
event is when you eventually try to navigate to the last used board in the
LoadBoards method, you need to make sure the application has started
rendering; otherwise, the navigation will fail.
303
CHAPTER 10 LOCAL DATA
public BoardListPageViewModel (
IBoardRepository boardRepository)
{
this.boardRepository = boardRepository;
}
Load the list of boards and populate your collection.
public void LoadBoards()
{
Boards.Clear();
304
CHAPTER 10 LOCAL DATA
305
CHAPTER 10 LOCAL DATA
base.OnQueryChanged(oldValue, newValue);
if (string.IsNullOrWhiteSpace(newValue))
ItemsSource = null;
else
ItemsSource = Boards
306
CHAPTER 10 LOCAL DATA
.ToList<Board>();
That was a different set of changes to the previous loading of data up,
but quite often you will find there are handy ways to share data that has
already been loaded into your application. This makes it possible to build
apps that will perform better and consume less memory due to the nature
of sharing what has already been loaded.
The final change is to open the BoardListPage.xaml file and make the
following modification (in bold):
<Shell.SearchHandler>
<widgetBoard:BoardSearchHandler
Boards="{Binding Boards}"
Placeholder="Enter board name"
ShowsResults="True"
DisplayMemberName="Name" />
</Shell.SearchHandler>
Loading a Board
Up until this point you have relied on passing the Board into the
FixedBoardPageViewModel and displaying the details of that. The loading
process would become rather inefficient if you were to load all boards and
the associated BoardWidgets when listing all boards in the system, so you
need to do this in a two-step process: first, list the boards as you did in the
307
CHAPTER 10 LOCAL DATA
previous section and, second, load the board in the view model. This will
be a slightly involved process, so let’s walk through it step by step. Open
the FixedBoardPageViewModel.cs file and make the following changes.
Add the following fields to store the board that is loaded and the
repository to perform the load:
public FixedBoardPageViewModel(
WidgetTemplateSelector widgetTemplateSelector,
WidgetFactory widgetFactory,
IBoardRepository boardRepository)
{
WidgetTemplateSelector = widgetTemplateSelector;
this.widgetFactory = widgetFactory;
this.boardRepository = boardRepository;
308
CHAPTER 10 LOCAL DATA
if (widgetViewModel is null)
{
continue;
}
widgetViewModel.Position = boardWidget.Position;
Widgets.Add(widgetViewModel);
}
}
}
309
CHAPTER 10 LOCAL DATA
The above method will create a new BoardWidget model class and save
it into the database for you.
Finally, you need to call the SaveWidget method. For the purpose of
your application, you are going to provide an autosave feature, so each
time a widget is added to the board, you will save it immediately to the
database. In order to achieve this, you just need to add the bold line into
your AddWidget method.
310
CHAPTER 10 LOCAL DATA
SaveWidget(widgetViewModel);
}
IsAddingWidget = false;
}
You can’t run your code yet because you don’t have an implementation
of your IBoardRepository interface, so let’s look at two different database
options that will allow you to provide an implementation for your
IBoardRepository.
SQLite
SQLite is a lightweight cross-platform database that has become the
go-to option for providing database support in mobile applications. The
database is stored locally in a single file on the device’s file system.
SQLite is supported natively by Android and iOS; however, they require
access via C++. There are several C# wrappers around the native SQLite
engine that .NET developers can use. The most popular choice is the C#
wrapper called SQLite-net.
Installing SQLite-net
In order to install and use SQLite-net, you need to install the NuGet
package called Sqlite-net-pcl. You may notice the extra -pcl suffix in the
NuGet package name and find this confusing. This is an artifact of an old
piece of technology used in Xamarin.Forms applications. The name has
been retained, but don’t worry; this is the correct package for adding to a
.NET MAUI project.
311
CHAPTER 10 LOCAL DATA
Using SQLite-net
The first step is to create your IBoardRepository implementation. Add a
new class file called SqliteBoardRepository in your Data folder, and make
it implement your IBoardRepository interface.
using SQLite;
using WidgetBoard.Models;
namespace WidgetBoard.Data;
312
CHAPTER 10 LOCAL DATA
builder.Services.AddTransient<IBoardRepository,
SqliteBoardRepository>();
313
CHAPTER 10 LOCAL DATA
using SQLite;
namespace WidgetBoard.Models;
314
CHAPTER 10 LOCAL DATA
using SQLite;
namespace WidgetBoard.Models;
315
CHAPTER 10 LOCAL DATA
316
CHAPTER 10 LOCAL DATA
Note that sorting the list in this way will be more efficient than loading
the list into memory and then sorting the items.
317
CHAPTER 10 LOCAL DATA
if (board is null)
{
return null;
}
board.BoardWidgets = widgets;
return board;
}
The first line calling Find allows you to find an entity with the supplied
primary key value. This retrieves the Board. Next, you need to retrieve the
collection of BoardWidgets. This is performed in a very similar manner
to loading your collection of Boards. Finally, you assign the widgets you
loaded into the board before returning it to the caller.
It is worth noting that the Sqlite-net-pcl package does not provide more
complex querying operations such as joins. If this is something that you
still require, it is possible to write the SQL directly and execute against
the connection. If you wish to join your Board and BoardWidget tables
together, you can achieve this as follows:
Note that the above query is purely aimed at showing how joins work;
it does not provide you with any particularly useful in the context of your
application.
318
CHAPTER 10 LOCAL DATA
LiteDB
LiteDB is a simple, fast, and lightweight embedded .NET document
database. LiteDB was inspired by the MongoDB database, and its API is
very similar to the official MongoDB .NET API.
319
CHAPTER 10 LOCAL DATA
Installing LiteDB
In order to install and use LiteDB, you need to install the NuGet package
called LiteDB. Don’t worry; it is perfectly fine to install both the LiteDB
and SQLite packages side by side into your project. In fact, that is precisely
what you will do here.
You can do this by following these steps:
Using LiteDB
The first step is to create your IBoardRepository implementation. Add a
new class file called LiteDBBoardRepository in your Data folder, and make
it implement your IBoardRepository interface.
using LiteDB;
using WidgetBoard.Models;
namespace WidgetBoard.Data;
320
CHAPTER 10 LOCAL DATA
builder.Services.AddTransient<IBoardRepository,
LiteDBBoardRepository>();
321
CHAPTER 10 LOCAL DATA
The above should look very similar to the SQLite way of accessing the
database. Here you make use of the IFileSystem implementation you
registered in the previous section. Then you make use of that to determine
where to store your database file. Finally, you open a connection using the
path to your database file. Note that if the file does not exist, one will be
created for you.
Then you need to get access to that collection in order to allow you to
perform your operations against it.
boardCollection = database.GetCollection<Board>("Boards");
boardWidgetCollection
= database.GetCollection<BoardWidget>("BoardWidgets");
323
CHAPTER 10 LOCAL DATA
You also need to add the following line to your constructor to make
sure querying is possible:
324
CHAPTER 10 LOCAL DATA
if (board is null)
{
return null;
}
return board;
}
The first line calls the FindById method, which allows you to find an
entity with the supplied primary key value. This retrieves the Board. Next,
you need to retrieve the collection of BoardWidgets. This is performed in a
very similar manner to loading your collection of Boards. Finally, you assign
the widgets you loaded into the board before returning it to the caller.
325
CHAPTER 10 LOCAL DATA
Database Summary
There is an abundance of options when it comes to choosing not only
which database but then also the Object Relational Mapping (ORM) layer
on top of it. The aim of this section is to give a taste of what some options
offer and to encourage you to decide which will benefit your application
and team most.
Both options I covered provide support for encryption; SQLite requires
that you install an additional package called sqlite-net-sqlcipher, and
LiteDB supports encryption out of the box.
I strongly encourage you to evaluate which database will provide you
with the best development experience and the users of your application
with the best user experience. Some databases perform better in different
scenarios.
Moving forward with this application, you will continue to use LiteDB.
326
CHAPTER 10 LOCAL DATA
builder.Services.AddSingleton(Preferences.Default);
327
CHAPTER 10 LOCAL DATA
Having the ability to provide a String value surely means you could
in theory store anything in there, right? While this is technically possible,
it is highly recommended that you only store small amounts of text.
Otherwise, the performance of storing and retrieval can be impacted in
your applications.
public FixedBoardPageViewModel(
WidgetTemplateSelector widgetTemplateSelector,
WidgetFactory widgetFactory,
IBoardRepository boardRepository,
IPreferences preferences)
{
WidgetTemplateSelector = widgetTemplateSelector;
this.preferences = preferences;
Widgets = new ObservableCollection<IWidgetViewModel>();
}
328
CHAPTER 10 LOCAL DATA
This means that every time a user opens a board to view it, the ID will
be remembered in Preferences. When the application is opened again in
the future, it will use that ID to open the last viewed board.
A possible alternative way of achieving this type of functionality could
be to maintain a last opened column in the database and always find the
latest of that set.
329
CHAPTER 10 LOCAL DATA
public AppShellViewModel(
IBoardRepository boardRepository,
IPreferences preferences,
IDispatcher dispatcher)
{
this.boardRepository = boardRepository;
this.preferences = preferences;
this.dispatcher = dispatcher;
}
330
CHAPTER 10 LOCAL DATA
{
lastUsedBoard = board;
}
}
if (lastUsedBoard is not null)
{
dispatcher.Dispatch(() =>
{
BoardSelected(lastUsedBoard);
});
}
}
There are a few new concepts here, so let’s break them down into
understandable chunks.
First is the use of the preferences.Get method, as you learned about
before writing the above code. You supply the key name and the default
value to be returned if the key does not exist. You use -1 for the default
because it is not a valid ID for a database key.
The final new concept is the use of the IDispatcher implementation
provided by .NET MAUI. This allows you to trigger a deferred action and
make sure that it is dispatched onto the UI thread. Your method will be
called on the UI thread, but you want the OnAppearing logic to finish
before you attempt to navigate somewhere; by calling dispatcher.
Dispatch, you are queuing up an action to be performed once the UI
thread is no longer busy. .NET MAUI does handle a lot of dispatching for
you when you trigger updates in bindings, but there are times when you
need to make sure that you are updating things on the UI thread.
If you run your code now, you can create a new board and view it once
saved. If you then close and reopen the application, you will see that the
board you created is now shown for you. Providing an experience like
this can go a long way to an enjoyable user experience (UX) as they are
returning to where they were previously.
331
CHAPTER 10 LOCAL DATA
you could have first checked whether the key existed, like
if (preferences.ContainsKey("LastUsedBoardId"))
{
// Perform your logic
}
Removing a Preference
There may be times when you need to remove an option from the
Preferences store or even remove all options. If you want to remove your
LastUsedBoardId preference, you can write
Preferences.Remove("LastUsedBoardId");
Preferences.Clear();
<HorizontalStackLayout>
<Label
Text="{Binding LastUsedBoard}"
MinimumWidthRequest="200"
VerticalOptions="Center" />
<Button
Text="Clear"
Command="{Binding ClearLastUsedBoardCommand}"
SemanticProperties.Hint="Clears the last used
board value from settings. This means the
application won't automatically load a board
when opened." />
</HorizontalStackLayout>
</VerticalStackLayout>
</ContentPage>
The above makes use of all the good practices that we have covered
so far: adding in compiled bindings, including accessibility information
through SemanticProperties, etc. The key detail is that we have added a
Label which will display the name of the last used board and a Button to
allow for clearing this value.
333
CHAPTER 10 LOCAL DATA
using WidgetBoard.ViewModels;
namespace WidgetBoard.Pages;
using System.Windows.Input;
using WidgetBoard.Data;
namespace WidgetBoard.ViewModels;
334
CHAPTER 10 LOCAL DATA
if (lastUsedBoardId != -1)
{
LastUsedBoard = boardRepository.
LoadBoard(lastUsedBoardId)?.Name ?? string.Empty;
}
335
CHAPTER 10 LOCAL DATA
This concludes how you can store, load, and remove application
settings. Now let’s proceed to learning about how to secure application
settings.
Secure Storage
When building an application, there will quite often be an occasion where
you need to store an API token or some form of data that needs to be held
securely. .NET MAUI provides another API that makes sure that the values
you supply are held securely on each of the platforms’ secure storage
locations.
As always with a new API provided by .NET MAUI, you must register it
with the MauiAppBuilder in your MauiProgram.cs file, so let’s open up that
file and add the following line into the CreateMauiApp method:
builder.Services.AddSingleton(SecureStorage.Default);
<Label
Text="Open Weather API token"
VerticalOptions="Center" />
<HorizontalStackLayout>
<Entry
336
CHAPTER 10 LOCAL DATA
Text="{Binding OpenWeatherApiToken}"
MinimumWidthRequest="200"
IsPassword="True" />
<Button
Text="Save"
Command="{Binding SaveApiTokenCommand}"
SemanticProperties.Hint="Saves the currently entered
Open Weather API token into secure storage." />
</HorizontalStackLayout>
As before, most of this should feel familiar; one key detail to highlight
is the use of the IsPassword property on the Entry element – this allows
you to add an entry field that will mask the entered characters with the *
character and therefore protect the value from prying eyes. Now that we
have added in the UI, let’s open the SettingsPageViewModel.cs file and
actually interact with the ISecureStorage API.
Add the following fields and properties to the class:
And then you can modify the constructor to look as follows (with
changes in bold):
public SettingsPageViewModel(
IPreferences preferences,
IBoardRepository boardRepository,
337
CHAPTER 10 LOCAL DATA
ISecureStorage secureStorage)
{
var lastUsedBoardId = preferences.
Get("LastUsedBoardId", -1);
if (lastUsedBoardId != -1)
{
LastUsedBoard = boardRepository.
LoadBoard(lastUsedBoardId)?.Name ?? string.Empty;
}
OpenWeatherApiToken = secureStorage.GetAsync(
"OpenWeatherApiToken").GetAwaiter().GetResult() ??
string.Empty;
}
338
CHAPTER 10 LOCAL DATA
await secureStorage.SetAsync("OpenWeatherApiToken",
OpenWeatherApiToken);
OpenWeatherApiToken = secureStorage.GetAsync(
"OpenWeatherApiToken").GetAwaiter().GetResult() ??
string.Empty;
SecureStorage.Default.RemoveAll();
Platform Specifics
As mentioned, the SecureStorage API makes use of each of the platform-
specific APIs to handle the actual storage of the data you pass in. It is worth
noting that the implementations for each individual platform are different
and may change in the operating systems but SecureStorage will leverage
whatever is in the operating system and therefore will always be the most
secure option. This section explains how.
339
CHAPTER 10 LOCAL DATA
Android
The data you pass in is encrypted with the Android
EncryptedSharedPreferences class, from the Android Security library,
which automatically encrypts keys and values using a two-scheme
approach:
340
CHAPTER 10 LOCAL DATA
Windows
SecureStorage on Windows uses the DataProtectionProvider class to
encrypt values securely. The .NET MAUI implementation allows for the
data to be protected against the local user or computer account.
For further reading, refer to the Microsoft documentation at https://
docs.microsoft.com/uwp/api/windows.security.cryptography.
dataprotection.dataprotectionprovider?view=winrt-22621.
341
CHAPTER 10 LOCAL DATA
Figure 10-2. The application loads back up and shows the previously
added widgets
Then selecting the Settings tab will present the user with the new
settings-based page you just added. Figure 10-3 shows an example of the
settings tab page.
342
CHAPTER 10 LOCAL DATA
Figure 10-3. The application showing the settings page to the user
Summary
In this chapter, you have
• Learned about the different types of local data
343
CHAPTER 10 LOCAL DATA
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch10.
344
CHAPTER 10 LOCAL DATA
Extra Assignment
You have provided the ability for users to add widgets to their boards
and automatically save them so when they next load the board, it will
be remembered for them. I would like to see if you can add the ability to
remove the widgets from the board and the database.
Source Code
I would love for you to have an attempt at this extra assignment, but I have
also provided the source code. The source code for this extra assignment
can be found on the GitHub repository at https://github.com/Apress/
Introducing-.NET-MAUI-2nd-ed/tree/main/ch10-extra.
345
CHAPTER 11
Remote Data
Abstract
In this chapter, you will be exploring the topic of remote data, learning what
exactly it is, types of it, how to interact with it, and what to consider when
doing so. You will then build upon this learning by building a new widget,
the Weather Widget, to display the current weather. This will be done by
interacting directly with the Open Weather API. You will get exposure
to handling HTTP requests and responses with an API, how to handle
the response being in a JSON format, and the varying levels of flexibility
when mapping to the JSON data. You will finish off by simplifying the
implementation with a fantastic NuGet package that generates source code
for you, simply from an interface you define to represent the web service.
Loading Times
One of the worst experiences for a user is to tap on a button or open a
new page/application and just see the application lock up while it is
loading data. The user will think that the application has crashed, and
in fact, platforms like Android and Windows will likely indicate that the
application has crashed/locked up if the load takes too long. Thankfully
.NET offers you the async and await keywords. They are not essential, but
they really do make your life easier. There could be an entire chapter or
even book on this topic; however, my good friend Brandon Minnick has
already covered a lot of this in his AsyncAwaitBestPractices repository
on GitHub. We will be covering the basics in order to build a responsive
application. I thoroughly recommend you do if you want to dig deeper
(https://github.com/brminnick/AsyncAwaitBestPractices).
A common use case is to display a visual that makes it clear to the
user that the application is busy loading. This can be with a simple
ActivityIndicator, which loads the platform-specific spinner/loading
icon users should feel familiar with, or you can make use of the animation
features I covered in Chapter 9 to show something more involved. With
this loading display, you then initiate your web service call. If you get a
response, you display the result of that response in your application (e.g.,
items in a shopping list or, in your scenario, the user’s current weather).
348
CHAPTER 11 REMOTE DATA
Failures
During the building of a recent application, some of the most valuable
testing a friend provided for me was to install the application and then ride
the London Underground and observe just how flaky a mobile phone’s
data connection really can be.
There are two key questions to consider when dealing with network
connectivity issues:
1. What does the user need to know?
2. How does the application need to recover?
Don’t worry, I will be covering examples of how to answer these
questions as we build our weather widget.
Security
As a developer of applications, it is essential that you maintain the trust
that your users put in you with regard to keeping their data safe. With
this in mind, you should always choose HTTPS over HTTP. In fact, most
platforms won’t allow HTTP traffic by default to avoid it accidentally being
used. There are ways to disable the prevention of HTTP traffic; however, I
strongly advise against it, so I won’t cover how to do so in this book.
I strongly recommend that as you build your applications, you consider
security as a top priority. The Open Web Application Security Project
(OWASP) is a nonprofit foundation that works to improve the security of
software, and it provides some really great resources and guidance on what
you should consider when building websites and mobile applications. As a
good starting point, look at their Mobile Application Security Testing Guide
repository on GitHub at https://github.com/OWASP/owasp-mastg/.
Quite often APIs will require levels of authentication that complicate the
flow to pulling data from them. This typically happens when your application
needs to consume data specific to a user and not just the API itself. I won’t
349
CHAPTER 11 REMOTE DATA
Web Services
Web services act as a mechanism to query or obtain data from a remote
server. They typically offer many advantages to developers building them
because they provide the ability to charge based on usage, protect the
developer’s intellectual property, and other reasons.
350
CHAPTER 11 REMOTE DATA
https://api.openweathermap.org/data/2.5/weather?lat=20.7984&
lon=-156.3319&units=metric&appid=APIKEY
351
CHAPTER 11 REMOTE DATA
You can open this in any web browser to view the following response back;
just make sure to replace the APIKEY text with your own API key. You can see
the key details that you will need for your application highlighted in bold.
{
"coord": {
"lon":-156.3319,
"lat":20.7984
},
"weather":[
{
"id":802,
"main":"Clouds",
"description":"scattered clouds",
"icon":"03d"
}
],
"base":"stations",
"main": {
"temp":22.73,
"feels_like":22.96,
"temp_min":21.23,
"temp_max":24.1,
"pressure":1017,
"humidity":73,
"sea_level":1017,
"grnd_level":945
},
"visibility":10000,
"wind": {
"speed":3.09,
"deg":300
},
352
CHAPTER 11 REMOTE DATA
"clouds": {
"all":40
},
"dt":1729711746,
"sys": {
"type":2,
"id":18862,
"country":"US",
"sunrise":1729700629,
"sunset":1729742100
},
"timezone":-36000,
"id":5852697,
"name":"Pukalani",
"cod":200
}
Using System.Text.Json
In order to consume and deserialize the contents of the JSON returned to
you, you need to use one of the following two options:
• System.Text.Json
Newtonsoft has been around for many years and is a go-to option for
many developers. System.Text.Json has become its successor and is my
recommendation for this scenario, especially as it is backed by Microsoft
and James Newton-King, the author of Newtonsoft, who works for
Microsoft.
Let’s go ahead and use System.Text.Json as it is the recommended way
to proceed and is included with .NET MAUI out of the box.
353
CHAPTER 11 REMOTE DATA
Now that you have seen what the data looks like, you can start to build
the model classes that will allow you to deserialize the response coming
back from the API.
namespace WidgetBoard.Communications;
Your Weather class maps to the weather element in the JSON returned
from the API. You can see that you are mapping to the main and icon
elements and you have added a calculated property that returns a URL
pointing to the icon provided by the Open Weather API. The last property
you are mapping, IconUrl, is yet another great example of remote data.
The API provides you with an icon that can be rendered inside your
354
CHAPTER 11 REMOTE DATA
using System.Text.Json.Serialization;
namespace WidgetBoard.Communications;
This class will currently only map to the current Temperature, there are
many other values that you could map to if you wish to show more detail
in your widget. With the Temperature property mapping, you can see how
it is possible to map from a property in your model to an element in JSON
that has a different name. This functionality is extremely valuable when
building your own models because it allows you to name the properties to
provide better context. I personally prefer to avoid abbreviations and stick
with explicit names to make the intentions of the code clear.
355
CHAPTER 11 REMOTE DATA
Your final model class to add should be called Forecast.cs and will
have the following contents:
namespace WidgetBoard.Communications;
This class maps to the top-level element in the returned JSON. You are
mapping to the Timezone element, the Current, which will contain your
previously mapped values, and an array of Weather elements.
Now that you have created the model classes that can be mapped to
the JSON returned from the Open Weather API, you can proceed to calling
the API in order to retrieve that JSON.
namespace WidgetBoard.Communications;
using System.Text.Json;
namespace WidgetBoard.Communications;
response.EnsureSuccessStatusCode();
357
CHAPTER 11 REMOTE DATA
You added a fair amount into this class file, so let’s walk through it step
by step and cover what it does.
First is the HttpClient backing field, which is set within the
constructor and will be supplied by the dependency injection layer. You
also have a constant representing the URL of the API.
Next is the main piece of functionality in the GetForecast method. The
first line in this method handles connecting to the Open Weather API and
passing your latitude, longitude, and API key values. You also make sure
to set ConfigureAwait(false) because you do not need to be called back
on the initial calling thread. This helps to boost performance a little as it
avoids having to wait until the calling thread becomes free.
Then you make sure that the request was handled successfully
by calling
response.EnsureSuccessStatusCode();
Note that the above will throw an exception if the status code received
was not a 200 (success ok).
358
CHAPTER 11 REMOTE DATA
WeatherWidgetView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentView
359
CHAPTER 11 REMOTE DATA
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:WidgetBoard.ViewModels"
x:Class="WidgetBoard.Views.WeatherWidgetView"
x:DataType="viewModels:WeatherWidgetViewModel">
<VerticalStackLayout>
<Label
Text="Today"
FontSize="20"
VerticalOptions="Center"
HorizontalOptions="Start"
TextTransform="Uppercase" />
<Label
VerticalOptions="Center"
HorizontalOptions="Center">
<Label.FormattedText>
<FormattedString>
<Span
Text="{Binding Temperature,
StringFormat='{0:F1}'}"
FontSize="60"/>
<Span
Text="°C" />
</FormattedString>
</Label.FormattedText>
</Label>
<Label
Text="{Binding Weather}"
FontSize="20"
VerticalOptions="Center"
HorizontalOptions="Center" />
360
CHAPTER 11 REMOTE DATA
<Image
Source="{Binding IconUrl}"
WidthRequest="100"
HeightRequest="100"/>
</VerticalStackLayout>
</ContentView>
Some of the above XAML should feel familiar based on the previous
code you have written. Some bits are new, so let’s cover them.
Label.FormattedText enables you to define text of varying formats
inside a single Label control. This can be helpful especially when parts of
the text change dynamically in length and therefore result in the contents
moving around. In your example, you are adding a Span with a text binding
to your Temperature property in the view model and a second Span with
the degrees Celsius symbol.
The second new concept is the use of Image. The binding on the Source
property looks relatively straightforward; however, it is worth noting that
.NET MAUI works some magic for you here. You are binding a string to the
property. Under the hood, .NET MAUI converts the string into something
that can resemble an image source. In fact, the underlying type is called
ImageSource. Further to this, it will inspect your string, and if it contains a
valid URL (e.g., starts with https://), then it will aim to load it as a remote
image rather than looking in the application’s set of compiled resources. .NET
MAUI will also potentially handle caching of images for you to help reduce
the amount of requests sent in order to load images from a remote source. In
order to make use of this functionality, you need to provide a UriImageSource
property on your view model rather than the string property.
The process of converting from one type to another is referred to as
TypeConverters and can be fairly common in .NET MAUI. I won’t go into
detail on how they work, so please go to the Microsoft documentation site
at https://learn.microsoft.com/dotnet/api/system.componentmodel.
typeconverter.
361
CHAPTER 11 REMOTE DATA
WeatherWidgetView.xaml.cs
You also need to make the following adjustments to the
WeatherWidgetView.xaml.cs file. This part is required because you
haven’t created a common base class for the widget views. At times there
can be good reasons to create them; however, because you want to keep
the visual tree as simple as possible, there isn’t a common visual base
class to use.
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
Now that you have created your widget view, you should create the
view model that will be paired with it.
362
CHAPTER 11 REMOTE DATA
proceed to adding the familiar bits and then walk through the newer
concepts. First, add a new class file in the ViewModels folder and call it
WeatherWidgetViewModel.cs. The initial contents should be modified to
look as follows:
using WidgetBoard.Communications;
namespace WidgetBoard.ViewModels;
public WeatherWidgetViewModel(IWeatherForecastService
weatherForecastService, ISecureStorage secureStorage)
{
this.weatherForecastService = weatherForecastService;
this.secureStorage = secureStorage;
Task.Run(async () => await LoadWeatherForecast());
}
363
CHAPTER 11 REMOTE DATA
if (apiKey is null)
{
return;
}
if (forecast?.Main is null)
{
return;
}
Temperature = forecast.Main.Temperature;
Weather = forecast.Weather.First().Main;
IconUrl = forecast.Weather.First().IconUrl;
}
Inside of your constructor, you keep a copy of the service and you also
start a background task to fetch the forecast information. Quite often you
wouldn’t start something like this from within a constructor; however,
given that you know your view model will only be created when it is being
added to the UI, this is perfectly acceptable.
Finally, you need to add the properties that your view wants to bind to.
364
CHAPTER 11 REMOTE DATA
That’s all you need in the view model for now. You can now register the
widget and get it ready for your first test run.
365
CHAPTER 11 REMOTE DATA
Inside your MauiProgram.cs file, you need to add the following lines
into the CreateMauiApp method:
builder.Services.AddHttpClient<WeatherForecastService>();
builder.Services.AddSingleton<IWeatherForecastService,
WeatherForecastService>();
WidgetFactory.RegisterWidget<WeatherWidgetView, WeatherWidgetVi
ewModel>(WeatherWidgetViewModel.DisplayName);
builder.Services.AddTransient<WeatherWidgetView>();
builder.Services.AddTransient<WeatherWidgetViewModel>();
The above code registers your widget’s view and view models with the
dependency injection layer and also registers it with your WidgetFactory,
meaning it can be created from your add widget overlay.
366
CHAPTER 11 REMOTE DATA
Then you can open a board and add a weather widget; you can see the
result in Figure 11-2.
367
CHAPTER 11 REMOTE DATA
This works fine provided you have a good network connection. The
moment you have a slow connection or even no connection, you will
notice that things don’t load quite as expected. In fact, you will likely
observe a crash. You knew this could happen based on your earlier
investigation into the things you need to consider when handling remote
data. Let’s now apply some techniques to handle these scenarios.
368
CHAPTER 11 REMOTE DATA
namespace WidgetBoard;
You also want to modify your loading code in the view model to make
use of this new State, with the changes in bold.
369
CHAPTER 11 REMOTE DATA
if (apiKey is null)
{
return;
}
try
{
State = State.Loading;
if (forecast?.Main is null)
{
State = State.Error;
return;
}
Temperature = forecast.Main.Temperature;
Weather = forecast.Weather.First().Main;
IconUrl = forecast.Weather.First().IconUrl;
State = State.Loaded;
}
catch (Exception)
{
State = State.Error;
}
}
The example above hasn’t added any extra logging, but I would
strongly advise that inside the catch statement, you log errors out so that
you can investigate the reason for the error.
370
CHAPTER 11 REMOTE DATA
And you also need to add the State property and backing field.
using System.Globalization;
namespace WidgetBoard.Converters;
371
CHAPTER 11 REMOTE DATA
372
CHAPTER 11 REMOTE DATA
373
CHAPTER 11 REMOTE DATA
<converters:IsEqualToStateConverter
x:Key="HasLoadedConverter"
State="Loaded" />
374
CHAPTER 11 REMOTE DATA
<Label
Text="{Binding Weather}"
FontSize="20"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Image
Source="{Binding IconUrl}"
WidthRequest="100"
HeightRequest="100"/>
</VerticalStackLayout>
<converters:IsEqualToStateConverter
x:Key="HasErrorConverter"
State="Error" />
375
CHAPTER 11 REMOTE DATA
You may have noticed that you have added a Button and bound its
command to the view model. You need to add this to your view model if
you wish to compile and run the application. The aim of the Button is to
allow the user to request a retry of loading the weather information if the
Error state is being shown.
Inside your WeatherWidgetViewModel.cs file, you need to make the
following change:
Then you need to update the constructor with the changes in bold:
public WeatherWidgetViewModel(IWeatherForecastService
weatherForecastService, ISecureStorage secureStorage)
{
this.weatherForecastService = weatherForecastService;
this.secureStorage = secureStorage;
This means that when a load fails for whatever reason, the user will
have the option to press the retry button and the widget will attempt to
load the weather details again. It will walk through the states you added, so
the UI will show the different UI options to the user as this happens.
This type of failure handling is considered manual. There are ways to
automatically handle retries through a package called Polly.
376
CHAPTER 11 REMOTE DATA
that was added earlier to load data from the Open Weather API isn’t the
most complex, but if you decided to add in the ability to handle connection
retries if a request fails and increase the delay between retry attempts, I am
sure you can imagine how complex it could become. We are going to add
such a feature in just a few lines with this new NuGet package.
Let’s go ahead and add the Microsoft.Extensions.Http.Resilience NuGet
package and then take a look at how to use it.
• Right-click the WidgetBoard solution.
• Select Manage NuGet Packages.
• Search for Microsoft.Extensions.Http.Resilience.
• Select the correct package.
• Click Add Package.
Now you can open the MauiProgram.cs file and make the following
changes (in bold):
builder.Services.AddHttpClient<WeatherForecastService>()
.AddStandardResilienceHandler(static options =>
{
options.Retry = new HttpRetryStrategyOptions
{
BackoffType = DelayBackoffType.Exponential,
MaxRetryAttempts = 3,
UseJitter = true,
Delay = TimeSpan.FromSeconds(2)
};
});
377
CHAPTER 11 REMOTE DATA
378
CHAPTER 11 REMOTE DATA
Prebuilt Libraries
I first recommend that you investigate whether the web service provider
also provides a client library to make the consumption easier. Quite
often providers supply a library, especially when there is a layer of
authentication required. There are no official client libraries for the Open
Weather API; however, there are a number of NuGet packages that provide
some support for using the API.
379
CHAPTER 11 REMOTE DATA
Now that you have the NuGet package installed, you can use it.
Open your IWeatherForecastService.cs file and make the following
modifications shown in bold:
using Refit;
namespace WidgetBoard.Communications;
The fantastic part of the above code is that you do not need to write
the implementation. Refit uses source code generators to do it for you! In
fact, it means you can delete your WeatherForecastService class as it is no
longer required.
The final change you are required to make is to change how you
register the IWeatherForecastService with your MauiAppBuilder in the
MauiProgram.cs file. Open it up and make the following changes.
First, add the using statement.
using Refit;
Then replace
builder.Services.AddSingleton<IWeatherForecastService,
WeatherForecastService>();
380
CHAPTER 11 REMOTE DATA
with
builder.Services
.AddRefitClient<IWeatherForecastService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://
api.openweathermap.org/data/2.5"));
This new line of code makes use of the Refit extension methods that
enable you to consume an implementation of IWeatherForecastService
whenever you register a dependency on that interface. It is worth
reiterating that the implementation for the IWeatherForecastService
is automatically generated for you through the Refit package. For further
reading on this package, I thoroughly recommend their website at
https://reactiveui.github.io/refit/.
If you run up the application, you will see the same result as
Figure 11-2 – the weather widgets the weather as expected.
Further Reading
You have added some complexities into your application in order to
handle the scenario when web service access doesn’t load as expected.
There is a really great library that can really help to reduce the amount of
code you need to write around these parts.
381
CHAPTER 11 REMOTE DATA
Summary
In this chapter, you have
• Learned about remote data
• Learned how you can interact with it
• Covered the common considerations
• Looked a concrete example with the Open Weather API
• Built your own implementation to consume the Open
Weather API
• Covered how to consume the data returned
• Talked through scenarios where things can go wrong
• Provided implementations to handle those scenarios
• Looked at how you can reduce the complexity of your
implementation with Refit
• Added in your Weather Widget
In the next chapter, you will
382
CHAPTER 11 REMOTE DATA
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch11.
Extra Assignment
There are so many possibilities for accessing remote data in your
application! Here are some extra widgets I would like you to consider
creating.
TODO Widget
The go-to example application to build in tutorials is a TODO application.
I would like you to expand upon this idea and add a TodoWidget into your
application. There are several TODO APIs that you could utilize to do this.
Do you have a favorite TODO service that you use? I personally like the
Microsoft TODO option. There is some good documentation over on the
Microsoft pages to help get you started at https://learn.microsoft.com/
graph/todo-concept-overview.
383
CHAPTER 11 REMOTE DATA
Source Code
I would love for you to have an attempt at this extra assignment, but I have
also provided the source code. The source code for this extra assignment
can be found on the GitHub repository at https://github.com/Apress/
Introducing-.NET-MAUI-2nd-ed/tree/main/ch11-extra.
384
CHAPTER 12
Getting Specific
Abstract
In this chapter, you will be learning about .NET MAUI Essentials and
how it enables you to access platform-specific APIs without having to
worry about any of the platform-specific complexities. Two concrete
examples are requesting permissions on each platform and accessing the
device’s geolocation information. You will explore what is required if you
really do need to interact with platform-specific APIs that have not been
abstracted for you. Finally, you will cover multiple techniques, concepts,
and architectures that enable you to tweak the UI and behavior of your
applications based on the platforms they are running on.
Permissions
A common theme I have been discussing in this book is how .NET MAUI
does a lot of the heavy lifting when it comes to dealing with each supported
platform. This continues with permissions because .NET MAUI abstracts a
large number of permissions.
It is worth noting that every operating system is different. Not
all require permissions for certain features. Refer to the Microsoft
documentation on what .NET MAUI supports and what is required for
each platform at https://learn.microsoft.com/dotnet/maui/platform-
integration/appmodel/permissions-available-permissions.
There are two key methods that enable you to interact with the
permission system in .NET MAUI.
386
CHAPTER 12 GETTING SPECIFIC
387
CHAPTER 12 GETTING SPECIFIC
Requesting Permission
Once you have confirmed that the user has not been prompted with a
permission request, you can proceed to prompting them by using the
Permissions.RequestAsync method along with the specific permission to
request. In your example, this will be the LocationWhenInUse permission.
388
CHAPTER 12 GETTING SPECIFIC
namespace WidgetBoard.Services;
389
CHAPTER 12 GETTING SPECIFIC
namespace WidgetBoard.Services;
Now that you have a blank class, you can add the method for handling
permission requests ready for use.
390
CHAPTER 12 GETTING SPECIFIC
if (Permissions.ShouldShowRationale<Permissions.
LocationWhenInUse>())
{
// Prompt the user with additional information as to
why the permission is needed
}
return status;
}
Now that you have added the ability to request the user’s permission to
use the geolocation APIs on the device, you can proceed to using it.
builder.Services.AddSingleton(Geolocation.Default);
391
CHAPTER 12 GETTING SPECIFIC
392
CHAPTER 12 GETTING SPECIFIC
This implementation first makes sure that you are running on the main
thread, which is required for location-based access. Then it calls your
permission handling method, and if the app has permission, it calls the
IGeolocation implementation and returns the resulting Location object.
Now you are ready to make use of the LocationService.
builder.Services.AddSingleton<ILocationService,
LocationService>();
public WeatherWidgetViewModel(
IWeatherForecastService weatherForecastService,
ISecureStorage secureStorage,
ILocationService locationService)
393
CHAPTER 12 GETTING SPECIFIC
{
this.weatherForecastService = weatherForecastService;
this.locationService = locationService;
LoadWeatherCommand = new Command(async () => await
LoadWeatherForecast());
}
Modify your State enum to include a new value so that you can
handle when something goes wrong with permission access. Add a
PermissionError value, as can be seen below in bold.
if (apiKey is null)
{
return;
}
394
CHAPTER 12 GETTING SPECIFIC
try
{
State = State.Loading;
Temperature = forecast.Main.Temperature;
Weather = forecast.Weather.First().Main;
IconUrl = forecast.Weather.First().IconUrl;
State = State.Loaded;
}
catch (Exception ex)
{
State = State.Error;
}
}
You have introduced a few changes here, so let’s break them down.
First, you are calling the locationService to get the device’s location.
If it returns null, it means the application does not have permission and
you set the State to PermissionError.
If you have permission, you pass the device’s current location into the
weatherForecastService.GetForecast method.
395
CHAPTER 12 GETTING SPECIFIC
<converters:IsEqualToStateConverter
x:Key="HasPermissionErrorConverter"
State="PermissionError" />
Then you can add a section that will render when the State
property is equal to PermissionError. You should add this into the
WeatherWidgetView.xaml file after the following section:
396
CHAPTER 12 GETTING SPECIFIC
<Button
Text="Retry"
Command="{Binding LoadWeatherCommand}" />
</VerticalStackLayout>
Now that you have added all of the required bits of code to call into
the Permissions and Geolocation APIs, you need to configure each of your
supported platforms to enable the location permission.
Android
Android requires several permissions and features to be configured in
order for your application to use the LocationWhenInUse permission. You
can configure them inside the Platforms/Android/MainApplication.cs
file, so open it and make the following additions in bold:
397
CHAPTER 12 GETTING SPECIFIC
using Android.App;
using Android.Runtime;
[assembly: UsesPermission(Android.Manifest.Permission.
AccessCoarseLocation)]
[assembly: UsesPermission(Android.Manifest.Permission.
AccessFineLocation)]
[assembly: UsesFeature("android.hardware.location", Required
= false)]
[assembly: UsesFeature("android.hardware.location.gps",
Required = false)]
[assembly: UsesFeature("android.hardware.location.network",
Required = false)]
namespace WidgetBoard;
Note that the use of the assembly keyword requires that the attributes
are applied at the assembly level and not on the class like the current
[Application] attribute usage. For further reference on how to get started
with geolocation, refer to the Microsoft documentation at https://
learn.microsoft.com/dotnet/maui/platform-integration/device/
geolocation?tabs=android-get-started.
If you run the application on Android now, you will see that the first
time you add a Weather widget onto a board, the system will present the
following popup to the user asking them to allow permission for your
application to use the location feature. Figure 12-1 shows the result of
running your application on Android.
398
CHAPTER 12 GETTING SPECIFIC
iOS/Mac
Apple requires that you specify the reason your application wants to use
the Geolocation feature in the process of defining that your application
uses the feature. You can configure this by modifying the Platforms/iOS/
Info.plist and Platforms/MacCatalyst/Info.plist files for iOS and
399
CHAPTER 12 GETTING SPECIFIC
Mac Catalyst, respectively. Both files require the same change, so let’s open
them and add the following lines in. Note that I am opting to edit the files
inside Visual Studio Code as I find it provides a better editing experience.
There is a built-in editor inside Visual Studio, but I personally prefer to edit
the XML directly. Add the following lines inside the <dict> element:
<key>NSLocationWhenInUseUsageDescription</key>
<string>In order to provide accurate weather
information.</string>
400
CHAPTER 12 GETTING SPECIFIC
401
CHAPTER 12 GETTING SPECIFIC
Windows
Windows applications have the concept of capabilities, and it is up to
developers to declare which capabilities are required in their applications.
In order to do so for your application, you need to modify the Platforms/
Windows/Package.appxmanifest file. Note that I am opting to edit the files
inside Visual Studio Code as I find it provides a better editing experience.
Add the following line inside the <Capabilities> element:
<DeviceCapability Name="location"/>
For further reference on how to get started with Geolocation, refer to the
Microsoft documentation at https://learn.microsoft.com/dotnet/maui/
platform-integration/device/geolocation?tabs=windows-get-started.
If you run the application on Windows now, you don’t see a permission
request popup. Figure 12-3 shows the result of running the application on
Windows.
402
CHAPTER 12 GETTING SPECIFIC
namespace WidgetBoard.Services;
403
CHAPTER 12 GETTING SPECIFIC
#else
location = new Location(37.334722, -122.008889);
#endif
return Task.FromResult(location);
}
}
The above code will be compiled in different ways based on the target
platform. The resulting compiled code for the Android platform looks as
follows:
namespace WidgetBoard.Services;
This means that only the code specific to the platform will be compiled
and shipped to that platform.
This approach can work well in this scenario, but as soon as you need
to use multiple classes or other platform-specific libraries, the code will
become complex very quickly. In more complex scenarios, you can use the
platform-specific folders created in your project for you.
404
CHAPTER 12 GETTING SPECIFIC
namespace WidgetBoard.Services;
The above code will not compile now because you haven’t implemented
ILocationService. This is expected until you add in your platform-specific
implementations, so don’t worry. You add the partial keyword because this
is only a partial implementation. The platform-specific files and classes you
will add shortly will complete this partial implementation.
Next, you need to create your Android platform-specific
implementation. To do this, you add a new class file under the
/Platforms/Android/ folder and call it PlatformLocationService.cs,
just like the one above. You want to modify its contents to the following:
namespace WidgetBoard.Services;
405
CHAPTER 12 GETTING SPECIFIC
This class will only be compiled when the Android platform is being
targeted, and therefore, you get a very similar compiled output to the one
in the “Platform-Specific Code with Compiler Directives” section. The key
difference is that you don’t need to add any of those unpleasant #if directives.
When building platform-specific implementations this way, the
namespace of your partial classes must match! Otherwise, the compiler
won’t be able to build a single class.
We now need to add in the implementations for iOS, macOS, and
Windows; rather than stepping through the same steps as the Android
implementation above, we will mix and match the two approaches that we
have just covered: compiler directives and platform-specific folders.
Open the PlatformLocationService.cs file in the Services folder and
modify the contents to match the below (with changes in bold)
namespace WidgetBoard.Services;
406
CHAPTER 12 GETTING SPECIFIC
#endif
return Task.FromResult<Location?>(location);
}
#endif
}
OnPlatform
A common example of needing to change control properties is around the
sizing of text or spacing around controls (Margin or Padding). I always find
that the final finishing touches to get an application feeling really slick and
polished can result in needing to tweak details like this per platform. There
are two main ways to achieve this, and they depend on whether you are a
XAML- or C#-oriented UI builder. Let’s look over both with an example.
407
CHAPTER 12 GETTING SPECIFIC
The code above shows that the FontSize property is currently fixed to
a value of 80. With the OnPlatform markup extension, you can change this
value based on the platform the application is running on. The following
408
CHAPTER 12 GETTING SPECIFIC
code example shows how you can retain the default value of 80 and then
override for the platforms that you wish:
The code example above states that all platforms will default to using a
FontSize of 60 unless the application is running on Android and a value of
25 will be used or if the application is running on iOS and a value of 30 will
be used.
Conditional Statements
If you had built your UI in C# or wanted to at least modify the FontSize
property of a Label control in a similar way, you could write the following
conditional C# statement:
public ClockWidgetView()
{
if (DeviceInfo.Platform == DevicePlatform.Android)
{
FontSize = 25;
}
else if (DeviceInfo.Platform == DevicePlatform.iOS)
{
FontSize = 30;
}
else
{
FontSize = 60;
}
}
409
CHAPTER 12 GETTING SPECIFIC
Handlers
Handlers are an area where .NET MAUI really shines! If you have
come from a Xamarin.Forms background, you will appreciate the pain
that custom renderers brought. If you don’t have any Xamarin.Forms
experience, you are very lucky! I won’t dig down too deep into the details
of the old approach as this is a book on .NET MAUI and not the past;
however, I feel there is value in talking about the old issues and how they
have been overcome by the new handler architecture.
In both Xamarin.Forms and .NET MAUI, we predominantly build our
user interfaces with abstract controls: controls defined in the Microsoft
namespace and not specifically any platform controls. These controls
eventually need to be mapped down to the platform-specific layer. In the
Xamarin.Forms days, you would have a custom renderer. The renderer
would be responsible for knowing about the abstract control and also the
platform-specific control and mapping property values and event handlers
and such between the two. This is considered a tightly coupled design,
meaning that it becomes really quite difficult to enhance the controls and
their rendering. If you wanted to override a small amount of behavior,
you would have to implement a full renderer responsible for mapping all
properties/events. This was very painful!
410
CHAPTER 12 GETTING SPECIFIC
411
CHAPTER 12 GETTING SPECIFIC
examples to show how it could have been implemented. Figure 12-5 shows
how the BoardLayout class would map onto platform-specific controls on
each layer.
You didn’t take this approach in your scenario because there was no
benefit. In fact, it would result in more code because you would need to
map to each platform individually as well as implementing the layout
logic in each of those platform-specific layers. This concept may sound
like it will always cause more effort; however, in the situation of a Button,
it makes sense because each platform already has a definition of what a
button is and how it behaves.
Quite often as application developers, you will be using existing
controls rather than building your own controls, so rather than needing
to build everything you see in Figure 12-5, you can customize controls
through the use of handlers.
412
CHAPTER 12 GETTING SPECIFIC
handler.PlatformView.SetSelectAllOnFocus(true);
413
CHAPTER 12 GETTING SPECIFIC
If you are familiar with any specific platform development (e.g., iOS
development in Swift), the code to perform the selecting of all text should
look familiar to doing it in that language (e.g. Swift), and that is because it
is using the platform-specific APIs.
This covers how to perform the selecting of all text when an Entry
gains focus; the final steps cover how to register this behavior to be
performed.
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
"SelectAllText", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetSelectAllOnFocus(true);
#elif IOS || MACCATALYST
handler.PlatformView.EditingDidBegin += (s, e) =>
414
CHAPTER 12 GETTING SPECIFIC
{
handler.PlatformView.PerformSelector(new ObjCRuntime.
Selector("selectAll"), null, 0.0f);
};
#elif WINDOWS
handler.PlatformView.GotFocus += (s, e) =>
{
handler.PlatformView.SelectAll();
};
#endif
});
The result of the above will be to append our custom mapping that we
are naming “SelectAllText” to the EntryHandler, which means all Entry
controls will inherit this mapping.
public BoardDetailsPage(BoardDetailsPageViewModel
boardDetailsPageViewModel)
{
InitializeComponent();
BindingContext = boardDetailsPageViewModel;
Microsoft.Maui.Handlers.EntryHandler.Mapper.Appen
dToMapping("SelectAllText", (handler, view) =>
415
CHAPTER 12 GETTING SPECIFIC
{
#if ANDROID
handler.PlatformView.SetSelectAllOnFocus(true);
#elif IOS || MACCATALYST
handler.PlatformView.EditingDidBegin += (s, e) =>
{
handler.PlatformView.PerformSelector(new
ObjCRuntime.Selector("selectAll"), null, 0.0f);
};
#elif WINDOWS
handler.PlatformView.GotFocus += (s, e) =>
{
handler.PlatformView.SelectAll();
};
#endif
});
}
The result of the above will be to append our custom mapping that we
are naming “SelectAllText” to the EntryHandler, which means all Entry
controls on the BoardDetailsPage will inherit this mapping.
416
CHAPTER 12 GETTING SPECIFIC
Summary
In this chapter, you
• Learned about permissions on the various platforms
and how to request them
• Learned how to use the Geolocation API
• Wrote your own platform-specific interaction when
necessary
• Discovered how to tweak the UI based on the platform
upon which your application is running
• Further tweaked the UI through the use of the handler
architecture
In the next chapter, you will
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch12.
417
CHAPTER 12 GETTING SPECIFIC
Extra Assignment
You have only scratched the surface on the platform integration APIs that
.NET MAUI offers you. I would love for you to look over the other possible
APIs and build your own widgets that would benefit from them. The
documentation for the platform integration APIs can be found at https://
learn.microsoft.com/dotnet/maui/platform-integration/.
Barometer Widget
You can make use of the Barometer API in order to report the ambient
air pressure back to the user. In fact, this might be a good addition to the
Weather widget rather than a whole new widget. The documentation for
this API can be found at https://learn.microsoft.com/dotnet/maui/
platform-integration/device/sensors?#barometer.
Geocoding Lookup
I am reluctant to enable permissions like location access to apps I don’t
believe really need them. Perhaps you can enhance your Weather widget
to allow the user to supply their nearest city, town, or postal code and
then use the Geocoding API to reverse lookup the longitude and latitude
information required for the Open Weather API. The documentation
for the Geocoding API can be found at https://learn.microsoft.com/
dotnet/maui/platform-integration/device/geocoding.
Source Code
I would love for you to have an attempt at this extra assignment, but I have
also provided the source code. The source code for this extra assignment
can be found on the GitHub repository at https://github.com/Apress/
Introducing-.NET-MAUI-2nd-ed/tree/main/ch12-extra.
418
CHAPTER 13
Testing
Abstract
Testing is such an important part of the software development process;
it enables you to verify that what you have delivered is what was required
and also validate that the software behaves correctly. It also provides the
safety net of catching regressions in the products that you build.
There are many different approaches for designing and writing tests
and where they fit into the software development process. This chapter is
not intended to provide full insight into those approaches, but it will expose
you to various methods of testing a .NET MAUI application, why they can be
beneficial, and pique your interest in learning to use them in more depth.
Unit Testing
Unit testing is the process of ensuring that small units, typically a method
or class, of an application meet their design and behave as intended. One
big benefit of testing such a small unit of the code is that it makes it easier
for you to identify where issues may lie or creep in as part of regression.
I have worked on many legacy systems throughout my career where the
teams neglected to apply unit testing, and the experience when trying to
identify the cause of a bug in a large system really can be costly in terms of
time and money.
Despite unit testing featuring near the end of this book, it is a concept
that should be adopted early in the development process. Unit testing can
aid in the design and building of code that is easier to read and maintain
because it forces you to expose these small units of functionality and
ultimately follow SOLID principles.
Unit testing itself will not catch all bugs in the system and should not
be relied upon as a sole means of testing your applications. When used in
combination with other forms of testing such as integration, functional, or
end-to-end testing, you can build up confidence that your application is
stable and delivers what is required.
Let’s see how to implement unit testing with .NET MAUI.
xUnit
xUnit (https://xunit.net) appears to be the choice of the .NET MAUI
team. One main reason for this is likely the support around being able to
run xUnit-based unit tests on actual devices, meaning you can test device-
specific implementations.
420
CHAPTER 13 TESTING
NUnit
NUnit (https://nunit.org) is an old favorite of mine. I have used it on
so many projects in the past! It has some great features like being able to
run the same test case with multiple sets of data to reduce the amount of
testing code you need to write and ultimately maintain.
MSTest
MSTest is a testing framework that is built and supplied by Microsoft. It
doesn’t appear as feature rich as NUnit or xUnit, but it still does a great job
(https://learn.microsoft.com/dotnet/core/testing/unit-testing-
with-mstest).
421
CHAPTER 13 TESTING
you have class library projects with the code you wish to unit test, then this
can be done without the following changes. Let’s add a test project to the
solution and then make the necessary changes.
2. Click Add.
3. Click New Project.
4. Enter Test in the Search for templates box.
Figure 13-1 shows the Add a new project dialog in
Visual Studio.
422
CHAPTER 13 TESTING
8. Click Next.
9. Select the framework. The default should be fine;
just make sure it matches the target version of the
.NET MAUI application project.
10. Click Create.
<TargetFrameworks>net9.0;net9.0-android;net9.0-ios;net9.0-
maccatalyst</TargetFrameworks>
423
CHAPTER 13 TESTING
Without this second change, you will see a compilation error reporting
that error CS5001: Program does not contain a static 'Main' method
suitable for an entry point. This is due to the fact that you are building an
application and .NET applications expect to have a static Main method
as the entry point to the application. The OutputType for .NET MAUI
applications must be Exe, which might feel slightly confusing as you rarely
end up with an exe file that will be delivered.
If you are building against a newer version of .NET MAUI, you can
replace net9.0 with the version you are using, such as net10.0.
424
CHAPTER 13 TESTING
5. Click OK.
425
CHAPTER 13 TESTING
<ItemGroup>
<PackageReference Include="coverlet.collector"
Version="6.0.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"
Version="17.11.1"/>
<PackageReference Include="xunit" Version="2.9.2"/>
<PackageReference Include="xunit.runner.visualstudio"
Version="2.8.2"/>
<PackageReference Include="Microsoft.Maui.Controls"
Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Maui.Controls.
Compatibility" Version="$(MauiVersion)"/>
</ItemGroup>
The reason why we have made this change manually is due to the
Version value that we added; we did not add a specific version but instead
used the variable $(MauiVersion); this makes use of the installed version.
Now you have set up everything ready to begin writing and running your
unit tests.
426
CHAPTER 13 TESTING
Testing BoardDetailsPageViewModel
Inside the class file that you just created, add the following:
[Fact]
public void SaveCommandCannotExecuteWithoutBoardName()
{
var viewModel = new BoardDetailsPageViewModel(null, null);
Assert.Equal(string.Empty, viewModel.BoardName);
Assert.False(viewModel.SaveCommand.CanExecute(null));
}
[Fact]
public void SaveCommandCanExecuteWithBoardName()
{
var viewModel = new BoardDetailsPageViewModel(null, null);
viewModel.BoardName = "Work";
Assert.True(viewModel.SaveCommand.CanExecute(null));
}
Testing INotifyPropertyChanged
I covered in Chapter 4 that INotifyPropertyChanged serves as the
mechanism to keep your views and view models in sync; therefore, it can
be really useful to verify that your view models are correctly implementing
INotifyPropertyChanged by ensuring that it raises the PropertyChanged
event when it should.
The following test shows how to create an instance of the
BoardDetailsPageViewModel, subscribe to the PropertyChanged event,
modify a property that you expect to fire the PropertyChanged event, and
then assert that the event was invoked:
427
CHAPTER 13 TESTING
[Fact]
public void SettingBoardNameShouldRaisePropertyChanged()
{
var invoked = false;
var viewModel = new BoardDetailsPageViewModel(null, null);
This provides you with the confidence to know that if the BoardName is
not showing in your user interface, it will probably not be an issue inside
the view model.
428
CHAPTER 13 TESTING
using WidgetBoard.Services;
namespace WidgetBoard.Tests.Mocks;
429
CHAPTER 13 TESTING
430
CHAPTER 13 TESTING
namespace WidgetBoard.Tests.Mocks;
return false;
}
431
CHAPTER 13 TESTING
using WidgetBoard.Communications;
namespace WidgetBoard.Tests.Mocks;
432
CHAPTER 13 TESTING
using WidgetBoard.Tests.Mocks;
using WidgetBoard.ViewModels;
namespace WidgetBoard.Tests.ViewModels;
433
CHAPTER 13 TESTING
[Fact]
public async Task NullLocationResultsInPermissionErrorState()
{
var viewModel = new WeatherWidgetViewModel(
MockWeatherForecastService.ThatReturnsNoForecast(after:
TimeSpan.FromSeconds(5)),
MockSecureStorage.ThatContains("OpenWeatherApiToken",
"SomethingSecure"),
MockLocationService.ThatReturnsNoLocation(after:
TimeSpan.FromSeconds(2)));
await viewModel.LoadWeatherForecast();
Assert.Equal(State.PermissionError, viewModel.State);
Assert.Equal(viewModel.Weather, string.Empty);
}
This first test, as the name implies, verifies that if a null location is
returned from the ILocationService implementation, the view model
State will be set to PermissionError and no Weather will be set.
[Fact]
public async Task NullForecastResultsInErrorState()
{
var viewModel = new WeatherWidgetViewModel(
MockWeatherForecastService.ThatReturnsNoForecast(after:
TimeSpan.FromSeconds(5)),
MockSecureStorage.ThatContains("OpenWeatherApiToken",
"SomethingSecure"),
MockLocationService.ThatReturns(new Location(0.0, 0.0),
after: TimeSpan.FromSeconds(2)));
434
CHAPTER 13 TESTING
await viewModel.LoadWeatherForecast();
Assert.Equal(State.Error, viewModel.State);
Assert.Equal(viewModel.Weather, string.Empty);
}
This second test, as the name implies, verifies that if a null forecast is
returned from the IWeatherForecastService implementation, the view
model State will be set to Error and no Weather will be set.
[Fact]
public async Task ValidForecastResultsInSuccessfulLoad()
{
var weatherForecastService =
MockWeatherForecastService.ThatReturns(
new Communications.Forecast
{
Main = new Communications.Main
{
Temperature = 18.0
},
Weather =
[
new Communications.Weather
{
Icon = "abc.png",
Main = "Sunshine"
}
]
},
after: TimeSpan.FromSeconds(5));
435
CHAPTER 13 TESTING
await viewModel.LoadWeatherForecast();
Assert.Equal(State.Loaded, viewModel.State);
Assert.Equal("Sunshine", viewModel.Weather);
}
This final test, as the name implies, verifies that if a valid forecast is
returned from the IWeatherForecastService implementation, the view
model State will be set to Loaded and the Weather will be correctly set.
436
CHAPTER 13 TESTING
using WidgetBoard.ViewModels;
namespace WidgetBoard.Tests.Mocks;
Now you can use this in your unit tests to verify that your
ClockWidgetView binds correctly to its view model.
using WidgetBoard.Tests.Mocks;
using WidgetBoard.Views;
namespace WidgetBoard.Tests.Views;
437
CHAPTER 13 TESTING
{
var time = new DateTime(2022, 01, 01);
var clockWidget = new ClockWidgetView(null);
Assert.Null(clockWidget.Text);
Assert.Equal(time.ToString(), clockWidget.Text.Trim());
}
}
Device Testing
Device testing is really a form of unit testing; however, it provides some
unique abilities so it deserves its own top-level section. It essentially
enables you to write unit tests that can be run on a device and therefore
truly test any platform-specific pieces of functionality. A perfect example
of this is to test the PlatformLocationService you implemented in the
previous chapter to return the longitude and latitude coordinates of each
platform provider’s headquarters.
438
CHAPTER 13 TESTING
439
CHAPTER 13 TESTING
Replace CreateMauiApp
Inside the MauiProgram.cs file, the CreateMauiApp method can be
replaced with the following:
• Select Projects.
• Select WidgetBoard.
• Click Add.
440
CHAPTER 13 TESTING
<ItemGroup>
<MauiIcon Include="Resources\AppIcon\appicon.svg"
ForegroundFile="Resources\AppIcon\appiconfg.svg"
Color="#512BD4"/>
<MauiSplashScreen Include="Resources\Splash\splash.svg"
Color="#512BD4" BaseSize="128,128"/>
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.png"
Resize="True" BaseSize="300,185"/>
<MauiFont Include="Resources\Fonts\*"/>
The reason for this is that by referencing the main app project, the
build tasks detect duplicate files and will produce build errors. Note that
if you are not referencing another .NET MAUI app project in your device
runner test project, then you want to keep the above.
441
CHAPTER 13 TESTING
using WidgetBoard.Services;
using Xunit;
namespace WidgetBoard.DeviceTests.Services;
#if ANDROID
Assert.Equal(37.419857, location.Latitude);
Assert.Equal(-122.078827, location.Longitude);
#elif WINDOWS
Assert.Equal(47.639722, location.Latitude);
Assert.Equal(-122.128333, location.Longitude);
#else
Assert.Equal(37.334722, location.Latitude);
Assert.Equal(-122.008889, location.Longitude);
#endif
}
}
442
CHAPTER 13 TESTING
Now that you have written your tests, you can run them on your
devices.
443
CHAPTER 13 TESTING
You can click on a specific test and choose to run it, or you can simply
Run All Tests. This part is entirely manual so it will require a human to
perform these tasks but it can be left to run for as long as the tests need.
Finally, you will see the results of the test runs, and you can click them
to see more information. Figure 13-4 shows the device test runner and a
set of test results.
You can run these tests on all the platforms that you support to make
sure that the code does what is expected.
Snapshot Testing
Snapshot testing is similar to unit testing, but it avoids the need to write
Assert statements to manually define each expectation in the test. Instead,
the result of a test is compared to a golden master. A golden master is a
snapshot of a previous test run that you as the test author accept as the
444
CHAPTER 13 TESTING
{
LoadWeatherCommand: {},
IconUrl: https://openweathermap.org/img/wn/[email protected],
State: Loaded,
Temperature: 18.0,
Weather: Sunshine,
Type: Weather
}
When this test is run, each time the serialized output of the
WeatherWidgetViewModel will be compared to the above golden master.
If any of the values are different from those in the golden master, the test
will fail.
445
CHAPTER 13 TESTING
• Select WidgetBoard.
• Click Add.
Using VerifyTests, you can take a copy of your
WeatherWidgetViewModelTests class in the WidgetBoard.Tests project
and modify it to the following. The limited changes are shown in bold to
highlight the differences from the original.
[UsesVerify]
public class WeatherWidgetViewModelTests
{
[Fact]
public async Task NullLocationResultsInPermission
ErrorState()
446
CHAPTER 13 TESTING
{
var viewModel = new WeatherWidgetViewModel(
MockWeatherForecastService.ThatReturnsNoForecast(after:
TimeSpan.FromSeconds(5)),
MockSecureStorage.ThatContains("OpenWeatherApiToken",
"SomethingSecure"),
MockLocationService.ThatReturnsNoLocation(after:
TimeSpan.FromSeconds(2)));
await viewModel.LoadWeatherForecast();
await Verify(viewModel);
}
[Fact]
public async Task NullForecastResultsInErrorState()
{
var viewModel = new WeatherWidgetViewModel(
MockWeatherForecastService.ThatReturnsNoForecast(after:
TimeSpan.FromSeconds(5)),
MockSecureStorage.ThatContains("OpenWeatherApiToken",
"SomethingSecure"),
MockLocationService.ThatReturns(new Location(0.0, 0.0),
after: TimeSpan.FromSeconds(2)));
await viewModel.LoadWeatherForecast();
await Verify(viewModel);
}
[Fact]
public async Task ValidForecastResultsInSuccessfulLoad()
{
var weatherForecastService =
447
CHAPTER 13 TESTING
MockWeatherForecastService.ThatReturns(
new Communications.Forecast
{
Main = new Communications.Main
{
Temperature = 18.0
},
Weather =
[
new Communications.Weather
{
Icon = "abc.png",
Main = "Sunshine"
}
]
},
after: TimeSpan.FromSeconds(5));
await viewModel.LoadWeatherForecast();
await Verify(viewModel);
}
}
448
CHAPTER 13 TESTING
You remove the Assert statements and replace them by calling the
Verify method. In your original scenario, you were only asserting a small
number of things, but you can imagine that if the number of Assert
statements were to grow, then this single method call to Verify really does
reduce the complexity of your tests.
Brand-new tests will always fail until you accept the golden master.
There is tooling that can make this task easier, which is again provided by
the VerifyTests developers.
Passing Thoughts
I end this snapshot testing section with the statement that it is not for
everyone. Some people really like the reduction in test case size, while
it verifies more than most typical unit tests by the sheer fact that it
verifies the whole object under test. As a counter argument, some people
dislike that the expected state or golden master is in a file separate to the
tests. I personally believe they provide great value, and I hope that this
introduction to snapshot testing will give you enough context to decide
whether it is going to be a good fit for you and your team, or at least give
you the desire to experiment with the concept.
Summary
Now you have an overview of different testing techniques and the benefits
they bring. You may prefer snapshot testing over writing your own asserts.
I don’t mind either way so long as you do test your code. We have not
concluded all testing-related topics in this book; the next chapter covers
automation testing.
449
CHAPTER 13 TESTING
• Explored what device tests are and how you can apply
them to your applications
In the next chapter, you will
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch13.
450
CHAPTER 14
Automation Testing
Abstract
The previous chapter covered a number of testing techniques to help prove
how small units of code “do what they say on the tin.” This chapter will
broaden the scope of what is being tested into what would be considered
integration or end-to-end testing. Furthermore, the focus of this chapter
will be to focus on performing this testing in an automated manner –
hence the title “Automation Testing.”
452
CHAPTER 14 AUTOMATION TESTING
Installing Appium
The WidgetBoard application can run on Android, iOS, macOS, and
Windows.
Installing Node.js
I mentioned earlier that Appium works by having a server execute the
tests; for this reason, we need to install Node.js. In order to install the
environment, you can follow the steps below:
1. Navigate to https://nodejs.org/.
453
CHAPTER 14 AUTOMATION TESTING
Install Appium
Now that you have Node.js installed, you should be able to open a terminal or
command prompt session on either macOS or Windows and install Appium
using node package manager (npm). Let’s take a look at each in turn.
macOS
Windows
This will find the package called appium via node package manager,
and the -g argument will install it as a global tool. This means that you can
just type appium into a future terminal or command prompt session and
start an Appium server.
With Appium install, you can proceed to installing the relevant drivers
for each platform that you wish to test.
454
CHAPTER 14 AUTOMATION TESTING
455
CHAPTER 14 AUTOMATION TESTING
456
CHAPTER 14 AUTOMATION TESTING
457
CHAPTER 14 AUTOMATION TESTING
458
CHAPTER 14 AUTOMATION TESTING
This concludes the steps on how to install Appium and all of its
dependencies, so let’s now proceed to creating our test project and tests.
459
CHAPTER 14 AUTOMATION TESTING
460
CHAPTER 14 AUTOMATION TESTING
I mentioned that this book is doing things a little bit differently to most
public samples. I don’t want to take away from the work of these samples.
If you are happy to follow this approach, then I should highlight that my
good friend Gerald and the reviewer of this book has created a template
to make the steps that you just followed a lot simpler. You can check the
details out for this at https://github.com/jfversluis/Template.Maui.
UITesting.
Now that you have created the project, you will proceed to introduce
some helper implementations to initialize the Appium layer so that the
tests can interact with the application on each target platform.
461
CHAPTER 14 AUTOMATION TESTING
the address and port the server is running on, and then start it. Let’s create
a new class file, call it AppiumServerHelper, and then modify its contents
to the following:
using OpenQA.Selenium.Appium.Service;
namespace WidgetBoard.AutomationTests;
462
CHAPTER 14 AUTOMATION TESTING
The code that you just added will build a service to connect to a default
address of 127.0.0.1 and port of 4723. For the examples in this book,
the default values are fine, but if you opt for using different values in your
Appium server, then remember to update these.
Now that you have the code to start the server, the next step is to create
a driver instance for the platform being tested. Let’s proceed to doing
this now.
using NUnit.Framework;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;
using OpenQA.Selenium.Appium.Enums;
using OpenQA.Selenium.Appium.iOS;
463
CHAPTER 14 AUTOMATION TESTING
using OpenQA.Selenium.Appium.Mac;
using OpenQA.Selenium.Appium.Windows;
namespace WidgetBoard.AutomationTests;
[SetUpFixture]
public class AppiumSetup
{
private static AppiumDriver? driver;
[OneTimeSetUp]
public void RunBeforeAnyTests()
{
AppiumServerHelper.StartAppiumLocalServer();
driver = CreateDriver();
}
switch (platformName)
{
case "Android":
case "iOS":
case "Mac":
case "Windows":
}
464
CHAPTER 14 AUTOMATION TESTING
return null;
}
[OneTimeTearDown]
public void RunAfterAllTests()
{
driver?.Quit();
465
CHAPTER 14 AUTOMATION TESTING
case "Android":
var androidOptions = new AppiumOptions
{
AutomationName = "UIAutomator2",
PlatformName = platformName,
App = TestContext.Parameters["app"]
};
You may have wondered why I included the console results for each
platform in order to show that each driver had been installed successfully;
if you look back now, you should notice that the PlatformName and
AutomationName entries match those reported in the console results.
Note if you wish to run your tests on an Android Virtual Device (AVD)
or emulator, then you need to supply an additional option avd. This option
can be added via the AddAdditionalAppiumOption called avd; if you
supply this, it will boot an Android emulator up that matches the name
supplied, for example:
androidOptions.AddAdditionalAppiumOption("avd", "pixel_5_-_
api_33");
466
CHAPTER 14 AUTOMATION TESTING
You did not supply this in your code, so in order to test your
application, you must make sure that the Android emulator is booted
manually.
case "iOS":
var iOSOptions = new AppiumOptions
{
AutomationName = "XCUITest",
PlatformName = platformName,
PlatformVersion = TestContext.
Parameters["platformVersion"],
DeviceName = TestContext.Parameters["deviceName"]
App = TestContext.Parameters["app"]
};
Note that the value for App can be the full path to the .app file to test or
the bundle ID if the app is already installed on the device.
467
CHAPTER 14 AUTOMATION TESTING
case "Mac":
var macOSOptions = new AppiumOptions
{
AutomationName = "mac2",
PlatformName = platformName,
App = TestContext.Parameters["app"]
};
Note that the value for App can be the full path to the .app file to test or
the bundle ID if the app is already installed on the device.
case "Windows":
var windowsOptions = new AppiumOptions
{
AutomationName = "windows",
PlatformName = platformName,
App = TestContext.Parameters["app"]
};
Note that the value for App needs to be the identifier of the deployed
application. Therefore, the application must be deployed before testing.
This concludes the changes required to instantiate an Appium
driver on each of the target platforms; let’s proceed to looking at the
configuration files required for each platform and then actually write
some tests.
468
CHAPTER 14 AUTOMATION TESTING
469
CHAPTER 14 AUTOMATION TESTING
Hopefully the file name alone will start to give the impression of
how we can customize the test run; we could easily introduce a second
.runsettings file to support testing the application running on an iPad Pro
13 inch or another iPhone variant. This means that all of the same tests can
be executed against different devices without any code changes.
470
CHAPTER 14 AUTOMATION TESTING
471
CHAPTER 14 AUTOMATION TESTING
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
472
CHAPTER 14 AUTOMATION TESTING
using OpenQA.Selenium.Appium.Windows;
namespace WidgetBoard.AutomationTests;
return App.FindElement(MobileBy.Id(id));
}
}
473
CHAPTER 14 AUTOMATION TESTING
[Test, Order(1)]
public void SaveButtonIsDisabledByDefault()
{
FindUIElement("AddBoardButton").Click();
Assert.That(saveButton.Enabled, Is.False);
FindUIElement("Cancel").Click();
}
This method doesn’t just test that the add button works, it does a
number of other things; let’s break it down line by line to understand it in
more detail.
[Test, Order(1)]
The above states that the method is a test in the NUnit framework and
that it will be the first test to be executed.
FindUIElement("AddBoardButton").Click();
We will make use of the new method we added a short while ago; it will
attempt to find a UI element with the AutomationId of "AddBoardButton"
and then interact with it. I use the word “interact” because on a mobile
platform, there isn’t really a click interaction but a touch interaction. If
Appium cannot find the UI element, an exception will be thrown stating it
couldn’t find it; this works as an implicit assertion because the test will fail
and it will fail with a useful error message.
The result of the Click method call will result in the BoardDetailsPage
being presented to the user in the application.
474
CHAPTER 14 AUTOMATION TESTING
Assert.That(saveButton.Enabled, Is.False);
The above line will verify that the save button is currently disabled; this
is down to the application requiring a name to be provided for the board.
This test would then catch a regression if a developer accidentally turned
off that rule.
FindUIElement("Cancel").Click();
This final step is not really part of the test, but it resets the state of
the application by navigating back to the BoardListPage. Some test
enthusiasts might highlight how tests should not rely on or affect the
outcome of other tests, and in the majority of scenarios, I would agree. In
this scenario, however, I believe there is value in testing each individual
unit of functionality but also testing them in combination to prove that
the units integrate with each other. Plus sometimes you need data in your
application to test specific scenarios and what better way to create the data
than through automation?
The test made use of AutomationIds which the application code does
not currently support; let’s add in the ones to support this test; open
the BoardListPage.xaml file and modify the ToolbarItem to match the
following (changes in bold):
<ContentPage.ToolbarItems>
<ToolbarItem
Text="Add"
AutomationId="AddBoardButton"
Command="{Binding AddBoardCommand}" />
</ContentPage.ToolbarItems>
475
CHAPTER 14 AUTOMATION TESTING
You should notice how the AutomationId you just added matches that
used in the test. You will additionally need to modify the BoardDetailsPage.
xaml file to introduce two further AutomationIds; make the following
changes in bold:
<Grid ColumnDefinitions="*,*,*">
<Button
Text="Cancel"
Command="{Binding CancelCommand}"
AutomationId="CancelButton" />
<Button
Text="Save"
Grid.Column="2"
Command="{Binding SaveCommand}"
AutomationId="SaveButton" />
</Grid>
This makes it possible to assert that the save button is disabled and
then to interact with the cancel button.
476
CHAPTER 14 AUTOMATION TESTING
[Order(2)]
[TestCase("Work", 4, 4)]
[TestCase("Family", 2, 2)]
public void CanSaveBoard(string boardName, int numberOfColumns,
int numberOfRows)
{
FindUIElement("AddBoardButton").Click();
FindUIElement("BoardNameEntry").SendKeys(boardName);
FindUIElement("NumberOfColumnsEntry").
SendKeys(numberOfColumns.ToString());
FindUIElement("NumberOfRowsEntry").SendKeys(numberOfRows.
ToString());
Assert.That(saveButton.Enabled, Is.True);
saveButton.Click();
Assert.That(createdBoard, Is.Not.Null);
Assert.That(createdBoard.Displayed, Is.True);
}
I won’t break down each line this time as a fair amount should feel
familiar; I will highlight the new concepts though. As I mentioned, this test
will be executed twice: once with a boardName of "Work" and once with a
boardName of "Family".
FindUIElement("BoardNameEntry").SendKeys(boardName);
This will result in the value inside the boardName parameter being
entered into the entry field with AutomationId of BoardEntryName.
477
CHAPTER 14 AUTOMATION TESTING
Assert.That(createdBoard, Is.Not.Null);
Assert.That(createdBoard.Displayed, Is.True);
The three lines above are called after the save happens; this is
important to highlight because it means that the application will have
navigated back to the BoardListPage. These lines above will find the Label
inside the CollectionView with AutomationId of the board name and then
verify that it is visible to the user through the Displayed property. This is
something I wanted to highlight because the properties available in the
Appium layer are designed to be platform agnostic and therefore do not
directly match the .NET MAUI property names.
[Test, Order(3)]
public void CanSelectEntryInListOfBoards()
{
App.FindElement(By.XPath("//XCUIElementTypeStaticText[
@name='Work']")).Click();
Assert.That(grid, Is.Not.Null);
Assert.That(grid.Displayed, Is.True);
}
478
CHAPTER 14 AUTOMATION TESTING
The only new concept is the use of the By.XPath method; this is a
helper method provided by Appium to perform a lookup by XPath. I won’t
be digging any further into XPath or the complicated scenarios you can
build using it, but hopefully the above example shows how you can build
a relatively straightforward lookup. The code above looks for an element
of type XCUIElementTypeStaticText which has the name of ‘Work’. It is
worth noting that lookups by XPath typically perform worse than using the
AutomationId; in our scenario, we could use the AutomationId, but this
example was just to show how you could build it another way.
Now that you have some insight into how you can instruct Appium
to discover and select elements within your application’s visual tree, I
would like to highlight another library that can help to simplify the code
that you need to write. This library is available at https://github.com/
jfversluis/Plugin.Maui.UITestHelpers.
This now concludes the chapter on automation testing and testing in
general in the book. If you were to run the tests inside your IDE or using
the command-line tooling, you should be able to observe the application
being tested under automation.
Summary
Now you have an overview of automation testing, the required effort to
set up a system to support it through Appium, and some techniques to
use when writing automation tests. I really hope this chapter has been
as enjoyable learning about the options as it has been to expose them.
It really is impressive what you can automate through a framework
like Appium!
479
CHAPTER 14 AUTOMATION TESTING
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch14.
480
CHAPTER 15
Essentially .NET MAUI Graphics offers a surface that can render pixel-
perfect graphics on any platform supported by .NET MAUI. Consider .NET
MAUI Graphics as an abstraction layer, like .NET MAUI itself, on top of
the platform-specific drawing libraries. So we get all the power of each
platform but with a simple unified .NET API that we as developers can
work with.
Drawing a Line
Inside the Draw method, you can interact with the ICanvas to draw a line
using the DrawLine method. The following code shows how this can be
achieved:
482
CHAPTER 15 LET’S GET GRAPHICAL
In addition to drawing lines, you can draw many different shapes such
as ellipse, rectangle, rounded rectangle, and arc. You can draw even more
complex shapes through paths.
Drawing a Path
Paths are not to be confused with the Shapes API provided with .NET
MAUI. Paths in .NET MAUI Graphics enable you to build up a set of
coordinates in order to draw a more complex shape.
483
CHAPTER 15 LET’S GET GRAPHICAL
canvas.StrokeColor = Colors.Red;
canvas.StrokeSize = 6;
canvas.DrawPath(path);
}
You first build up a PathF through the MoveTo, LineTo, and Close
methods. The MoveTo method moves the current location of the path to the
specified coordinates, and then the LineTo method draws a line from the
current location that you just set in MoveTo to the coordinates specified in
the LineTo method call. Finally, the Close method allows you to close the
path. This means that the final location will have a line added back to the
starting location. Notice that you didn’t explicitly add a LineTo(40, 10)
method call in; Close does this for you. Then you set the StrokeColor and
StrokeSize before calling the DrawPath method. Figure 15-2 shows the
result of the Draw method from above.
It is this DrawPath method that you will be utilizing in the new widget
you will be building as part of this chapter.
484
CHAPTER 15 LET’S GET GRAPHICAL
to reset the current graphics state back to the default values with the
ResetState method. These three methods can provide a large amount
of functionality in specific scenarios. Say you have implemented a chart
rendering control where the chart is rendered and then each individual
series is rendered separately. You want to preserve the state of the chart’s
graphics settings but wish to reset each time you render a series (e.g., each
column in a bar chart).
Further Reading
You have only scratched the surface of what is possible with the .NET
MAUI Graphics layer. I strongly recommend that you refer to the Microsoft
documentation at https://learn.microsoft.com/dotnet/maui/user-
interface/graphics/ where it shows much more complex scenarios such
as painting patterns, gradients, images, rendering text, and much more.
485
CHAPTER 15 LET’S GET GRAPHICAL
namespace WidgetBoard.ViewModels;
486
CHAPTER 15 LET’S GET GRAPHICAL
487
CHAPTER 15 LET’S GET GRAPHICAL
488
CHAPTER 15 LET’S GET GRAPHICAL
using Microsoft.Maui.Controls;
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
489
CHAPTER 15 LET’S GET GRAPHICAL
Each of the event handles and the Draw method have the blank or
default implementation. Let’s build this file up slowly and discuss the key
parts as you do so.
First, you need to add the backing fields to store the interactions from
the user.
490
CHAPTER 15 LET’S GET GRAPHICAL
In this method, you add the current touch to the current path and
again call Invalidate to cause the Draw method to be called.
The final event handler to modify is for the EndInteraction event.
491
CHAPTER 15 LET’S GET GRAPHICAL
This method loops through all of the paths that you have created from
the user interactions, setting the stroke color and size and then drawing the
path that was built up by the three event handlers that you just implemented.
WidgetFactory.RegisterWidget<SketchWidgetView, SketchWidgetView
Model>(SketchWidgetViewModel.DisplayName);
builder.Services.AddTransient<SketchWidgetView>();
builder.Services.AddTransient<SketchWidgetViewModel>();
492
CHAPTER 15 LET’S GET GRAPHICAL
493
CHAPTER 15 LET’S GET GRAPHICAL
using System.ComponentModel;
using WidgetBoard.ViewModels;
namespace WidgetBoard.Views;
494
CHAPTER 15 LET’S GET GRAPHICAL
if (BindingContext is INotifyPropertyChaned
propertyChanged)
{
495
CHAPTER 15 LET’S GET GRAPHICAL
propertyChanged.PropertyChanged +=
ClockWidgetViewModelOnPropertyChanged;
}
}
}
}
Most of the above should look familiar. I would like to highlight the
following additions that may not feel familiar.
The first item is the following line which assigns the view model as the
Drawable property on GraphicsView; this means that the view model will
have to implement the IDrawable interface because it will be responsible
for drawing on the canvas.
Drawable = drawable;
The next item is the code that subscribes to the PropertyChanged event
and then calls Invalidate on the GraphicsView to force the canvas to be
redrawn. This allows the Time changes in the view model to trigger the
canvas to be redrawn.
propertyChanged.PropertyChanged +=
ClockWidgetViewModelOnPropertyChanged;
private void ClockWidgetViewModelOnPropertyChanged(object?
sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AnalogClockWidgetViewModel.Time))
{
Invalidate();
}
}
496
CHAPTER 15 LET’S GET GRAPHICAL
namespace WidgetBoard.ViewModels;
SetTime(DateTime.Now);
}
497
CHAPTER 15 LET’S GET GRAPHICAL
this.dispatcher.DispatchDelayed(
TimeSpan.FromSeconds(1),
() => SetTime(DateTime.Now));
}
498
CHAPTER 15 LET’S GET GRAPHICAL
Figure 15-4. The angles required for hour, minute, and second
increments
Given the above statements, you will now add in each part
incrementally to the Draw method in your AnalogClockWidgetViewModel.
cs file. The first step is to add in some constants and the color-related parts.
canvas.StrokeSize = 5;
canvas.StrokeColor = App.Current?.PlatformAppTheme == AppTheme.
Dark ? Colors.White : Colors.Black;
The above records the small and large angle increments that were
detailed in the list above, then the line width has been set, and finally, the
line color will either be white or black depending on whether the app is
running on a system with dark or light mode.
499
CHAPTER 15 LET’S GET GRAPHICAL
The next step is to render the hour markers on the clock. Interactions with
the canvas work in a similar way to general user interface building; the earlier
items are added or drawn the lower down in the visual tree they are; therefore,
if you rendered two things in the same space, it would be the last thing
rendered that would be visible. Let’s add the code to render the markers.
canvas.Translate(dirtyRect.Center.X, dirtyRect.Center.Y);
var hourMarkerLength = dirtyRect.Size.Height / 10;
The above code first determines the radius of the clock being half the
height of the widget; you could introduce some padding here if you wanted
or possibly check if the width is smaller than the height and take that value.
The next step is to perform a translation; this essentially means you are
moving where the origin is on the canvas – your code has now moved the
origin from the top left to the center of the canvas. Then inside the for
loop, the code will rotate the canvas by a largeIncrement, which is 30°,
and then draw a line to represent the marker. That is all the code required
to render the markers, so let’s proceed to rendering the hour hand using
the above statement: “the hour hand will increment by 30° + the amount of
minutes that have elapsed”.
500
CHAPTER 15 LET’S GET GRAPHICAL
You have just replaced the addition of minutes elapsed with seconds
elapsed and made the line slightly bigger.
Finally, the code for the second hand can be added as follows:
canvas.StrokeSize = 3;
501
CHAPTER 15 LET’S GET GRAPHICAL
WidgetFactory.RegisterWidget<AnalogClockWidgetView, AnalogClock
WidgetViewModel>(AnalogClockWidgetViewModel.DisplayName);
builder.Services.AddTransient<AnalogClockWidgetView>();
builder.Services.AddTransient<AnalogClockWidgetViewModel>();
502
CHAPTER 15 LET’S GET GRAPHICAL
Figure 15-5. The application showing both the original clock and
new analog clock widgets
Summary
In this chapter, you have
• Learned what .NET MAUI Graphics is
• Gained an insight into some of the power provided by
.NET MAUI Graphics
• Built your own sketch widget with the .NET MAUI
GraphicsView control
• Taken the Graphics APIs further to also create an
analog clock widget
503
CHAPTER 15 LET’S GET GRAPHICAL
Source Code
The resulting source code for this chapter can be found on the GitHub
repository at https://github.com/Apress/Introducing-.NET-MAUI-2nd-
ed/tree/main/ch15.
Extra Assignment
Perhaps you can think of another concept where you can use .NET MAUI
Graphics – maybe the chart control idea I discussed or even just showing
the battery level in a widget or other device information.
Source Code
I would love for you to have an attempt at this extra assignment, but I have
also provided the source code. The source code for this extra assignment
can be found on the GitHub repository at https://github.com/Apress/
Introducing-.NET-MAUI-2nd-ed/tree/main/ch15-extra.
504
CHAPTER 16
Releasing Our
Application
Abstract
Once you have built your application, you need to get it to your users.
There are many ways to achieve this. You can publish a release build and
ship it directly to your customers or you can make use of the stores that
each platform provider offers.
Shipping directly to an end customer can sometimes be the best
option, such as when you are building an internal application and you
don’t want it to be publicly accessible.
Most often the recommended way to ship applications to users is to
go through the stores provided by each platform provider (e.g., App Store
from Apple, Play Store from Google, and Microsoft Store from Microsoft).
This does involve agreeing to terms and conditions, and these providers
take a percentage of any income you make. There are many benefits that
justify paying the fees. They provide trusted platforms for users to find and
download your applications. The store will provide a much wider reach
for your intended audience. The store also manages the ability to provide
updates seamlessly.
This chapter has been split into two key sections: how you would go
about distributing your application and additional improvements you can
make to boost performance and reduce application size.
macOS
1. Open the Terminal application.
2. Enter the following command and then press return:
506
Chapter 16 releasing Our appliCatiOn
Windows
1. Open the Command Prompt application.
2. Enter the following command and then press return:
Android
Android has the biggest mobile user base. However, given the model it
follows of allowing manufacturers to customize the Android operating
system as well as providing varying sets of hardware, it can be the most
problematic.
An Android Package, or APK for short, is the resulting application file
that runs on an Android device. If you wish to provide a mechanism to
download this file (e.g., a website or file share), users can side-load the
application onto their Android device. This is not recommended in the
public domain because it can be very difficult to trust the packages that are
freely downloaded from the Internet.
When you wish to distribute using the Google Play Store, you are
required to build an Android App Bundle, or AAB for short. It contains all
of the relevant files needed to compile an APK ready for installation on a
user’s device.
Essentially you build an Android App Bundle, sign it with a specific
signing key that you own, and upload the bundle to Google Play. Google
uses this bundle when a user comes to download your application and
compiles a specific APK for that device. This is the way to do things now.
If you have worked with Android apps in the past, you may recall building
the APK yourself. This runs into the issue that the APK is architecture
specific, and in the current market where there are multiple architectures
supported by the various Android devices, you can end up with an
507
Chapter 16 releasing Our appliCatiOn
-p:ApplicationVersion=”12”
Again this might not feel very helpful using a fixed number, but if
you imagine generating a number during your CI/CD pipeline build,
then you could provide that in as the value and then you have dynamic
508
Chapter 16 releasing Our appliCatiOn
509
Chapter 16 releasing Our appliCatiOn
-p:AndroidSigningKeyAlias={keyname}
-p:AndroidSigningKeyPass="{keypassword}"
-p:AndroidSigningStorePass="{keypassword}"
Once you have run the above command, you will find that the tooling
has created a file named com.tinysoft.widgetboard-Signed.aab under the
WidgetBoard/bin/Release/net9.0-android/publish folder.
If you plan to use these details in your CI/CD pipelines, then I would
strongly recommend you look into secure options such as GitHub Secrets,
or whatever is appropriate on the platform that you are using.
Additional Resources
Both Microsoft and Google provide documentation on how to distribute
applications via the Google Play Store. See the following links.
• Microsoft: How to publish an application ready for the
Play Store, https://learn.microsoft.com/dotnet/
maui/android/deployment/overview
• Google: How to upload your application to the Play
Store, https://developer.android.com/studio/
publish/upload-bundle
• It is also worth noting that other stores/platforms
provide the ability to distribute, install, and run
Android applications. Amazon devices such as the
Kindle Fire are built on top of Android and allow the
running of Android applications. Amazon provides its
own store, details of which can be found at https://
developer.amazon.com.
510
Chapter 16 releasing Our appliCatiOn
iOS
iOS and macOS are considered really painful when dealing with
distributing and signing. Having spent several years going through this
pain, I want to break down the key concepts to hopefully reduce the pain
that you might experience. Thankfully the Apple tooling has come a long
way since I started building mobile apps in 2007, so you don’t have to
relive all those painful memories.
The following sections cover the development and application settings
you will need to create on the Apple developer website at https://
developer.apple.com.
Certificate
You need to generate a certificate on the machine that will build your
application. Most documentation takes you through the complex scenarios
of creating a Certificate Signing Request and then uploading to Apple.
There is actually a far simpler way by using Xcode. The following steps can
help to achieve this:
• Click the Xcode main menu.
• Click Settings.
• Click the Accounts tab.
• Click the Manage Certificates button, shown in
Figure 16-1.
511
Chapter 16 releasing Our appliCatiOn
Identifier
This represents your application. It requires you to define unique details
to identify the application that will be exposed to the public store as well
as defining what capabilities your application requires. Note that the value
you provide to Apple needs to match the value set in your project file under
<ApplicationId></ApplicationId> in your csproj file (WidgetBoard.
csproj in our case).
512
Chapter 16 releasing Our appliCatiOn
Capabilities
iOS applications run under a sandboxed environment. Apple provides a
set of App Services that can be utilized by your applications and enhance
its capabilities. Capabilities include services like in-app purchasing, push
notifications, Apple Pay, and such. The use of these services needs to be
defined at compile time, and the usage of them will be reviewed when you
upload your application to Apple for review. Therefore, it’s important that
you make sure you only have the ones you need. Don’t worry, though; a
failure here will give a fairly useful error message and can be a relatively
easy fix. More information can be found at https://developer.apple.
com/documentation/xcode/capabilities.
Changes to the capabilities of your application will invalidate your
provisioning profiles so they will need to be edited in the https://
developer.apple.com portal to update them with the newer capabilities.
Entitlements
Entitlements tie in closely with capabilities and allow you to configure
settings during compilation. You need to add an Entitlements.plist file
to your application and then add the relevant entries for the configuration.
Information on how to configure this can be found at https://learn.
microsoft.com/dotnet/maui/ios/deployment/entitlements.
Provisioning Profiles
Provisioning profiles determine how your application will be provisioned
for deployment. There are two main types:
513
Chapter 16 releasing Our appliCatiOn
In order to sign the iOS application, you need to provide two additional
arguments:
• -p:CodesignKey="": This is the name of the certificate
that you will have created using Figure 16-1.
514
Chapter 16 releasing Our appliCatiOn
Note that if you run this command now, you should see two warnings
and zero errors reported. If you were proactive during the building of the
application and solved the warnings, then great work! I haven’t opted to
ignore; in fact, I wanted to show how they can manifest into some less
clear warnings when we turn on full trimming later on in this chapter. For
clarity, the two warnings that I am referring to are
Additional Resources
Both Microsoft and Apple provide documentation on how to distribute
applications via the Apple App Store.
• Microsoft: How to publish an application ready for the
App Store, https://learn.microsoft.com/dotnet/
maui/ios/deployment/overview
• Apple: How to upload your application to the App
Store, https://developer.apple.com/app-store/
515
Chapter 16 releasing Our appliCatiOn
macOS
When distributing your .NET MAUI application for macOS, you can
generate a .app or a .pkg file. A .app file is a self-contained app that can be
run without installation, whereas a .pkg is an app packaged in an installer.
In order to build the macOS application that can be distributed via the
Apple App Store, you will need to generate a .pkg file. In order to do this,
you will need to provide the following additional build properties:
• -p:CreatePackage=true: This will tell the tooling to
create a .pkg file.
• -p:EnableCodeSigning=true: This will enable code
signing for the application.
Using the above details, we can now publish a signed iOS application
using the following example:
Once you have run the above command, you will find that the tooling
has created a file named WidgetBoard-1.0.pkg under the WidgetBoard/
bin/Release/net9.0-maccatalyst/publish folder.
Additional Resources
Both Microsoft and Apple provide documentation on how to distribute
applications via the Apple App Store.
Windows
When distributing your .NET MAUI app for Windows, you can publish
the app and its dependencies to a folder for deployment to another
system. Publishing a .NET MAUI app for Windows creates an unpackaged
application by default, making it possible for you to create an installer
517
Chapter 16 releasing Our appliCatiOn
for your application. If you wish to distribute your application via the
Microsoft Store, then you need to modify your application to be published
as a packaged application (MSIX format).
MSIX is a Windows app package format that provides a modern
packaging experience to all Windows apps.
In order to build a packaged application, you can open your
WidgetBoard.csproj file and modify the following line (change in bold):
<WindowsPackageType>MSIX</WindowsPackageType>
518
Chapter 16 releasing Our appliCatiOn
Where ABC123 will be the value of the Thumbprint for the certificate
that you just created.
Once you have run the above command, you will find that the tooling
has created a file named WidgetBoard.msix under the WidgetBoard/bin/
Release/net9.0-windows/publish/win10-x64 folder.
Additional Resources
Microsoft provides documentation on how to distribute applications via
the Microsoft Store.
• Microsoft: How to publish an application ready for the
App Store, https://learn.microsoft.com/dotnet/
maui/windows/deployment/overview
519
Chapter 16 releasing Our appliCatiOn
520
Chapter 16 releasing Our appliCatiOn
dotnet test
This is far simpler than the publishing step. Running the tests in a CI
environment really should be considered a critical set of criteria when
building any application. The safety net that this provides in making sure
your changes do not unintentionally break other bits of functionality alone
makes it worthwhile.
Performance
Android has always been one of the slower platforms when building
mobile applications. Don’t get me wrong; the applications can perform
well on the higher-end devices, but Android devices come in a wide
range of specifications, and typically in the business environment, it is
the cheaper devices that get bought in bulk and are expected to perform
well. There are some concepts that you should consider when publishing
your Android applications in order to boost the performance of your
applications.
521
Chapter 16 releasing Our appliCatiOn
Startup Tracing
There are some extra steps that you can do in order to boost the startup
times of your Android applications. Startup tracing essentially profiles
an application when it starts to determine what libraries and other
initializations are required so when you release the application, it will
benefit from a faster startup time. It is worth noting that boosting the
startup time can result in an increase in application size, so I recommend
playing around with the settings to find the right balance for your
application.
Microsoft has published two great blog posts on how startup tracing
can be configured, the improvements it makes, and how the application
can be affected:
• https://devblogs.microsoft.com/dotnet/dotnet-7-
performance-improvements-in-dotnet-maui/
• https://devblogs.microsoft.com/xamarin/faster-
startup-times-with-startup-tracing-on-android/
Image Sizes
One thing that can perform really poorly is the use of images that do not
match the dimensions in which they need to be rendered on screen.
For example, an image that displays at 100×100 pixels in the application
really should be that size when supplied. If you were to render an image
that was actually 300×300 pixels, it will not only look poor on the device
due to scaling, but it will slow the application down. Plus, it involves
storing an image that is bigger than really needed. Therefore, make sure
that your images are correctly sized to gain the best experience when
rendering them.
522
Chapter 16 releasing Our appliCatiOn
Use of ObservableCollection
A lot of common coding examples show how to bind an
ObservableCollection to the ItemsSource property of a control. This
can have its uses, but it can have a big performance overhead. The reason
is that each time an element is added to the collection, a UI update will
be triggered because the control is monitoring for changes against the
ObservableCollection. If you do not need live updating items in a
collection, it is typically much faster to use a List and simply raise the
PropertyChanged event from INotifyPropertyChanged instead.
Let’s take a look at the code you added in Chapter 9 and see how it can
be improved:
523
Chapter 16 releasing Our appliCatiOn
This new code will result in the UI only being updated once rather than
once per each board that is added to the Boards collection.
Additional Resources
We have covered a large number of techniques and good practices to
follow throughout the course of this book in order to avoid a poorly
performing application. Microsoft does provide guidance on how to detect
issues and resolve them here: https://learn.microsoft.com/dotnet/
maui/deployment/performance.
Trimming
While devices these days do tend to offer generous amounts of storage
space, it is still considered a very good practice to minimize the amount of
memory your apps really consume, especially when considering mobile
devices that have limited data networks in order to download the apps.
What Is Trimming?
Trimming is performed by the tooling to remove unused code from
compiled assemblies. This helps to reduce the size of your applications
by trimming out any unused parts of libraries that you use. By default,
trimming is set to partial trimming, which means that only the .NET MAUI
assemblies will be scanned and have any unused code removed; this is
because those assemblies have been updated to be trim safe.
524
Chapter 16 releasing Our appliCatiOn
525
Chapter 16 releasing Our appliCatiOn
We have covered XAML and its limitations, but another key feature
to avoid is Reflection, or if you do use it, be careful to make sure that the
functionality the code is reflecting over will not be trimmed out. Not only
can it trick the compiler into not realizing APIs are used but it can also not
perform well.
It is worth considering that some third-party packages that you end
up using in your applications may not be trimmer safe. For this reason,
the default setting of partial is set. This means that only the assemblies
provided by Microsoft will be linked because they are built to be trimmer
safe. In an ideal world, the third-party libraries would also be trimmer
safe, but I can safely say that the people building these fantastic packages
are already spread thin building them, so if it is something that you really
require, I strongly urge you to investigate helping them provide it or
sponsoring the people that build it to help them.
Enable Trimming
You can turn on full trimming with
<TrimMode>full</TrimMode>
If you make the change above in the WidgetBoard.csproj file and then
run the dotnet publish command again, you should see the following
warnings being reported. Note that I have run the command for net9.0-
ios, but you could run it against any target you desire.
Warning IL2087
The first warning that I wanted to highlight is reported as follows:
/Users/shaunlawrence/Documents/work/projects/introducing-maui-
samples/second-edition/WidgetBoard/WidgetBoard/MauiProgram.
cs(95,9): Trim analysis warning IL2087: WidgetBoard.MauiProgram.
AddPage<TPage,TViewModel>(IServiceCollection, String): ‘serviceType’
argument does not satisfy 'DynamicallyAccessedMemberTypes.
526
Chapter 16 releasing Our appliCatiOn
This will resolve the warning and make sure that trimming does not
introduce any unexpected behavior.
Warning IL2026
This warning is reported multiple times, and while the warning message
might be similar, the fix is slightly different for each example that we will
work through. The first warning is reported as
ILLink : Trim analysis warning IL2026: WidgetBoard.ViewModels.
BoardDetailsPageViewModel: Using member 'Microsoft.Maui.Controls.
QueryPropertyAttribute.QueryPropertyAttribute(String, String)' which
has 'RequiresUnreferencedCodeAttribute' can break functionality when
trimming application code. Using QueryPropertyAttribute is not trimming
friendly and might not work correctly. Implement the IQueryAttributable
527
Chapter 16 releasing Our appliCatiOn
[QueryProperty(nameof(BoardCreatedCompletionSource), "Created")]
528
Chapter 16 releasing Our appliCatiOn
Warning IL2026
This is the second occurrence of the IL2026 warning that we should be
observing and fixing. You will notice that the error message is different to
the previous section despite it being the warning; it is reported as
/Users/shaunlawrence/Documents/work/projects/introducing-
maui-samples/second-edition/WidgetBoard/WidgetBoard/obj/Release/
net9.0-ios/ios-arm64/Microsoft.Maui.Controls.SourceGen/Microsoft.Maui.
Controls.SourceGen.CodeBehindGenerator/Pages_BoardDetailsPage.
xaml.sg.cs(30,3): Trim analysis warning IL2026: WidgetBoard.Pages.
BoardDetailsPage.InitializeComponent(): Using member 'Microsoft.Maui.
Controls.Xaml.Internals.XamlTypeResolver.XamlTypeResolver(IXmlName
spaceResolver, Assembly)' which has 'RequiresUnreferencedCodeAttribute'
can break functionality when trimming application code. Loading XAML
at runtime might require types and members that cannot be statically
analyzed. Make sure all of the required types and members are preserved.
You may also recall me saying that there are two warnings reported
in the “Generating Your iOS Application” section; this is where we take a
closer look at that. For the sake of repeating myself, you can see that the
original warning is as follows:
1>BoardDetailsPage.xaml(54,38): Warning XC0045 XamlC:
Binding: Property "IsChecked" not found on "WidgetBoard.ViewModels.
BoardDetailsPageViewModel".
The main reason I am repeating this here is to show the difference
between the warning you see before setting TrimMode to Full and after. I
want to highlight that the warning before actually tells you how to fix the
issue whereas I am not sure I would know where to start with the new
warning. With this in mind, I would fully recommend working through
all warnings in your application prior to enabling features like trimming
or NativeAOT. Now that we have some valuable context in the original
warning message, let’s proceed to fixing it.
529
Chapter 16 releasing Our appliCatiOn
<VerticalStackLayout IsVisible="{Binding
IsChecked, Source={x:Reference FixedRadioButton},
x:DataType=RadioButton}">
This change tells the XAML compiler what type is being provided to
the Source property and means that a compiled binding can be created.
This time the type being provided is of type RadioButton.
There is also a second instance of this warning to fix. I am only
including the warning which is reported prior to enabling full trimming
mode because it tells us how to fix it.
<Picker
ItemsSource="{Binding AvailableWidgets}"
SelectedItem="{Binding SelectedWidget}"
SemanticProperties.Description="{Binding Text,
Source={x:Reference SelectTheWidgetLabel}, x:DataType=Label}"
This change tells the XAML compiler what type is being provided to
the Source property and means that a compiled binding can be created.
This time the type being provided is of type Label.
This now concludes the changes required to enable full trimming
mode. You won’t be clear of trimmer warnings, but the changes required to
fix them are the same for when enabling NativeAOT support which we will
now cover.
530
Chapter 16 releasing Our appliCatiOn
Ahead-of-Time Compilation
Ahead of Time or AOT for short is the process. At the time of writing, AOT is
not supported for Android, but all other platforms are supported.
We covered back in Chapter 1 how .NET MAUI applications run on
the Mono runtime on Android, iOS, and macOS. By enabling AOT in a
.NET MAUI application, our applications will run on an entirely different
runtime – the NativeAOT runtime.
Enable NativeAOT
In order to enable AOT compilation in your applications, you can add the
following to your project. In fact, let’s add it to the WidgetBoard.csproj file.
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
</PropertyGroup>
This will enable more analyzers as per the trimming option that you
turned on earlier. The warnings generated from the analyzers should
not be ignored because they will most likely lead to runtime errors. This
is a very important thing to consider when enabling trimming or AOT
support because you will most likely be able to compile and publish
your application; there is no guarantee that it will work though. I have
confidence that following the testing chapters earlier, you will have created
a suitable test suite to have the confidence that your application will
behave at runtime.
Currently at the time of writing, Refit is not trim or NativeAOT
compliant; therefore, despite showing how great it was to use back
in Chapter 11, if you want to build an application that fully supports
NativeAOT, you would need to remove the dependency on Refit. Let’s
proceed to fixing this to truly make the application support NativeAOT.
531
Chapter 16 releasing Our appliCatiOn
Remove Refit
The first step is to remove the Refit NuGet package from the project. I have
opted to do this directly in the project file. Open the WidgetBoard.csproj
file and delete the following entry:
<PackageReference Include="Refit.HttpClientFactory"
Version="7.2.1" />
namespace WidgetBoard.Communications;
using System.Text.Json;
namespace WidgetBoard.Communications;
532
Chapter 16 releasing Our appliCatiOn
{
private readonly HttpClient httpClient;
private const string ServerUrl = "https://api.
openweathermap.org/data/2.5/weather?";
533
Chapter 16 releasing Our appliCatiOn
builder.Services
.AddRefitClient<IWeatherForecastService>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri
("https://api.openweathermap.org/data/2.5"))
.AddStandardResilienceHandler(static options =>
{
options.Retry = new HttpRetryStrategyOptions
{
BackoffType = DelayBackoffType.Exponential,
MaxRetryAttempts = 3,
UseJitter = true,
Delay = TimeSpan.FromSeconds(2)
};
});
Wants to be changed to
builder.Services
.AddHttpClient<WeatherForecastService>()
.AddStandardResilienceHandler(static options =>
{
options.Retry = new HttpRetryStrategyOptions
{
BackoffType = DelayBackoffType.Exponential,
MaxRetryAttempts = 3,
UseJitter = true,
Delay = TimeSpan.FromSeconds(2)
};
});
534
Chapter 16 releasing Our appliCatiOn
builder.Services.AddSingleton<IWeatherForecastService,
WeatherForecastService>();
Now that we have reintroduced the code, if you run a dotnet publish
command, you will see that the tooling now reports a warning; let’s take a
look at how to fix that.
Warning IL3050
The warning that the tooling reports is as follows:
1>WeatherForecastService.cs(29,16): Warning IL3050 : Using
member 'System.Text.Json.JsonSerializer.Deserialize<TValue>(String,
JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute'
can break functionality when AOT compiling. JSON serialization and
deserialization might require types that cannot be statically analyzed and
might need runtime code generation. Use System.Text.Json source generation
for native AOT applications.
The reason a warning is reported is due to the fact that the
deserialization code does not guarantee a hard reference to the properties
of the Forecast class being deserialized. Now it may be say because our
code might prevent the linker from removing the properties, but to be
safe, we can make use of a source generator provided by System.Text.Json
to make sure nothing will be trimmed away. In order to prevent this from
happening, we can create a ForecastContext class and pass it into the
Deserialize method. In fact, the last part of the warning message told us
what to do just not how to do it. Let’s take a look at this now; you should
add the following class into your Forecast.cs file:
[JsonSerializable(typeof(Forecast))]
partial class ForecastContext : JsonSerializerContext
{
}
535
Chapter 16 releasing Our appliCatiOn
return JsonSerializer.Deserialize<Forecast>(stringContent,
ForecastContext.Default.Forecast);
This will tell System.Text.Json to use the new generator parsing context
and make sure that no properties for the Forecast class are trimmed
out. Running a dotnet publish command should also confirm that this
warning has now been removed.
<ItemGroup>
<TrimmerRootAssembly Include="LiteDB" />
</ItemGroup>
The above changes will exclude LiteDB from being trimmed from your
application.
This now concludes the changes required to resolve warnings for
trimming and NativeAOT support; let’s take a look at the improvements
it makes.
536
Chapter 16 releasing Our appliCatiOn
Results
I mentioned earlier that the use of trimming and NativeAOT can reduce
application sizes. Table 16-1 shows the improvements.
Table 16-1. Application sizes when using different trim modes and
NativeAOT
Android iOS Mac Catalyst Windows
Crashes/Analytics
Given that I have covered how things can go wrong, I would like to cover
a way in which you can gain insight to when that happens. Each of the
platform providers does offer a way to collect crash information and report
it to you in order to make sure that you can prevent things like crashes
from ruining the experience your applications provide.
There are frameworks/packages that aim to make this process easier by
collecting and collating information from each platform into a centralized
site. Further to this, you can enable the collection of analytic information
to aid your understanding of how your users like to interact with your
application and identify areas that you can improve upon.
537
Chapter 16 releasing Our appliCatiOn
In fact, a lot of the effort in my day job goes into finding ways to
improve products. This only truly comes to light when you learn how your
users interact with your applications. Capturing analytic information
isn’t the sole route I recommend taking. End user engagement can also
be a fantastic thing to do if you have the opportunity. I would also like
to highlight things like App Tracking Transparency by Apple and the
Google equivalent as you want to make sure that when collecting analytic
information, you are not passing on information that can be used to track
your users, or you at least make them aware of it. Further to this, it is
considered good practice to allow users to opt in to enable the collection of
analytical information rather than just capturing it or making them opt out.
There are some companies that provide solutions for this already. They
are fee based but do offer a free tier with fewer features.
Sentry
Sentry offers a .NET MAUI package that will make it easier to collect crash
and analytical information. The website contains details on its usage and
pricing: https://sentry.io/for/dot-net/.
Sentry also has the source code open sourced on GitHub and provides
usage examples as well as assisting in understanding what the code does:
https://github.com/getsentry/sentry-dotnet/tree/main/src/
Sentry.Maui
Obfuscation
It is a very safe assumption that if you are providing a compiled application
to users’ devices, any of the code in the application can be compromised,
intellectual property (IP) can be stolen, or an attacker can learn about
vulnerabilities in your application. If you really wish to retain your IP,
then you likely want to keep it on a server-side component and have your
538
Chapter 16 releasing Our appliCatiOn
application call it via a web API. That being said, there is still serious value
in making use of tools that obfuscate the compiled code base to make it
more difficult for an attacker to decipher what the application is doing.
Let’s take a look at a simple class and how it will look when decompiled
after obfuscation.
The code decompiled using ILSpy without being obfuscated first looks
as follows:
using System;
public class SomethingSecure
{
private string PrivateSecret { get; } = "abc";
internal string InternalSecret { get; } = "def";
public string PublicSecret { get; } = "ghi";
}
If you run the original code through an obfuscation tool and then
decompile the source, you will end up with something like the following:
// \u0008\u0002
using System;
[\u000f\u0002(1)]
[\u000e\u0002(0)]
public sealed class \u0008\u0002
539
Chapter 16 releasing Our appliCatiOn
{
private readonly string m_\u0002 = \u0002\u0003.\
u0002(-815072442);
private readonly string m_\u0003 = \u0002\u0003.\
u0002(-815072424);
private readonly string m_\u0005 = \u0002\u0003.\
u0002(-815072430);
private string \u0002()
{
return this.m_\u0002;
}
internal string \u0003()
{
return this.m_\u0003;
}
public string \u0005()
{
return this.m_\u0005;
}
}
It is clear from the above that it is much more difficult now to follow
what this code is doing.
Obfuscation does not make it impossible for attackers to gain an
understanding of what the code does. It does, however, make that task
much more difficult. I would also add that you should consider any of
the code within your client-based applications as insecure; ultimately if
someone wanted to break into it to understand what is happening, they
can and will. I would highly recommend making sure that any items such
as passwords or web service tokens are stored in places like SecureStorage
and not directly in code. For any important algorithms you implement,
540
Chapter 16 releasing Our appliCatiOn
if you want these to remain your own intellectual property, you should
keep these deployed to a server-side component which you can control
access to.
Summary
In this chapter, you have
541
Chapter 16 releasing Our appliCatiOn
542
CHAPTER 17
Conclusion
Abstract
Wow! If you made it this far, I want to thank you so much! I really hope that
you have enjoyed reading this book as much as I enjoyed writing it. This
book was designed to give you an insight into what .NET MAUI offers and
how you can use it to build real-world applications. The sample we built
together covers a lot of the key concepts. Of course I could have filled the
book with hundreds more pages, adding in so many more widgets and
features to the application. This application is a concept that is near and
dear to my heart, so I can tell you that it will continue to evolve over time. I
would love to hear where you decide to take it next, and I would love to see
what you create next.
The process of building this application has taken you through many
different concepts including
• Creating a .NET MAUI application project
544
CHAPTER 17 CONCLUSION
All of the items above made for a really fun journey! And the end
result is almost identical to my original plan. Figure 17-2 shows the final
application with the widgets.
Figure 17-2. The final application showing the widgets that we have
added plus the results of some of the extra assignment sections
545
CHAPTER 17 CONCLUSION
Finally, in Figure 17-3, you can see the end result running on an old
Kindle Fire HD device that I have sitting on my home office desk.
546
CHAPTER 17 CONCLUSION
I am repeating myself here, but I would really love to hear from you
about your experience reading this book and where you have decided to
take our application next.
Useful Resources
There are so many great places to find information on either building .NET
MAUI applications or solving issues that may arise during that experience. The
following list is a collection of websites that provide some really great content
along with a few specific examples of content creators on those platforms.
StackOverflow
Stack Overflow (https://stackoverflow.com) is a question-and-answer
site where you can seek assistance for issues that you encounter. Often
someone else has already asked the question so you can find the answer
you need. If you can't find a .NET MAUI-specific question/answer, it is
worth also looking for Xamarin.Forms question/answers given that it is the
predecessor to .NET MAUI.
547
CHAPTER 17 CONCLUSION
GitHub
GitHub (https://github.com/dotnet/maui) is where the .NET MAUI
repository is hosted and the framework is developed in the open. I strongly
recommend keeping up to date with the discussions and issues on this
repository.
YouTube
There are some really great content creators providing video tutorials
on how to build .NET MAUI applications. Two great creators are in fact
Microsoft employees; however, they build this content in their own free
time, which I believe goes to show just how passionate they are about the
framework.
Gerald Versluis
www.youtube.com/c/GeraldVersluis
James Montemagno
www.youtube.com/c/JamesMontemagno
Social Media
There is a whole host of social media options such as LinkedIn, Discord,
Twitter, Bluesky, and Facebook. I urge you to find the platform that works
best for you and start finding and following people that work on or with the
technology.
548
CHAPTER 17 CONCLUSION
Looking Forward
While .NET MAUI offers us a lot, there is still so much more that will
evolve. I fully expect there to be some extensive work applied to improving
the ability to test the user interfaces of .NET MAUI applications along with
further enhancements in the usage of .NET MAUI Graphics, which has the
potential to not only render applications identically across each platform
(which is very similar to how Flutter works) but also to boost performance
by moving away from the native controls that come with Android.
I feel the need to highlight the lack of sections here. In the first edition,
I had points highlighting what I wanted to see in terms of better testing
support. I was able to delete all of those items in this update because .NET
MAUI provides support for all of it. That isn’t to say it won’t and shouldn’t
continue to evolve. I am sure concepts like automation testing will
continue to become easier.
Thank you again for reading!
549
Index
A Warning IL3050, 535, 536
Analog clock widget
AbsoluteLayout, 118–120, 225
creation, 493–501
Accessibility
registering, 502
applications
AnalogClockWidgetView, 494
Android, 245
modification, 494–496
iOS, 245
AnalogClockWidgetViewModel
macOS, 246
contents, 497–503
Windows, 246
AnalogClockWidgetViewModel.cs
definition, 227
file, 499
dynamic text sizing, 240–245
Android
principles, 228, 229
Appium, 454
reasons, 228
application, 509, 510
resources
generation, 508
checklist, 247, 248
image sizes, 522
website, 248
mappers, 413
screen reader support, 230–237
ObservableCollection, 523, 524
suitable contrast, 238–240
platform-specific
Adaptive icon, 111
components, 397–399
AddAdditionalAppiumOption, 466
Android App Bundle (AAB),
AddBoardCommand, 178, 179
507, 509
AddScoped, 60
Android driver, 454, 466–467
AddSingleton, 59
Android Package (APK), 507, 508
AddTransient, 60, 527
Android platform, 46, 47
AddWidget method, 259, 310
accessibility, 245
Ahead-of-time (AOT), 11
app icons, 111
Ahead-of-time compilation
platform-specific lifecycle
AOT compilation, 531, 532
events, 67, 68
Refit NuGet package, 532–535
552
INDEX
553
INDEX
554
INDEX
555
INDEX
556
INDEX
557
INDEX
558
INDEX
559
INDEX
560
INDEX
561
INDEX
562
INDEX
563
INDEX
564
INDEX
565
INDEX
566
INDEX
layouts, 118–129 W
prerequisites, 105–109
Warning IL2026, 527–530
splash screen, 112, 113
Warning IL2087, 526, 527
XAML, 113–118
Warning IL3050, 535, 536
User interface renders, 13, 14
WeatherForecastService class, 532
WeatherForecastService Mock,
V 432, 433
ValidForecastResultsInSuccessful WeatherWidgetView, 359–362
Load test, 445 WeatherWidgetViewModel,
VerifyTests, 445 362–365, 428, 445
VerticalStackLayout, 115, Web Assembly (WASM) support, 13
116, 126–129 Web Content Accessibility
View-based file, 113 Guidelines (WCAG),
ViewModels, 78–80 228, 247
adding IWidgetViewModel, 90 Web services
MVVM-based architecture, 89–94 adding Refit NuGet
Shell, 147–149 Package, 379–381
user interface essentials, 108, 109 code generation libraries, 379
Views, 77, 78, 94–97 converting state to UI, 371, 372
Vision impairment, 228 displaying error state, 375, 376
VisualElement, 274, 275, 277, 280 displaying loaded state, 374
Visual Studio, 15, 16 displaying loading state, 372
automation testing, 471 loading code, 369
building and running, 37–39 network resilience handling,
build target selection 376, 378
drop-down, 38 Open Weather API, 350–368
configure your project dialog, 35 prebuilt libraries, 379
creating, 33–36 visual feedback, 369
macOS, 27–30 WidgetBoard, 38, 40, 41
toolbar with Pair to Mac WidgetBoard.AutomationTests
buttons, 30 project, 472
Windows, 25, 26 WidgetBoard Project, 74, 82, 172,
Visual Studio Code, 16 285, 440
Visual Studio Installer, 31 WidgetBoard.SnapshotTests, 445
567
INDEX
568