Desktop & Mobile Programming with .NET MAUI & C#
Desktop & Mobile Programming with .NET MAUI & C#
Programming with
.NET MAUI and C#
KAMIL PAKULA
Copyright © 2024 Kamil Pakula
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any
form or by any means, without the prior written permission of the author.
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
iii
CONTENTS
Preface ..............................................................................................................................................................xi
Chapter 1 - Introduction to .NET MAUI and the Slugrace Project ........................................................ 1
What Is .NET MAUI? .................................................................................................................................. 1
How Does .NET MAUI Work? .................................................................................................................. 1
The Slugrace Project .................................................................................................................................... 1
Settings Page............................................................................................................................................. 2
Race Page .................................................................................................................................................. 3
Bets and Results ....................................................................................................................................... 4
Game Over Page ...................................................................................................................................... 5
What You Need ............................................................................................................................................ 6
Chapter 2 – Creating the Project................................................................................................................... 7
Android Emulator ....................................................................................................................................... 8
Anatomy of a .NET MAUI Project ............................................................................................................. 9
The App.xaml and App.xaml.cs Files ................................................................................................. 10
The AppShell.xaml and AppShell.xaml.cs Files ................................................................................ 11
The MainPage.xaml and MainPage.xaml.cs Files ............................................................................. 12
The MauiProgram.cs File ...................................................................................................................... 16
The Platforms Folder ............................................................................................................................. 17
App Startup Flow of Control ............................................................................................................... 18
The Project File ....................................................................................................................................... 19
The Resources Folder ............................................................................................................................ 20
The Other Folders .................................................................................................................................. 20
Chapter 3 - Introduction to XAML ............................................................................................................. 21
Why Use XAML? ....................................................................................................................................... 22
Views in XAML .......................................................................................................................................... 23
The Label View .......................................................................................................................................... 23
Property Attributes .................................................................................................................................... 24
Hot Reload .................................................................................................................................................. 25
XAML vs C# ............................................................................................................................................... 26
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
v
The GameInfo ContentView .............................................................................................................. 100
The SlugsStats and SlugStats ContentViews .................................................................................... 102
The PlayersStats and PlayerStats ContentViews ............................................................................. 103
The Graphical Assets ........................................................................................................................... 105
The Racetrack, SlugImage, TrackImage, SlugInfo and WinnerInfo ContentViews .................... 106
The Bets and PlayerBet ContentViews .............................................................................................. 111
The Results and PlayerResult ContentViews ................................................................................... 113
GameOverPage ........................................................................................................................................ 115
Chapter 9 - Styles in XAML ...................................................................................................................... 117
About Styles.............................................................................................................................................. 117
Style Object and ResourceDictionary .................................................................................................... 118
Implicit vs Explicit Styles ........................................................................................................................ 120
Style Inheritance....................................................................................................................................... 122
Styles Applicable to Multiple Types ..................................................................................................... 123
Style scopes ............................................................................................................................................... 124
Precedence of Styles ................................................................................................................................ 128
Styles in the Slugrace Project .................................................................................................................. 131
ContentPages ........................................................................................................................................ 131
Borders .................................................................................................................................................. 133
Buttons .................................................................................................................................................. 135
The Other Controls .............................................................................................................................. 137
Chapter 10 - Visual States and Control Templates ............................................................................... 145
Visual States Terminology ...................................................................................................................... 145
CommonStates ......................................................................................................................................... 146
Visual States in a Style ............................................................................................................................ 149
Specific Visual States ............................................................................................................................... 150
ControlTemplates .................................................................................................................................... 151
Custom Visual States ............................................................................................................................... 153
Setting States on Other Objects .............................................................................................................. 162
Chapter 11 - Markup Extensions .............................................................................................................. 166
Shared Resources and the StaticResource Markup Extension ........................................................... 166
vi
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
vii
Binding Fallbacks ..................................................................................................................................... 235
Multi-bindings ......................................................................................................................................... 236
Chapter 16 – Introduction to MVVM ...................................................................................................... 240
How MVVM Works ................................................................................................................................ 240
The Three Components of MVVM ........................................................................................................ 241
Basic MVVM ............................................................................................................................................. 242
Commands ................................................................................................................................................ 246
Binding Context ....................................................................................................................................... 248
Chapter 17 – The MVVM Toolkit ............................................................................................................ 251
Installing the MVVM Toolkit ................................................................................................................. 251
Implementing the MVVM Toolkit ......................................................................................................... 252
MVVM Toolkit Commands .................................................................................................................... 255
Models in the Slugrace Application ...................................................................................................... 256
PlayerSettings View and View Model .................................................................................................. 258
ZeroToEmptyStringConverter ............................................................................................................... 263
Behaviors................................................................................................................................................... 264
SettingsViewModel ................................................................................................................................. 266
SettingsPage View ................................................................................................................................... 270
EventToCommandBehavior ................................................................................................................... 273
The Messaging System ............................................................................................................................ 275
Chapter 18 - Navigation ............................................................................................................................. 280
Navigation to Another Page ................................................................................................................... 280
Passing Data Between Pages .................................................................................................................. 283
Navigating Back ....................................................................................................................................... 287
GameViewModel ..................................................................................................................................... 289
GameInfo .................................................................................................................................................. 293
SlugViewModel........................................................................................................................................ 297
PlayerViewModel .................................................................................................................................... 301
Slugs’ Stats and Slug Stats ...................................................................................................................... 308
Players’ Stats and Player Stats ............................................................................................................... 310
Slug Image, Slug Info and TrackImage ................................................................................................. 312
viii
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
ix
Chapter 21 - Popups ................................................................................................................................... 389
Quit Popup ............................................................................................................................................... 389
Accidents................................................................................................................................................... 392
Assets ..................................................................................................................................................... 394
Accident Model Class .......................................................................................................................... 394
AccidentViewModel ............................................................................................................................ 395
Controlling Accidents ......................................................................................................................... 398
Handling the Accidents ...................................................................................................................... 402
Accident Popup.................................................................................................................................... 407
Accident Sound .................................................................................................................................... 409
Testing Accidents ................................................................................................................................. 412
Broken Leg Accident ........................................................................................................................... 414
Overheat Accident ............................................................................................................................... 419
Heart Attack Accident ......................................................................................................................... 421
Grass Accident ..................................................................................................................................... 422
Asleep Accident ................................................................................................................................... 424
Blind Accident ...................................................................................................................................... 425
Puddle Accident................................................................................................................................... 427
Electroshock Accident ......................................................................................................................... 428
Turning Back Accident........................................................................................................................ 430
Devoured Accident .............................................................................................................................. 431
Chapter 22 - Final Touches ........................................................................................................................ 433
Fixed Window Size on Windows .......................................................................................................... 433
InstructionsPage....................................................................................................................................... 434
Chapter 23 - Deployment .......................................................................................................................... 438
Publishing for Windows to a Folder ..................................................................................................... 438
Publishing for Android for Ad-Hoc Distribution ............................................................................... 446
Conclusion ................................................................................................................................................... 452
Index.............................................................................................................................................................. 453
x
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Preface
This book covers all the basics that you need to start programming desktop and mobile
applications with C# and .NET MAUI. I assume you have no prior knowledge of the .NET MAUI,
but you should have at least some basic knowledge of the C# programming language and the
object-oriented programming paradigm.
After a short introduction to .NET MAUI, we’ll start by setting up our working environment. Then
we’ll be talking about some basic stuff like views, layouts, content pages, content views and XAML.
Next, we’ll discuss properties, styles, themes, visual states, control templates, markup extensions,
platform differences, namespaces, data binding, the MVVM pattern, navigation, animation, sound
and popups, to mention just the most important topics. In parallel, we’ll be creating the Slugrace
project - a racing game that we will be developing throughout the whole book. You will find the
code and assets that you need for the project (images and sound files) on Github. There is a
separate branch for each chapter. You will find the links at the beginning of each chapter. Finally
we’ll deploy the app to Windows and Android.
There’s one convention I’m using in the book - the parts of code that were modified or added are
highlighted in a shade of yellow so that you can immediately see the changes. Portions of code that
did not change but I’d like to draw your attention to are highlighted in gray.
I hope you’ll have at least as much fun reading the book as I had writing it.
xi
Chapter 1 - Introduction to .NET MAUI
and the Slugrace Project
In this book we will learn the basics of .NET MAUI, which is a cross-platform framework used for
GUI applications of any kind. As learning by doing is a concept I’m a big fan of, throughout the
book we’ll be creating an app, which actually is a betting/racing game. We’ll be creating two
versions of the game, for desktop and mobile. Before we have a closer look at the app we’re about
to start creating, though, let’s talk about the .NET MAUI framework.
In .NET MAUI we use C# to write the code. Although we can use just C# for most of our project,
there’s also the XAML markup language that, although optional, is practically always used because
it makes it so much easier to create the presentation part of the app. In our project we’ll be using C#
and XAML.
You can use .NET MAUI to create apps for Windows, macOS, Android and iOS from a single
shared code base.
1
The app is going to contain three pages that need access to data (Settings Page, Race Page and
Game Over Page and one that doesn’t (Instructions Page).
Settings Page
When you start the game, you first see the Settings Page. Here’s the desktop version:
There’s a background image of the four slugs in the upper part of the page. The slugs even have
names. From left to right these are:
2
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
- the ending conditions (when the game should end – there are
three options to choose from).
The mobile version looks a bit different, but it has the same
functionality. Here you can see only a small portion of the
background image.
Race Page
When you’re done setting the players’ names and initial money,
and the ending condition, you just press the Ready button. When
you do, you navigate to the Race Page, which is the main page of
the app. You will spend most of your time on this page unless
you lose right away.
3
- the Players’ Stats panel where you can see the names of the
players and their current amount of money,
- the main game panel in the middle with the racetrack where the
slugs run (you can also see the odds here, which are updated
after each race),
- the Bets panel where you can place your bets by typing them in
or using a slider.
Here, on the right, you can see the mobile version of this page.
You can see how much money each player had before the last race, how much they bet and on
which slug and whether they won or lost and how much.
When you press the Next Race button in the Results panel, it’ll change back to the Bets panel.
4
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
- End Game – this button lets you end the game at any time and
takes you to the Game Over Page,
- Instructions – this button takes you to the Instructions Page,
- Sound – this button toggles the sound on or off.
The mobile version looks pretty much the same, as you can see below.
So, this is the application we’re going to create using the .NET MAUI framework and C#. But what
do we need to get started?
5
What You Need
You definitely need a text editor or IDE to write your code in. In
this book we’ll be using Visual Studio 2022 with the .NET Multi-
platform App UI development workload installed, so make sure
you have it on your computer and you’ll be good to go.
As we want to build our app also for Android, you also need an
Android device or an emulator. In this book we’ll be using the
latter.
In the next chapter we’ll create the project and we’ll create an
Android emulator.
6
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
7
We’re going to
target .NET 8.0, so
make sure it’s
selected in the
dropdown list. Then
hit Create.
Android Emulator
For the Android version of the app we’ll be using an emulator. Installing an emulator is described
in the .NET MAUI documentation, so I’m not going to repeat it here. The instructions there are
pretty clear. Just make sure to enable hardware acceleration or else your emulator will work very
slowly. To create an emulator, in Visual Studio, in the Tools menu, go to Android and select
Android Device Manager:
There’s just one on my machine right now. Your list will most probably be empty when you open
this window for the first time. Just click the +New button and select the device you want to use to
emulate a real Android device. The installation may take some time. You can pick a model and set
some properties of the device:
8
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Hit the Create button when you’re done. Consult the documentation for more details. Anyway, I
assume you have Visual Studio 2022 installed (the free Community version is just fine) and also an
Android emulator. If so, we’re good to go.
9
These are the code-behind files that are used to handle the logic of their corresponding XAML files.
Whenever we create a XAML file later in the book, it will always be accompanied by a
corresponding code-behind file.
Anyway, what are these three XAML files and their corresponding code-behind files for? Let’s have
a look at them one by one.
We’re going to talk about XAML syntax in detail later in the book, but if you know XML, you will
notice lots of similarities here. So, here we have the root element, Application, and in it is nested
the Application.Resources element. The nesting is clearly visible due to indentation. Inside the
Application.Resources element, you can see the ResourceDictionary element, the latter
nested in the former. If we move deeper and deeper in the hierarchy, we have the
ResourceDictionary.MergedDictionaries element and in it two ResourceDictionary
elements. The App.xaml file defines the application resources that are available in the entire
application. Not just in a single page, not in a couple selected pages, but everywhere. We’ll be
talking about ResourceDictionary elements and their scope in the application in due time.
Now, if you look at the innermost ResourceDictionary elements, you will see they have their
Source attributes set to the Colors.xaml and Styles.xaml files contained in
the Styles folder inside the Resources folder. Check it out. Expand the
Resources folder in the Solution Explorer and you will see the two files
there.
10
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
namespace Slugrace;
Here we have the App class that inherits from Application. This class represents the application at
runtime. As you can see, the class is defined as partial, which means the code you see in the file is
not the full code the class consists of. The rest of the code is somewhere else, and to be precise, it’s
in the XAML file. So, the code in the App.xaml file is combined with the code in the code-behind file
to produce the complete App class.
In the constructor, you can see the InitializeComponent method. This method is called right to
create the class instance by merging with the code in the XAML file. You will see this method in
every constructor in the code-behind files that have an accompanying XAML file. On the other
hand, you won’t see it if you write all your code in C#, without the XAML file, which is also
possible (we’ll see how to do it soon), although not recommended.
Finally, the code in the constructor creates the initial app window and assigns it to the MainPage
property. This way the application knows what to display when you run it. As you can see, the
initial window is created by instantiating the AppShell class. Speaking of which…
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>
11
Here we have Shell as the root element. The purpose of this element is to give your app some
structure.
Your app may be structured in a couple ways. There may be a flyout, there may be tabs or just a
single page, which is the case here.
The single page that we get out of the box is represented by the ShellContent element. The
attributes that are set on this element include the title of the page, the template of the page and the
URI-based route to MainPage, which is to be displayed when the app starts running.
namespace Slugrace;
We just have the InitializeComponent method in the constructor that instantiates the AppShell
class.
As you can see in the XAML file above, the first page that is going to be displayed is MainPage.
Let’s have a look at it next.
<ScrollView>
<VerticalStackLayout
Padding="30,0"
Spacing="25">
<Image
Source="dotnet_bot.png"
HeightRequest="185"
Aspect="AspectFit"
SemanticProperties.Description="dot net bot in a race car number eight" />
<Label
Text="Hello, World!"
Style="{StaticResource Headline}"
SemanticProperties.HeadingLevel="Level1" />
12
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Label
Text="Welcome to .NET Multi-platform App UI"
Style="{StaticResource SubHeadline}"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Fill" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
This is the page that will be displayed when you run the app. The root element is ContentPage and
inside it is a ScrollView element. This element contains a VerticalStackLayout, which in turn
contains an image, two labels and a button.
These elements are added here so that you can see something on your screen when you run the
app, but eventually we will cut out all the code between the opening and closing tags of the
ContentPage and replace it with our own code. But let’s leave it for now. What can you expect to
see? Well, you’ll see an image, two labels and a button arranged vertically (this is what the
VerticalStackLayout is for). Besides, if the view is too large to fit in your screen, a scrollbar will
be provided (this is what the ScrollView is for).
Before we run the app and see what it all looks like, let’s have a look at the code-behind file. If you
open the MainPage.xaml.cs file, you’ll see this:
namespace Slugrace;
public MainPage()
{
InitializeComponent();
}
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
}
}
13
Here we have more code than in the other two code-behind files. In the constructor we have the
InitializeComponent method again. Below the constructor we have another method,
OnCounterClicked. This method takes care of the button click event and just displays the number
of clicks on the button. This method is needed for the button to work. So, let’s now run the app and
see what the MainPage looks like. Let’s run it twice, actually, first on Windows, then on Android.
Under the top menu you can see the Play button with a filled green triangle on it. It’s used to run
the app with debugging. Make sure Windows Machine is selected in the dropdown next to it.
Press the Play button. Visual Studio may prompt you to enable Developer Mode if you haven’t
done it before. Just click the settings for developers link and switch on Developer Mode. Close the
Enable Developer Mode for Windows dialog and now you can proceed. The app builds and runs.
You can now see the image, the two labels and the button stacked vertically:
14
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
You can also see some other elements, like the page title in the upper left corner, which we didn’t
define in the MainPage file, but they are defined somewhere else, in the layout, which we’re going
to see later on. Anyway, let’s try the app out. Click the button several times and watch the text on
it. Here’s what I could see after clicking the button 8 times.
Let’s close the app. You can close the app window directly (A) or hit the Stop button in Visual
Studio (B).
And now let’s run the app on Android. First of all, we have to select the device that we created in
before:
15
Now we can hit the Play button. It can take some time to start the
first time, but the next time it’ll be much faster. Anyway, here’s
what we can see after clicking the button several times.
Fine, stop the app now and let’s continue with our overview of
the project.
using Microsoft.Extensions.Logging;
namespace Slugrace;
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
This class is the common entry point for your app. As you will see in a minute, when we talk about
the Platforms folder, each native platform defines its own entry point. However, the entry point
code of each platform calls the CreateMauiApp method that is defined right here, in the
MauiProgram class. So, when each platform creates and initializes the app, it calls the method in
this common class.
But what does the CreateMauiApp method do? Well, it creates an app builder object which you
can use to configure your app. Here you can see it register some fonts, but we also use it to add
services for dependency injection and more. There is one thing the builder object always takes
16
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
care of. It associates the application with the application class. To do that, it calls the generic
UseMauiApp method and passes the application class as the type parameter, App. From now on, the
application is identified with the App class that we discussed before.
Now, as I mentioned, each platform has its own entry point. To see how it works, let’s move on to
the Platforms folder.
using Microsoft.UI.Xaml;
namespace Slugrace.WinUI;
For clarity, I removed the comments. The method is called in the last line of the code.
And now let’s have a look at the MainApplication.cs file in the Android folder:
using Android.App;
using Android.Runtime;
namespace Slugrace;
17
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
Again, the MauiProgram.CreateMauiApp method is called in the last line of the code.
1. Platform-specific initialization code is executed and calls the CreateMauiApp method in the
MauiProgram class.
3. The builder object associates the application with the App class.
builder.UseMauiApp<App>()
And only now the MainPage is displayed to you. Fine, and now let’s have a look at the remaining
folders in the project hierarchy.
18
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<TargetFrameworks
Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-
windows10.0.19041.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>Slugrace</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SupportedOSPlatformVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) ==
'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) ==
'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) ==
'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) ==
'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) ==
'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) ==
'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg"
ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
19
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True"
BaseSize="300,185" />
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility"
Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
</ItemGroup>
</Project>
Here you can see a PropertyGroup section and two ItemGroup sections. The PropertyGroup
contains information about the frameworks your app targets, app title, app version, and so on. You
can modify this code if you need to.
In the ItemGroup below you can see information about resources. The section specifies an app
icon, the splash screen and the default locations for images, fonts and other assets used by the app.
So, the project file contains information about the resources used in the app. But where actually are
the resources?
20
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the previous chapter we were discussing the structure of the default project that is created for us
when we select the .NET MAUI App template. As you saw, part of the code is written in C#, part in
XAML, which is an XML-like markup language used across several technologies such as .NET
MAUI, WPF, UWP or Xamarin, to mention just the most important ones.
You could create a project without XAML, with just C# code. The question is whether you should…
Before we talk about the advantages and disadvantages of XAML, though, let’s create a new page
in our Slugrace project which we will be using throughout this book to test new stuff. This page is
not going to be included in the project eventually, but I think it will be pretty helpful on the way. A
good name for this page would be TestPage. We’ll use the page to test new stuff before we
implement it in the actual project. So, open the Slugrace project, right-click the project name in the
Solution Explorer (Slugrace), select Add, then New Item… and in the window that opens select
.NET MAUI (A), then .NET MAUI ContentPage (XAML) (B), enter the name of the page (C) and hit
Add (D).
This will create two files in the root folder of the project, TestPage.xaml and its corresponding code-
behind file, TestPage.xaml.cs. The XAML file should open in the editor automatically. It has some
sample code that contains a Label element inside a VerticalStackLayout element. The Text
property of the label is set to “Welcome to .NET MAUI!”. Change it to something along the lines
of “Test Page” so that we know which page we’re on when it’s displayed. The full code of the
TestPage.xaml file should look like so:
21
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.TestPage"
Title="TestPage">
<VerticalStackLayout>
<Label
Text="Test Page"
VerticalOptions="Center"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ContentPage>
We also must tell the program to display this page when it’s executed instead of the MainPage.
Let’s make the changes in the AppShell.xaml file inside the ShellContent element. The title of the
page should be “Test”, the content template and route have to be changed accordingly:
Let’s test the app on Windows, so select Windows Machine on your Play button and hit the button.
You should see the TestPage with the text displayed correctly. Here’s the top part of the page:
The good news is that .NET MAUI is not the only technology that uses this markup language and if
you learn it, you will be able to use it (with slight modifications) in WPF or the other technologies
listed before. And besides, it’s not so difficult if you know any other markup language like XML or
HTML. There are lots of similarities.
So, is XAML worth learning or not? Well, I think it definitely is. Here are some reasons:
- it facilitates division of concerns: you use XAML for the UI and C# for the logic,
22
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
- it’s much easier and faster to create the UI of your app using a declarative markup language than
C# code after you master the former,
We’re going to see the difference between UI written with XAML and the same UI written with C#
in a moment. However, before we do that, let’s talk about the elements (like the label) that we place
in a XAML page. They are called views in XAML.
Views in XAML
In XAML most of the elements inherit from the View class. So, the layouts are views and practically
all the controls that we’ll be using in our project are views. I will be using the terms ‘view’ and
‘layout/control’ interchangeably in this book. One of the most commonly used views is Label. Let’s
talk about it first.
<Label
Text="Test Page"
VerticalOptions="Center"
HorizontalOptions="Center" />
As you can see, it has an opening angled bracket (<) and a closing one (>). In this particular case it’s
a self-closing tag, with a slash character preceding the closing bracket. We can use self-closing tags
if there is no content that should go between the opening and closing tag. But even with no content,
we still could use an opening and a closing tag. We can rewrite the code like so:
<Label
Text="Test Page"
VerticalOptions="Center"
HorizontalOptions="Center">
</Label>
Either way will do in this case. But if there were some content, we would have to use the latter
syntax. Let’s remove the Text attribute from the opening tag and use the same string as the content
of the label:
23
<Label
VerticalOptions="Center"
HorizontalOptions="Center">Test Page
</Label>
Now the closing angled bracket is required. Naturally, you could put everything on one line and it
would work the same:
The general rule is that if a view has some content, the content goes between the opening and
closing tags of the view. The content may be a simple string like here, but it may also be a more or
less complex hierarchy of other views. Like for example here - the Label is the content of the
VerticalStackLayout and the VerticalStackLayout is the content of the ContentPage. Views
nested inside other views are also referred to as their children.
Before we proceed, let’s restore the original shape of the label with the Text attribute set inside the
opening tag. As you can see, Text is not the only attribute here. Let’s have a look at the others.
Property Attributes
Just as the elements (like Label or Button) represent class instances (we call it Object Element
Syntax), so the attributes inside the opening tags of the elements are used to set the values of the
properties of those instances. This is why we call them property attributes. It’ll become even
clearer when you see the corresponding C# code. Let’s stay in the XAML file for a while, though.
There are three attributes on the Label instance right now. Let’s add some more. The order of the
attributes doesn’t matter. Here’s the modified code:
The names of the attributes are self-explanatory. Now run the app again to see the result:
24
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
One interesting thing is the FontAttributes attribute. There are two values assigned to it. If you
want to assign more than one value to an attribute (provided it’s at all possible), you separate them
with a comma.
And there are more attributes that you can use with a Label (as well as with any other view). Use
Intellisense to experiment if you like.
Now, before we proceed to the C# counterpart of the above XAML code, I’d like to draw your
attention to a very useful feature available in Visual Studio that will make your life a whole lot
easier.
Hot Reload
The feature I’m talking about is Hot
Reload. When you run the code, it
should be activated, but if it isn’t, you
can activate it manually. You will find
it here:
With this feature activated and your app still running, try to make some changes in the code. Let’s
change the values of some of the properties. Whenever you change a value, the change is
immediately reflected in the running app. Here’s what I tried:
...
<Label
Text="Test Page"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="Blue"
FontSize="60"
FontAttributes="Bold, Italic"
Rotation="180"
CharacterSpacing="20"
TranslationY="100" />
</VerticalStackLayout>
...
25
And here’s the result after my last modification:
So, with Hot Reload on, you can view your changes live in the running app without having to stop
and restart it every time.
XAML vs C#
And now let’s finally see, what our code would look like if we decided to go with pure C#, no
XAML at all. Here’s the XAML code that we’re going to rewrite in C#:
Let’s delete the TestPage completely. Right-click the XAML file, select Delete and confirm. This
will delete both the XAML file and the code-behind.
Then add a new TestPage like we did before, but this time select the .NET MAUI ContentPage
(C#) template (A). Name the file TestPage.cs (B):
26
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The file will open in the editor with some initial code already in it. Let’s modify the code to make it
the C# counterpart of the XAML code that we had before:
namespace Slugrace;
This code is more verbose than its XAML counterpart, although at this level of complexity the
difference isn’t that obvious. But when our UI gets more complex, the XAML code will be much
easier to read.
27
Look at how we set the values of the properties. We need to check their types in documentation.
For example FontSize is a property of type double, so we don’t use quotation marks in C#. If we
need to set multiple values, like with the FontAttributes enum, we use the pipe symbol (|) to
separate them.
After you’re done testing, remove the C# file and recreate the XAML version of the TestPage with
its corresponding code-behind, just like we did in the first place (without the later modifications in
the Label object except the Text attribute, which should be set to “Test Page”). Make sure
everything works as expected.
In this chapter we focused on just one view, the Label. But there are more, as we’re going to see in
the next one.
28
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In chapter 3 we were talking about the Label view and its property attributes. But there are lots of
other views that you get out of the box. In this chapter we’ll have a look at some of them, mainly
the ones that we’ll be using in our app. To just practice new stuff here, we’ll be using the TestPage
for that.
Button
Open the TestPage.xaml file and add a button below the label:
29
Buttons are used instead of labels if you want add some functionality to your app. There are a
couple events associated with them. Let’s have a look at them now.
Button Events
The events associated with a button are Clicked, Pressed and Released. Let’s implement them
in our button. As you saw in chapter 2, the counter button implements the Clicked event. First it
binds a method to the event:
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
}
Here it refers to the button by its name (CounterBtn), which the x:Name attribute is set to. We’ll be
talking about this attribute later on, but for now we’ll be referring to the button using the sender
argument.
The sender argument refers to the object that fired the event, so the button in our case. As it’s an
object, we must cast it to the Button type. So, let’s implement the Clicked event in our button
now. Here’s the XAML code in the TestPage.xaml file:
...
<Button
...
HorizontalOptions="Center"
Clicked="Button_Clicked" />
</VerticalStackLayout>
</ContentPage>
30
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
namespace Slugrace;
Now run the app and click the button. You should see the text,
border width and border color of the button change.
This change is only visible after you release the button. And now
remove the Clicked event from XAML and add the Pressed event
instead:
...
<Button
...
HorizontalOptions="Center"
Pressed="Button_Pressed" />
</VerticalStackLayout>
</ContentPage>
This time we’ll cast the sender object to Button in a different way:
namespace Slugrace;
By the way, the exclamation point in the code above is the so-called null forgiving operator. We use
it to tell the compiler that we know what we’re doing and there is no null reference. Without it, we
would get a warning.
31
This event will fire the moment you press the button, not only after
you release it. This time only the text on the button is going to
change.
Finally, let’s see how the Released event works. First, let’s add this
event in XAML:
...
<Button
...
Pressed="Button_Pressed"
Released="Button_Released" />
</VerticalStackLayout>
</ContentPage>
...
public partial class TestPage : ContentPage
{
...
private void Button_Pressed(object sender, EventArgs e)
...
private void Button_Released(object sender, EventArgs e)
{
(sender as Button)!.Text = "Click Me Again!";
}
}
Slider
The slider can be used to set a value (of type double) between two extreme values (minimum and
maximum) by dragging a thumb. You can set the minimum value, the maximum value and the
current value by respectively using the Minimum, Maximum and Value properties.
Let’s play with the Slider for a while. First let’s remove the XAML code that creates the button
and the code related to the button in the code-behind. We don’t need it anymore. Instead, let’s
create a slider:
32
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<Label
...
<Slider
Minimum="-100"
Maximum="200"
Value="0" />
</VerticalStackLayout>
</ContentPage>
As you can see, the minimum and maximum values are set to -100 and 200 respectively and the
current value is 0. So the slider thumb should sit at 0 when you run the app. Let’s try it out.
...
<Label
BindingContext="{x:Reference slider1}"
Text="{Binding Value}"
FontSize="30"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Slider
x:Name="slider1"
Minimum="-100"
Maximum="200"
Value="0" />
...
So, here we name the object slider1. Then, in the target view, which is the label, we set
BindingContext to the slider by its name and we set the label’s Text property to the Value
property of the slider. I also set the font size to make the text slightly more readable.
If you run the code now and move the slider thumb, you will see the text above it change in real
time.
33
The initial value is 0. If you drag the thumb
all the way to the left, the value will be -100.
If you drag the thumb all the way to the
right, the value will be 200. And the values
in-between are floating-point numbers (of type double).
...
<Slider
x:Name="slider1"
Minimum="-100"
Maximum="200"
Value="0"
HeightRequest="40"
BackgroundColor="LightBlue"
FlowDirection="RightToLeft"
MaximumTrackColor="Red"
MinimumTrackColor="Green"
ThumbColor="Black" />
...
Sometimes a vertical slider is the way to go. To make your slider vertical, just set its Rotation
property accordingly. Let’s simplify the first horizontal slider and add a vertical one:
...
<VerticalStackLayout>
<Label
BindingContext="{x:Reference slider2}"
Text="{Binding Value}"
...
<Slider
x:Name="slider1"
Maximum="10"
Minimum="0"
Value="7"
WidthRequest="200"
HorizontalOptions="Start" />
34
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Slider
x:Name="slider2"
Maximum="15"
Minimum="-15"
WidthRequest="200"
HorizontalOptions="End"
TranslationY="100"
Rotation="-90"
Value="0" />
</VerticalStackLayout>
</ContentPage>
Slider Events
There are a couple interesting Slider events, the most obvious one being probably ValueChanged.
It’s triggered when the value changes, which isn’t that hard to guess. Let’s see it in action. The
second slider has a range of -15 to 15. This value should be used to rotate the first slider. We could
achieve this using simple binding like before:
...
<Slider
x:Name="slider1"
BindingContext="{x:Reference slider2}"
Rotation="{Binding Value}"
Maximum="10"
...
<Slider
x:Name="slider2"
...
Try it out. If you drag the thumb of the second slider, the
first slider rotates.
35
ValueChanged event is a good solution. Have a look:
<Slider
x:Name="slider1"
Maximum="10"
Minimum="0"
Value="7"
WidthRequest="200"
HorizontalOptions="Start" />
<Slider
x:Name="slider2"
Maximum="15"
Minimum="-15"
WidthRequest="200"
HorizontalOptions="End"
TranslationY="100"
Rotation="-90"
Value="0"
ValueChanged="slider2_ValueChanged" />
</VerticalStackLayout>
</ContentPage>
...
public partial class TestPage : ContentPage
{
public TestPage()
...
private void slider2_ValueChanged(object sender, ValueChangedEventArgs e)
{
if (e.NewValue > 0)
{
label.Text = "Rotating normally";
slider1.Rotation = e.NewValue;
}
else
{
label.Text = "Rotating faster";
slider1.Rotation = e.NewValue * 5;
}
}
}
I removed the binding from the label. Instead I set its x:Name attribute so that we can reference it in
code. Now if you drag the thumb of the second slider, the first slider will be rotated and the label’s
36
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Text property will be set. The rotation will be different for the positive and negative range of the
second slider though:
Two other events are DragStarted and DragCompleted, which are fired when you respectively
start and finish dragging the slider thumb. Let’s add them to the first slider:
...
<Slider
x:Name="slider1"
Maximum="10"
Minimum="0"
Value="7"
WidthRequest="200"
HorizontalOptions="Start"
DragStarted="slider1_DragStarted"
DragCompleted="slider1_DragCompleted" />
<Slider
x:Name="slider2"
...
...
public partial class TestPage : ContentPage
{
public TestPage()
...
private void slider2_ValueChanged(object sender, ValueChangedEventArgs e)
...
private void slider1_DragStarted(object sender, EventArgs e)
{
label.Text = "Dragging started";
}
Now run the app and start dragging the first slider.
37
As soon as you stop dragging the slider thumb, the label
text will change to “Dragging Completed.” Fine, let’s
move on to the next view, the check box.
CheckBox
A CheckBox is a two-state button that can be either
checked or unchecked.
...
<VerticalStackLayout>
<Label
x:Name="label"
Text=""
FontSize="30"
VerticalOptions="Center"
HorizontalOptions="Center" />
<CheckBox />
<CheckBox IsChecked="True" />
</VerticalStackLayout>
</ContentPage>
You can now check or uncheck them by clicking on them. Try it out.
You can change the color of your check box if you want:
...
<CheckBox Color="Red"/>
<CheckBox IsChecked="True" Color="Green" />
...
CheckBox Events
As for the CheckBox events, there’s actually only one if you exclude those that are inherited from
the Element, VisualElement or BindableObject classes. It’s CheckedChanged.
38
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here’s an example where you can see how to use it. First, the XAML file:
...
<VerticalStackLayout>
<Label
...
<CheckBox
Color="Red"
CheckedChanged="CheckBox_CheckedChanged" />
<CheckBox
IsChecked="True"
Color="Green"
CheckedChanged="CheckBox_CheckedChanged" />
</VerticalStackLayout>
</ContentPage>
...
public partial class TestPage : ContentPage
{
public TestPage()
...
private void CheckBox_CheckedChanged(object sender, CheckedChangedEventArgs e)
{
string labelText = $"Color: {(sender as CheckBox)!.Color.ToHex()} - ";
if (e.Value)
{
labelText += "checked";
}
else
{
labelText += "unchecked";
}
label.Text = labelText;
}
}
39
RadioButton
A RadioButton is also a type of button, just like CheckBox, but it’s used if only one option out of
many should be selected. We also use the IsChecked property to set the value of a radio button’s
state. There are a couple other properties that we often use with radio buttons.
One of them is Content. We use it to set the string or view that is to be displayed in the radio
button.
Another important property is GroupName. All radio buttons sharing the same group name are
mutually exclusive, so only one of them can be selected at any given time. If you need more sets of
radio buttons, just use different group names for them.
Yet another property is Value. You can use it to store an optional unique value associated with the
radio button.
Let’s have a look at all these properties in action. Remove the check boxes from XAML and the
corresponding C# code from the code-behind and create the following two groups of radio buttons:
...
<VerticalStackLayout>
<Label
...
FontSize="20"
...
<VerticalStackLayout>
<Label Text="Pick your prize:" />
<RadioButton
GroupName="prizes"
Content="A houseplant"
Value="45" />
<RadioButton
GroupName="prizes"
Content="An antique chair"
Value="900" />
<RadioButton
GroupName="prizes"
Content="A pair of gloves"
Value="68" />
</VerticalStackLayout>
<VerticalStackLayout>
<Label Text="Where do you want it to be delivered?" />
<RadioButton
GroupName="delivery"
Content="New York"
IsChecked="True"
Value="25" />
<RadioButton
GroupName="delivery"
Content="Atlanta"
Value="35" />
40
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<RadioButton
GroupName="delivery"
Content="Boston"
Value="30" />
</VerticalStackLayout>
</VerticalStackLayout>
</ContentPage>
...
<Label
...
<VerticalStackLayout RadioButtonGroup.GroupName="prizes">
<Label Text="Pick your prize:" />
<RadioButton
Content="A houseplant"
Value="45" />
<RadioButton
Content="An antique chair"
Value="900" />
<RadioButton
Content="A pair of gloves"
Value="68" />
</VerticalStackLayout>
<VerticalStackLayout RadioButtonGroup.GroupName="delivery">
<Label Text="Where do you want it to be delivered?" />
<RadioButton
Content="New York"
IsChecked="True"
Value="25" />
<RadioButton
Content="Atlanta"
Value="35" />
<RadioButton
Content="Boston"
Value="30" />
</VerticalStackLayout>
</VerticalStackLayout>
</ContentPage>
41
RadioButton Events
If you want to react to a state change of a radio button, you have to implement the
CheckedChanged event. Here’s how to do it. First, the XAML code:
<Label
x:Name="label2"
FontSize="15"
VerticalOptions="Center"
HorizontalOptions="Center" />
<VerticalStackLayout RadioButtonGroup.GroupName="prizes">
<Label Text="Pick your prize:" />
<RadioButton
Content="A houseplant"
Value="45"
CheckedChanged="RadioButton_CheckedChanged" />
<RadioButton
Content="An antique chair"
Value="900"
CheckedChanged="RadioButton_CheckedChanged" />
<RadioButton
Content="A pair of gloves"
Value="68"
CheckedChanged="RadioButton_CheckedChanged" />
</VerticalStackLayout>
<VerticalStackLayout RadioButtonGroup.GroupName="delivery">
<Label Text="Where do you want it to be delivered?" />
<RadioButton
Content="New York"
IsChecked="True"
Value="25"
CheckedChanged="RadioButton_CheckedChanged_1" />
<RadioButton
Content="Atlanta"
Value="35"
CheckedChanged="RadioButton_CheckedChanged_1" />
<RadioButton
Content="Boston"
Value="30"
CheckedChanged="RadioButton_CheckedChanged_1" />
</VerticalStackLayout>
</VerticalStackLayout>
</ContentPage>
42
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
As you can see, I added another label here. And now the code-behind:
...
public partial class TestPage : ContentPage
{
public TestPage()
...
private void RadioButton_CheckedChanged(object sender, CheckedChangedEventArgs e)
{
var rb = sender as RadioButton;
string labelText = $"Your award: {rb!.Content}\nValue: ${rb.Value}\n";
label1.Text = labelText;
}
Entry
We’ll need some controls in our app to enter text. If this is to
be a single line of text, the Entry view is best suited for that.
For multiple lines of text we need an Editor, which we
discuss next.
The Entry has lots of useful properties. Let’s create an entry then and see some of them in action.
As usual, remove all the RadioButton - related code from the XAML and C# files first. In the
XAML file leave the first label only and set its x:Name attribute back to “label”. Then add an entry
like so:
...
<VerticalStackLayout>
<Label
x:Name="label"
FontSize="20"
VerticalOptions="Center"
HorizontalOptions="Center" />
43
<Entry
x:Name="entry"
Text="hey"
Placeholder="So empty here..." />
</VerticalStackLayout>
</ContentPage>
...
<Label
...
<Entry
x:Name="entry"
Text="hey"
Placeholder="So empty here..."
CharacterSpacing="10"
ClearButtonVisibility="WhileEditing"
FontAttributes="Bold"
FontSize="30"
HorizontalTextAlignment="Center"
TextColor="Coral"
PlaceholderColor="BlanchedAlmond" />
</VerticalStackLayout>
</ContentPage>
You can also use an entry for entering a password. All you need to do is set the IsPassword
property to True:
...
<Entry
...
PlaceholderColor="BlanchedAlmond"
IsPassword="True" />
...
44
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Entry Events
As with the other views before, there are quite a few events that you can use with an entry. Let’s
focus on two, TextChanged and Completed. The former is fired every time you type or delete a
character, the latter only after you hit Enter to finalize the text. Let’s implement them for our entry.
Here’s the XAML code (with the Entry view considerably simplified):
...
<Entry
FontSize="20"
TextChanged="Entry_TextChanged"
Completed="Entry_Completed" />
</VerticalStackLayout>
</ContentPage>
...
public partial class TestPage : ContentPage
{
public TestPage()
...
private void Entry_TextChanged(object sender, TextChangedEventArgs e)
{
var entry = (sender as Entry);
label.Text = $"{entry!.Text.Length} characters";
}
Now run the app and start typing something in the entry. The label text above will display the
length of your text.
45
There are two more events you might want to use with an Entry from time to time, Focused and
Unfocused. They derive from the VisualElement class. Let’s add them to our entry:
...
<Entry
TextChanged="Entry_TextChanged"
Completed="Entry_Completed"
Focused="Entry_Focused"
Unfocused="Entry_Unfocused" />
...
...
public partial class TestPage : ContentPage
{
...
private void Entry_Completed(object sender, EventArgs e)
...
private void Entry_Focused(object sender, FocusEventArgs e)
{
(sender as Entry)!.FontSize = 20;
}
Fine. But what if we need multiline text? Well, here comes the Editor view to the rescue.
Editor
The Editor is a view that allows you to enter multiple lines of text. It shares quite a lot of
properties and events with the Entry class. Let’s remove the Entry-related code from XAML and
C# and create an editor:
46
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<VerticalStackLayout>
<Label
...
<Editor
Placeholder="Type something..."
HeightRequest="100" />
</VerticalStackLayout>
</ContentPage>
You can also set the AutoSize property to accommodate your input. Let’s remove the
HeightRequest property and set the AutoSize property:
...
<Editor
Placeholder="Type something..."
AutoSize="TextChanges" />
...
There are lots of other views that we could talk about, but you can easily check them out in the
documentation if you need them. All the views we’ve covered so far are used to display a single
item, like a label, a button or a slider, to mention just a few.
But there are also views that allow you to display multiple items in a list or in another form. The
two most important ones are ListView and CollectionView. They share a lot of similarities, but
the CollectionView is preferred over the ListView in modern apps. Unlike ListView, it allows
for laying out the elements both vertically and horizontally, and also it supports single or multiple
selection. We’ll have a look at the CollectionView later in the book and in the next chapter we’ll
be talking about layouts.
47
Chapter 5 - Layouts
code: https://github.com/prospero-apps/Slugrace/tree/chapter5
In chapter 4 we were creating views like buttons, check boxes, sliders, entries and others. You
might have noticed that they all were enclosed within a VerticalStackLayout element. Open the
TestPage.xaml file. This is where we left off:
As you can see, there are two elements in the VerticalStackLayout. But what is the
VerticalStackLayout in the first place? Well, it’s one of the layouts that you can use to arrange
your views. In this particular case, the elements are stacked vertically. Let’s have a closer look at
this layout and the other layouts that you can use. We’re still going to use the TestPage because this
is all temporary code that we’re eventually not going to use in the app.
Layouts enable you to arrange the elements on the screen in almost any imaginable way. This is
because they can be nested in any way you like so that you get what you want. But working with
layouts requires from you some knowledge regarding child positioning and sizing. By children I
mean the elements that are inside the layout, so, for example, in the code above the Label and the
Editor are children of the VerticalStackLayout.
So, to begin with, let’s have a look at three closely-related layouts: StackLayout,
VerticalStackLayout and HorizontalStackLayout.
48
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<StackLayout Orientation="Horizontal" Margin="40" Spacing="10">
...
...
<ContentPage ...>
<StackLayout Orientation="Horizontal" Margin="40" Spacing="10">
<Button Text="1" FontSize="80" WidthRequest="100" HeightRequest="100" />
<Button Text="2" FontSize="80" WidthRequest="100" HeightRequest="100" />
<Button Text="3" FontSize="80" WidthRequest="100" HeightRequest="100" />
<Button Text="4" FontSize="80" WidthRequest="100" HeightRequest="100" />
<Button Text="5" FontSize="80" WidthRequest="100" HeightRequest="100" />
</StackLayout>
</ContentPage>
I also changed the font size to make the numbers even more visible. This code is pretty repetitive
and, yes, it could be done in a better, more concise way, but don’t worry about it for now.
49
Now the buttons look like here
on the right.
The buttons are vertically centered inside their parent element (which is the StackLayout). This is
the default behavior, but you can change it by setting the VerticalOptions property to a different
value, for example:
...
<StackLayout
Orientation="Horizontal"
Margin="40"
Spacing="10"
VerticalOptions="Start">
<Button Text="1" FontSize="80" WidthRequest="100" HeightRequest="100"/>
...
...
<StackLayout
Orientation="Horizontal"
Margin="40"
Spacing="10"
VerticalOptions="Start"
HorizontalOptions="Center">
<Button Text="1" FontSize="80" WidthRequest="100" HeightRequest="100"/>
...
50
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<HorizontalStackLayout
Margin="40"
Spacing="10"
VerticalOptions="Start"
HorizontalOptions="Center">
<Button Text="1" FontSize="80" WidthRequest="100" HeightRequest="100"/>
...
You should still see the same output in your app window. And now let’s have a look at another
layout, the Grid.
Grid
While the layouts we’ve covered so far arrange their children in one dimension (vertical or
horizontal), the Grid arranges them in two dimensions, in rows and columns. We use the
RowDefinitions and ColumnDefinitions properties to define the rows and columns respectively.
The rows and columns may have absolute or proportional sizes.
By default, a Grid contains one row and one column. Let’s rewrite our XAML code to use such a
basic Grid. Besides, we’re going to put a BoxView inside the grid as its only child. A BoxView is a
simple view that consists of a rectangle of a specified size and color.
...
<ContentPage ...>
<Grid Margin="40">
<BoxView HeightRequest="100" WidthRequest="100" Color="LightSeaGreen" />
</Grid>
</ContentPage>
51
Here it is, sitting in the center of
the only row and column.
So, how do we add more rows and columns? Well, we can do it like so:
...
<ContentPage ...>
<Grid Margin="40">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<BoxView
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="0"
Grid.Column="1"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Column="2"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
52
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<BoxView
Grid.Row="1"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="1"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="2"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="1"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="2"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Grid.Column="1"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Grid.Column="2"
HeightRequest="100"
WidthRequest="100"
Color="LightSeaGreen" />
</Grid>
</ContentPage>
53
As you can see in the code
above, I skipped all the
Grid.Row and Grid.Column
properties with the value of 0
because this is the default value.
What if we didn’t specify the BoxViews’ sizes? Let’s try it out. Here’s the code:
...
<Grid Margin="40">
...
<BoxView
Color="LightSeaGreen" />
<BoxView
Grid.Row="0"
Grid.Column="1"
Color="LightSeaGreen" />
<BoxView
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Color="LightSeaGreen" />
54
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<BoxView
Grid.Row="3"
Grid.Column="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Grid.Column="2"
Color="LightSeaGreen" />
</Grid>
</ContentPage>
...
<BoxView
Color="LightSeaGreen" />
<BoxView
Grid.Row="0"
Grid.Column="1"
Color="LightCyan" />
<BoxView
Grid.Column="2"
Color="LightCoral" />
<BoxView
Grid.Row="1"
Color="LightCyan" />
<BoxView
Grid.Row="1"
Grid.Column="1"
Color="LightCoral" />
<BoxView
Grid.Row="1"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Color="LightCoral" />
<BoxView
Grid.Row="2"
Grid.Column="1"
Color="LightSeaGreen" />
55
<BoxView
Grid.Row="2"
Grid.Column="2"
Color="LightCyan" />
<BoxView
Grid.Row="3"
Color="LightCyan" />
<BoxView
Grid.Row="3"
Grid.Column="1"
Color="LightCoral" />
<BoxView
Grid.Row="3"
Grid.Column="2"
Color="LightSeaGreen" />
</Grid>
</ContentPage>
Or, let’s set the colors back to original and add some spaces between the rows and columns instead.
We can do it by setting the RowSpacing and ColumnSpacing properties:
...
<Grid Margin="40" RowSpacing="10" ColumnSpacing="10">
<Grid.RowDefinitions>
...
56
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Naturally, you don’t have to put elements in each and every cell. Let’s remove some of the
children:
...
<BoxView
Color="LightSeaGreen" />
<BoxView
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Grid.Column="2"
Color="LightSeaGreen" />
</Grid>
</ContentPage>
57
Also, you can put more than one element inside each cell. Have a look:
...
<BoxView
Color="LightSeaGreen" />
<Label
Text="[0,0]"
FontSize="80"
FontAttributes="Bold"
TextColor="White"
HorizontalOptions="Center"
VerticalOptions="Center" />
<BoxView
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="1"
Color="LightSeaGreen" />
<Button
Grid.Row="1"
Grid.Column="1"
WidthRequest="200"
HeightRequest="100"
BackgroundColor="Black"
Text="Don't click"
FontSize="30" />
<BoxView
Grid.Row="1"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="2"
HeightRequest="100"
WidthRequest="100"
Color="OrangeRed"
HorizontalOptions="Start" />
58
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Label
Grid.Row="2"
Grid.Column="2"
Text="[2,2]"
FontSize="80"
FontAttributes="Bold"
TextColor="White"
HorizontalOptions="Center"
VerticalOptions="Center" />
<BoxView
Grid.Row="2"
Grid.Column="2"
HeightRequest="100"
WidthRequest="100"
Color="OrangeRed"
HorizontalOptions="End" />
<BoxView
Grid.Row="3"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Grid.Column="2"
Color="LightSeaGreen" />
</Grid>
</ContentPage>
...
<ContentPage ...>
<Grid Margin="40" RowSpacing="10" ColumnSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="3*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="200" />
59
</Grid.ColumnDefinitions>
<BoxView
Color="LightSeaGreen" />
<BoxView
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Color="LightSeaGreen" />
<BoxView
Grid.Row="1"
Grid.Column="1"
WidthRequest="240"
HeightRequest="80"
Color="Red" />
<BoxView
Grid.Row="1"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="1"
WidthRequest="150"
HeightRequest="300"
Color="Red" />
<BoxView
Grid.Row="2"
Grid.Column="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Grid.Column="2"
Color="LightSeaGreen" />
</Grid>
</ContentPage>
To make things more visible, I changed the color of the two BoxViews in the middle column and I
set their sizes. Now, let’s analyze how we defined the row heights and column widths. Here’s the
portion of the code where we did it:
...
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="3*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
60
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
...
Let’s start with the rows. We used absolute sizing for the first row. Its height is set to 50 device-
independent units, so it stays the same even if you resize the window (try it out).
The third row’s height is set to Auto. This means it autosizes based on the size of its children. The
tallest child in the third row is the red BoxView in the middle column, so the whole row adjusts to
it.
The second and fourth rows have heights set to 3* and * (which is the same as 1*) respectively. The
star notation is used for proportional sizing. It just means that the leftover height (so without taking
the rows with heights set to an absolute value or Auto into account) is allocated proportionally.
Here the second row takes three times as much of it as the fourth row. This proportion will be
retained if you resize the window as well.
Now the columns. We use absolute sizing for the third column. It just means the third column will
always be 200 device-independent units wide.
The width of the middle column is set to Auto, so it takes the width of its widest child, which is the
red BoxView in the second row.
...
<Grid
Margin="40"
RowSpacing="10"
ColumnSpacing="10"
RowDefinitions="50, 3*, Auto, *"
ColumnDefinitions="*, Auto, 200">
<BoxView
Color="LightSeaGreen" />
...
61
Much more concise, isn’t it?
Sometimes you may want an element to span multiple rows or columns. This is possible by setting
the Grid.RowSpan and Grid.ColumnSpan properties. Let’s remove some of the BoxViews to make
some room and see how spanning multiple rows and columns may be implemented:
...
<Grid
...
<BoxView
Color="LightSeaGreen"
Grid.ColumnSpan="3" />
<BoxView
Grid.Row="1"
Grid.Column="1"
WidthRequest="240"
HeightRequest="80"
Color="Red" />
<BoxView
Grid.Row="1"
Grid.Column="2"
Color="LightSeaGreen"
Grid.RowSpan="2" />
<BoxView
Grid.Row="2"
Color="LightSeaGreen" />
<BoxView
Grid.Row="2"
Grid.Column="1"
WidthRequest="150"
HeightRequest="300"
Color="Red" />
<BoxView
Grid.Row="3"
Color="LightSeaGreen" />
<BoxView
Grid.Row="3"
Grid.Column="2"
Color="LightSeaGreen" />
</Grid>
</ContentPage>
62
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
That’s basically all you have to know about the Grid layout for now. Let’s move on to the next
layout.
FlexLayout
The next layout I’d like to talk about is the FlexLayout. It’s similar to the StackLayout in that it
also arranges its children in a horizontal or vertical stack, but it also combines the functionality
available in the CSS flex display mode, if you have some CSS background.
Let’s remove the Grid and create a simple FlexLayout instead. We use the Direction property to
define how the children are to be arranged. It can be set to Row, RowReverse, Column or
ColumnReverse.
...
<ContentPage ...>
<FlexLayout>
<Button
Text="1"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
<Button
Text="2"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
<Button
Text="3"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
<Button
Text="4"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
<Button
Text="5"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
63
<Button
Text="6"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
<Button
Text="7"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
<Button
Text="8"
FontSize="100"
Margin="2"
WidthRequest="150"
HeightRequest="150" />
</FlexLayout>
</ContentPage>
...
<FlexLayout Direction="RowReverse">
<Button
...
64
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<FlexLayout Direction="Column">
<Button
...
or ColumnReverse:
...
<FlexLayout Direction="ColumnReverse">
<Button
...
Yes, I know, now the elements don’t fit in the column. This is where the Wrap property comes in
really handy. If set to Wrap, the elements that don’t fit in a single row or column are wrapped. Have
a look:
...
<FlexLayout Direction="Column" Wrap="Wrap" >
<Button
...
65
Here I set Direction back to
Column. Now all elements are
displayed on the screen.
You can also set Wrap to Reverse if you want the children to wrap in the other direction:
...
<FlexLayout Direction="Column" Wrap="Reverse" >
<Button
...
What matters is, as you can see, where the elements actually start. This rule applies to all values of
JustifyContent. Anyway, as Start is the default value, let’s try End:
66
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<FlexLayout Direction="Row" JustifyContent="End">
<Button
Text="1"
...
<Button
Text="2"
...
<Button
Text="3"
...
<Button
Text="4"
...
<Button
Text="5"
...
</FlexLayout>
</ContentPage>
Here are the other values you can set JustifyContent to (provided Direction is set to Row):
Center
SpaceBetween
SpaceAround
SpaceEvenly
JustifyContent works for the main axis the elements are arranged in. Here it’s the horizontal
axis, but if Direction were set to Column or ColumnReverse, it would be the vertical axis.
For the other axis we use the AlignItems property. So, if Direction is set to Row like here, this
property will be used to align the items vertically. The possible values are: Start, End, Center and
Stretch. Let’s center the buttons both horizontally (with JustifyContent) and vertically (with
AlignItems):
67
...
<FlexLayout
Direction="Row"
JustifyContent="Center"
AlignItems="Center">
<Button
...
AbsoluteLayout
In an AbsoluteLayout you position and size elements using explicit values or values relative to the
layout’s position and size. The position is specified by the top-left corner of the child relative to the
top-left corner of the parent (which is the AbsoluteLayout).
Let’s remove the FlexLayout with the buttons and create an AbsoluteLayout with BoxViews.
Let’s also set the background color of the AbsoluteLayout so that we can see it better. We use the
AbsoluteLayout.LayoutBounds property to position and size the children. The value of this
property consists of four numbers in a string. The first two numbers determine the position, the
other two are for sizing. Have a look:
...
<ContentPage ...>
<AbsoluteLayout Margin="40" BackgroundColor="Beige">
<BoxView
Color="Green"
AbsoluteLayout.LayoutBounds="100, 60, 100, 50" />
<BoxView
Color="Red"
AbsoluteLayout.LayoutBounds="400, 380, 200, 100" />
<BoxView
Color="Blue"
AbsoluteLayout.LayoutBounds="750, 30, 400, 400" />
</AbsoluteLayout>
</ContentPage>
Here we have three BoxViews. The first one is green. It’s positioned 100 units from the left (which
means its top-left corner is 100 units from the left), 60 units from the top and its size is 100 by 50
68
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<AbsoluteLayout Margin="40" BackgroundColor="Beige">
<BoxView
Color="Green"
AbsoluteLayout.LayoutBounds=".5, .5, 200, 200"
AbsoluteLayout.LayoutFlags="PositionProportional" />
<BoxView
Color="Red"
AbsoluteLayout.LayoutBounds="100, 400, .2, .1"
AbsoluteLayout.LayoutFlags="SizeProportional" />
<BoxView
Color="Blue"
AbsoluteLayout.LayoutBounds="1, 1, .25, .5"
AbsoluteLayout.LayoutFlags="PositionProportional,SizeProportional" />
</AbsoluteLayout>
</ContentPage>
Green proportional (.5, .5) - centered absolute (200, 200) - 200 x 200 units
Red absolute (100, 400) - 100 units from the proportional (.2, .1) - 20% of the width
left and 400 units from the top of the AbsoluteLayout and 10% of its
height
Blue proportional (1, 1) - all the way to the proportional (.25, .5) - a quarter of the
right and all the way to the bottom layout’s width and half its height
69
And this is how they look.
Nested Layouts
Layouts can be nested. The example we’re going to build isn’t particularly useful. It’s just to
demonstrate how layouts can be nested. The code is pretty lengthy, but it should be pretty easy to
read and understand:
...
<ContentPage ...>
<FlexLayout
BackgroundColor="Beige"
Direction="Column">
<HorizontalStackLayout
BackgroundColor="Black"
Spacing="50"
Padding="10">
<Label
Text="File"
TextColor="White"
FontSize="20"
FontAttributes="Bold" />
<Label
Text="Edit"
TextColor="White"
FontSize="20"
FontAttributes="Bold" />
<Label
Text="View"
TextColor="White"
FontSize="20"
FontAttributes="Bold" />
<Label
Text="Help"
TextColor="White"
FontSize="20"
FontAttributes="Bold" />
</HorizontalStackLayout>
70
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid
Margin="40"
Padding="10"
BackgroundColor="Bisque"
RowDefinitions="100, *, 100"
ColumnDefinitions="100, *, 100"
RowSpacing="10"
ColumnSpacing="10">
<Label
Text="Start"
FontSize="30"
TextColor="Firebrick" />
<AbsoluteLayout
Grid.Column="1"
BackgroundColor="Beige">
<Button
Text="1"
FontSize="40"
AbsoluteLayout.LayoutBounds=".1, .5, .3, .5"
AbsoluteLayout.LayoutFlags="PositionProportional, SizeProportional" />
<Button
Text="2"
FontSize="40"
AbsoluteLayout.LayoutBounds=".9, .5, .3, .5"
AbsoluteLayout.LayoutFlags="PositionProportional, SizeProportional" />
</AbsoluteLayout>
<BoxView
Grid.Column="2"
Grid.RowSpan="3"
Color="Firebrick" />
<BoxView
Grid.Row="1"
Grid.Column="0"
Grid.RowSpan="2"
Grid.ColumnSpan="2"
Color="Firebrick" />
</Grid>
</FlexLayout>
</ContentPage>
71
Chapter 6 - Properties in XAML
code: https://github.com/prospero-apps/Slugrace/tree/chapter6
Throughout the book we’ve been using properties a lot, practically all XAML objects had one or
more properties set. In this chapter we’ll have a look at properties in .NET MAUI in more detail. In
particular, we’ll clarify terms like property elements, content properties and attached properties.
But first, let’s recap on what we already know about properties.
Property Attributes
Most of the XAML objects so far had some properties set. Here’s a Label with a couple properties:
<Label
Text="File"
TextColor="Firebrick"
FontSize="20" />
Text, TextColor and FontSize are so-called property attributes, because these are attributes that
we use to set the properties of an object. So here we have a Label object with three properties set.
This is how we’ve been using properties in XAML so far and this is how we usually use them if
they are set to a simple value like a string or integer. If we must set them to a more complex value,
like another object for example, this syntax isn’t very useful. Instead we should use so-called
property elements. Actually, we can use property elements as an alternative to property attributes
even for simple types, which will make the code a little more verbose, but it’s up to you which
syntax you choose. Let’s have a look at property elements then.
Property Elements
As I just said, property elements can be used instead of property attributes even if we want to set
them to simple values. Let’s change all the property attributes in the example above to property
elements:
<Label>
<Label.Text>
File
</Label.Text>
<Label.TextColor>
Firebrick
</Label.TextColor>
<Label.FontSize>
20
</Label.FontSize>
</Label>
72
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
So, the value of each property is placed between the property element’s opening and closing tags
and it constitutes its content.
<Label
Text="File"
TextColor="Firebrick">
<Label.FontSize>
20
</Label.FontSize>
</Label>
Generally, you will usually use property attributes for simple values like that. But sometimes it’s
not so easy. This is the case if the value of the property is too complex to be written as a string. You
saw an example in the previous chapter where we were discussing the Grid layout. The Grid has
the RowDefinitions and ColumnDefinitions properties. These are collections of RowDefinition
and ColumnDefinition objects respectively. Here’s how we use the property element syntax to set
these two properties:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="3*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
</Grid>
You may also remember that in this particular case we can also use property attributes to simplify
the code:
<Grid
RowDefinitions="50, 3*, Auto, *"
ColumnDefinitions="*, Auto, 200">>
</Grid>
But it’s not always possible. We’ll be using property elements a lot throughout this book.
Attached Properties
Our next topic is attached properties. I’ve used this term several times so far. These are properties
that are defined in one class and attached to objects of another class. Have a look at the following
73
definition of a Grid and its children, similar to the one from the previous chapter, here abbreviated
to include just the attached properties:
<Grid>
...
<BoxView
Grid.Row="1"
Grid.Column="1" />
<BoxView
Grid.Row="1"
Grid.Column="2"
Grid.RowSpan="2"
Grid.ColumnSpan="3" />
</Grid>
Here, Grid.Row, Grid.Column, Grid.RowSpan and Grid.ColumnSpan are properties of the Grid
class, but they are set on the children of the Grid.
You saw another example of attached properties when we were talking about the
AbsoluteLayout. Have a look at the code again:
...
<AbsoluteLayout Margin="40" BackgroundColor="Beige">
<BoxView
Color="Green"
AbsoluteLayout.LayoutBounds=".5, .5, 200, 200"
AbsoluteLayout.LayoutFlags="PositionProportional"/>
<BoxView
Color="Red"
AbsoluteLayout.LayoutBounds="100, 400, .2, .1"
AbsoluteLayout.LayoutFlags="SizeProportional" />
<BoxView
Color="Blue"
AbsoluteLayout.LayoutBounds="1, 1, .25, .5"
AbsoluteLayout.LayoutFlags="PositionProportional,SizeProportional" />
</AbsoluteLayout>
</ContentPage>
Can you spot the attached properties right away? I’m sure you can.
Content Properties
Finally, there’s one more term worth mentioning as far as properties in XAML are concerned,
content properties.
74
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
We could use the property element syntax as well. Let’s use it for the Text property:
...
<ContentPage ...>
<VerticalStackLayout>
<Label
TextColor="Firebrick"
FontSize="100">
<Label.Text>
Hey there
</Label.Text>
</Label>
</VerticalStackLayout>
</ContentPage>
75
So, we could place the string “Hey there” directly between the Label tags:
...
<VerticalStackLayout>
<Label
TextColor="Firebrick"
FontSize="100">
Hey there
</Label>
</VerticalStackLayout>
</ContentPage>
[ContentProperty("Text")]
public class Label : View, IFontElement, ITextElement, ...
As you can see, the class is decorated with the ContentProperty attribute with the name of the
content property passed to it.
Actually, there are more objects defined in our code snippet above. There’s also ContentPage and
VerticalStackLayout. What are their content properties? Let’s check it out. Right-click on the
ContentPage object and select Go To Definition. As you can see, Content is the content property:
[ContentProperty("Content")]
public class ContentPage : TemplatedPage, IContentView, ...
And you can’t see the Content property in the XAML code. This is exactly because it’s the content
property, which can be omitted. If so, anything between the opening and closing tags of
ContentPage is its content. But we could still use it, if we wanted to, like so:
...
<ContentPage ...>
<ContentPage.Content>
<VerticalStackLayout>
<Label
TextColor="Firebrick"
FontSize="100">
Hey there
</Label>
</VerticalStackLayout>
</ContentPage.Content>
</ContentPage>
76
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
[ContentProperty(nameof(Children))]
public class VerticalStackLayout : StackBase
...
So, let’s add the content property explicitly to the code as well:
...
<ContentPage.Content>
<VerticalStackLayout>
<VerticalStackLayout.Children>
<Label
TextColor="Firebrick"
FontSize="100">
Hey there
</Label>
</VerticalStackLayout.Children>
</VerticalStackLayout>
</ContentPage.Content>
</ContentPage>
And remember, there’s only one content property for each class.
Good, I think some of the XAML terminology has been clarified, so let’s take a break from XAML
for a while. As you know, you can write all your code in C#. In the next chapter we’ll see how to
create objects and properties in C#.
77
Chapter 7 - Objects and Properties in C#
Code
code: https://github.com/prospero-apps/Slugrace/tree/chapter7
So far in the book we’ve been creating objects and properties mostly in XAML. But sometimes you
may have to create an object in C# code. Or maybe you don’t want to use XAML at all… This is also
possible. In this chapter we’ll be creating objects, like views and layouts, in C# and we’ll be setting
their properties in C#.
Creating objects in C# requires from you that you know what type the properties are. You can
figure it out from documentation. To make things clearer, we’ll be comparing XAML code with C#
code so that you can immediately see what corresponds to what. Actually, we’ll still be using some
XAML here, because we can easily mix and match. So, open your TestPage.xaml and
TestPage.xaml.cs pages and get ready.
Creating Views in C#
To start with, let’s rewrite the TestPage.xaml file to look like so:
And now let’s remove the label from the XAML file (still leaving
the VerticalStackLayout in place):
78
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentPage ...>
<VerticalStackLayout x:Name="layout" />
</ContentPage>
Now we’ll create it in C#. We’ll also add it to the VerticalStackLayout in C# code. So, open your
TestPage.xaml.cs file and make sure it looks like this:
namespace Slugrace;
layout.Add(label);
}
}
If you run the app now, it will look the same. And now let’s remove the label-related code and
create a button with a click event. First, let’s do it in XAML:
...
<ContentPage ...>
<VerticalStackLayout x:Name="layout">
<Button
Text="Click"
FontSize="24"
WidthRequest="200"
Margin="50"
BorderColor="Black"
BorderWidth="5"
BackgroundColor="DarkGoldenrod"
TextTransform="Uppercase"
FontAttributes="Bold"
CornerRadius="50"
Clicked="Button_Clicked" />
</VerticalStackLayout>
</ContentPage>
79
There’s a Clicked event that we implement like this in the code-behind:
If you click the button, the text on it will change. So, here you
can see some properties and an event. Let’s remove the button
from the XAML file, this time along with the VerticalStackLayout, and we’ll recreate both the
layout and the view in C#. The XAML file should look like so:
namespace Slugrace;
// create layout
VerticalStackLayout layout = new();
// create view
Button button = new()
{
Text = "Click",
FontSize = 24,
WidthRequest = 200,
Margin = 50,
BorderColor = Colors.Black,
BorderWidth = 5,
BackgroundColor = Colors.DarkGoldenrod,
TextTransform = TextTransform.Uppercase,
FontAttributes = FontAttributes.Bold,
CornerRadius = 50
};
80
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
// add event
button.Clicked += Button_Clicked!;
So far we’ve been creating simple objects and adding them to the layout. We also created a layout
in C# code and set it as the page’s content. Next, let’s create a more complex layout with some
children that use attached properties and see how to recreate it in C# code.
...
<ContentPage ...>
<Grid
Margin="40"
BackgroundColor="OldLace"
RowSpacing="10"
ColumnSpacing="10"
RowDefinitions="100, *"
ColumnDefinitions="*, 2*" >
<BoxView Color="Red" />
<BoxView Grid.Column="1" Color="Green" />
<BoxView
Grid.Row="1"
Grid.ColumnSpan="2"
Color="Blue" />
</Grid>
</ContentPage>
Next, remove the Grid from the XAML file and define it in C#
code. The XAML file should now contain just the ContentPage.
81
Watch how the Grid.Row, Grid.Column and Grid.ColumnSpan attached properties are set in C#.
Also watch how the rows and columns are defined:
namespace Slugrace;
// create grid
Grid grid = new()
{
Margin = 40,
BackgroundColor = Colors.OldLace,
RowSpacing = 10,
ColumnSpacing = 10,
RowDefinitions =
{
new RowDefinition { Height = new GridLength(100) },
new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }
},
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(2, GridUnitType.Star) }
}
};
// Row 0 and Column 0 are the default values, so we can leave them out.
grid.Add(new BoxView
{
Color = Colors.Red
});
// We can pass the column (1) and the row (0) as arguments to the Add method.
grid.Add(new BoxView
{
Color = Colors.Green
}, 1, 0);
82
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
If you work with other attached properties, just check out in the documentation how to use them in
code.
Naturally, there’s much more to XAML. What we’ve covered so far is just a tiny fraction of what
there is, but we are now ready to design the pages for our Slugrace project, so let’s leverage the
knowledge we just gained to do right that. And no worries… We’ll be learning lots of new XAML
concepts on the way.
83
Chapter 8 - Content Pages and Content
Views
code: https://github.com/prospero-apps/Slugrace/tree/chapter8
In this chapter we’ll create the pages that we need for the Slugrace project. We’ll also create some
content views, which are sort of reusable components. Let’s start by explaining the difference
between content pages and content views.
ContentPage vs ContentView
The ContentPage is the most common page type in .NET MAUI apps. It displays a single child,
which usually is a layout with all sorts of controls as its children. In this chapter we’re going to
create the first rough versions of the pages that will be included in the app. These pages won’t be
stylized for now, nor will they have any functionality for the time being. Styling and data binding
will be discussed a bit later in the book. In particular, we’re going to create the following pages in
this chapter:
- SettingsPage
- RacePage
- GameOverPage
We’re not going to create the InstructionsPage for now because we need screenshots of the app
there, which we will only have when the remaining pages are fully styled and functional.
As we haven’t implemented page navigation yet, for the time being we’ll be just setting
ContentTemplate to each page one by one in the AppShell.xaml file. At this moment it’s set to the
TestPage:
84
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<ShellContent
Title="Test"
ContentTemplate="{DataTemplate local:TestPage}"
Route="TestPage" />
</Shell>
This is why we see the TestPage each time we run the app. This solution is temporary and will be
changed as soon as we cover page navigation.
In the SettingsPage and in the RacePage some elements will be repeated (but with different data
bound to them). In the RacePage we’ll also have two elements (the Bets panel and the Results
panel) that will never be displayed at the same time. Instead, they will swap after each race. We’ll
implement these repeated and swappable elements as ContentViews.
A ContentView is a control that enables the creation of custom reusable controls. Well, there’s a lot
of work to do, so let’s get started.
SettingsPage
Eventually, we’re going to implement the MVVM pattern in our app, so all our pages will be saved
in the Views folder. But we don’t have the folder yet, so go ahead and create it in the root of your
app (A). Then right-click it and select Add -> New Item… In the window that pops up select .NET
MAUI on the left (B) - it should be there because you already used it before, but if it isn’t, just use
the search box on the right. Then select >NET MAUI ContentPage (XAML) (C), set the name of the
page to SettingsPage.xaml (D) and hit Add (E).
85
The new page, like any new page you add to the project, contains some initial code:
<ShellContent
Title="Test"
ContentTemplate="{DataTemplate views:SettingsPage}"
Route="SettingsPage" />
</Shell>
So, first of all, we added a new namespace to refer to the Views folder where the page sits. We’re
going to discuss namespaces in XAML in much more detail later on.
Then we changed the DataTemplate to actually use the new page when the app starts. Changing
the Route isn’t strictly necessary for now for the app to work, but let’s do it for consistency’s sake.
Run the app in Windows. We’ll be testing all the pages in Windows now and later we’ll handle the
Android versions. As you might expect, there are going to be some differences that we have to take
into account. You’ll see the SettingsPage as it looks now:
86
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
This page consists of several parts. These parts are visually separated from one another. They
include:
87
- the Ending Conditions panel,
The first thing we can do is remove the title bar near the top of the page. We won’t need it. To do
that, we have to set the Title property of the ContentPage to an empty string or remove it
completely:
Now, let’s think for a while: How are we going to implement this page? There are multiple ways to
do it, but I’m going to implement it like so:
- The Content property of the ContentPage will be set to a VerticalStackLayout (Content is the
content property of ContentPage, so you don’t have to set it explicitly), which will contain the four
main parts of the page.
- The Players panel will contain a Border, which is a control that allows you to group other
elements and surround them with an actual border. The properties of the Border are self-
explanatory, maybe except StrokeShape. This property is set to a shape (a rounded rectangle in
this case) with the four corner radii defined (here all four are set to 10). Inside the Border we’ll add
a VerticalStackLayout that will contain the label with the text "The Players", a
HorizontalStackLayout with the four radio buttons that belong to the players group, a Grid
with the headers (for Name and Initial Money) for the players settings and, temporarily, another
Grid with a BoxView and Label inside. This BoxView will serve as a placeholder for now. Later
we’ll replace it with reusable controls that we must first create. Each of these reusable controls will
contain controls related to a particular player.
88
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
- For the Ending Conditions panel, we’ll use another Border control. Inside the Border we’ll create
a VerticalStackLayout with a Label and a Grid. In the Grid we’ll put the ending conditions
radio buttons and entries.
<Grid
RowDefinitions="*"
ColumnDefinitions="100, 3*, 2*">
<Label
Grid.Column="1"
Text="Name (max 10 characters)" />
<Label
Grid.Column="2"
Text="Initial Money ($10 - $5000)" />
</Grid>
<Grid>
<BoxView
Color="Beige"
HorizontalOptions="FillAndExpand"
HeightRequest="300" />
89
<Label
Text="Player Settings"
FontSize="50"
FontAttributes="Italic"
HorizontalOptions="Center"
VerticalOptions="Center"/>
</Grid>
</VerticalStackLayout>
</Border>
90
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Let’s run the app now. You should see something more similar to what we are aiming at:
And now let’s create the PlayerSettings content view that we can then use instead of the
placeholder BoxView.
If you open the code-behind files of the SettingsPage and PlayerSettings classes, you will
notice that the former inherits from ContentPage, whereas the latter from ContentView.
Now, what do we need in the view? Have a look at it one more time:
Well, there are: a label, an entry, another label and another entry. Let’s put these elements in a one-
row Grid to make it easier to align with the headers. Make sure your PlayerSettings.xaml file
contains the following code:
91
<?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"
x:Class="Slugrace.Controls.PlayerSettings">
<Grid
RowDefinitions="*"
ColumnDefinitions="100, 3*, 10, 2*">
<Label
Text="Player Name" />
<Entry
Grid.Column="1" />
<Label
Grid.Column="2"
Text="$" />
<Entry
Grid.Column="3" />
</Grid>
</ContentView>
Now we have to put four instances of this control in the SettingsPage. This number will then vary
depending on how many players are supposed to play (which you will set by checking a radio
button).
This requires two steps on our part: First, we have to add the namespace to the namespaces section
of the page and then we have to add the controls just like any ordinary controls. One difference is
that we have to prefix the controls with the namespace prefix defined in the namespaces section,
which is controls (we defined it like so: xmlns:controls - this will become clearer when we talk
about namespaces). We’ll also remove the placeholder code that consists of the Grid with the
BoxView and Label. Here’s the code in the SettingsPage.xaml file:
92
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<controls:PlayerSettings />
</VerticalStackLayout>
</VerticalStackLayout>
</Border>
Sure, it needs styling, but all the main elements are there.
RacePage
Now it’s time to create the RacePage, which is the most important page of the app. You will spend
most of the time right there. This is where the actual game will be played. So, add a RacePage.xaml
file to the Views folder and, in order to be able to display this page when you run your app, modify
the code in AppShell.xaml to look like so:
93
Well, what is this page going to look like? This page is going to be more dynamic than the
SettingsPage. We’ll have animations here, the data displayed will be constantly changing as we
proceed and also the lower part of the page will be different before a race begins and after it
finishes.
We can distinguish some sections here. There are four smaller sections at the top:
- Game Info,
- Slugs’ Stats,
- Players’ Stats,
- the buttons
Let’s have a closer look at them before we move on to discuss the other sections:
- Game Info - here you will see the information about the game in general, so current race number,
number of races that are finished and races that are still to go, time elapsed and so on. What is
displayed here will depend on what ending condition you set in the SettingsPage.
So, if you selected the first ending condition (the game is over when there is only one player with
any money left), you will see just the current race number in this panel.
94
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
If you selected the third ending condition (the game is over not
later than after the racing time you set has elapsed), you will
see something like this:
We will implement the version for the second ending condition now, and later in the book we’ll
implement a mechanism for displaying the correct data.
As you can see, here we have four lines with the same type of data, so we’ll create a content view
for a single row (which corresponds to a single slug).
- Players’ Stats - here you will see statistics related to the players. Each player bets an amount of
money on a slug before each race. If their slug wins, they win
money. If their slug doesn’t win, they lose money. In this
panel the current amount of money each player has will be
displayed. Depending on which option you selected in the
SettingsPage, there may be one, two, three or four players in
the game. Here’s what it looks like if there are three:
Again, we’ll create a content view for a single row representing a single
player.
- the buttons - there will be three buttons in the top-right corner of the app
window (in the Windows version).
The first one will enable you to end the game any time you want, without waiting for the ending
condition to be fulfilled. The second button will be used for navigation to the InstructionsPage.
The third button will be used to mute/unmute the sounds. These buttons will be enclosed in a
VerticalStackLayout.
95
Next, in the central part of the app window, you will see the racetrack with the four slugs on it
waiting for the race to begin. There will also be some information about each slug on the track.
Some of this information will be the same as in the Slugs’ Stats panel (name, number of wins), but
additionally the odds (recalculated after each race) will be displayed. The higher the odds, the less
probable the slug is to win.
In order to implement this part of the UI, we’ll have to add the graphical game assets to the game.
Also, we’ll create a content view for a single slug because it’s not just a slug image as you might
think. Each slug will consist of three parts: the body and the two tentacles. The tentacles will be
animated and will change their position and rotation with respect to the body, so they have to be
separate parts. Besides, we’ll create a content view for the information that is displayed for each
slug on the racetrack.
Finally, let’s have a look at the bottom part of the page. It’s the Bets panel:
Here the players will place their bets before each race. The amount of money a player wants to bet
may be typed in in an entry or set using a slider. Then the player must select the slug they want to
place their bet on. As you can see, there’s a lot of repetitive stuff here, so we’ll create another
content view for a single player.
There’s also the Go button that will be used to start the race.
96
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Now, after the race is over, almost all the parts of the page will change.
The Game Info, Slugs’ Stats and Players’ Stats will be updated.
In the middle part of the page, the slugs will be near the end part of the racetrack and the image of
the slug that won will be displayed.
In the bottom part of the page the Bets panel will be replaced by the Results panel. The Results
panel will display information on how each player did in the last race. We’ll create another content
view for that. There will also be a Next Race button that will reset the page to display the Bets panel
again and change the racetrack part of the UI.
As the Bets and Results panels will be swapped after and before each race, we’ll implement them as
content views as well.
OK, this looks like a lot of work. So, let’s first create an outline of the page with all the basic parts of
the UI replaced by placeholders. Then we’ll start implementing each part one by one. Make sure
your code in the RacePage.xaml file looks like so:
<!--Game Info-->
<Border
97
Stroke="brown"
StrokeShape="RoundRectangle 10, 10, 10, 10"
StrokeThickness="5"
Padding="5">
<Label
Text="Game Info"
FontSize="36"
FontAttributes="Italic"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Border>
<!--Slugs' Stats-->
<Border
Grid.Column="1"
Stroke="brown"
StrokeShape="RoundRectangle 10, 10, 10, 10"
StrokeThickness="5"
Padding="5">
<Label
Text="Slugs' Stats"
FontSize="36"
FontAttributes="Italic"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Border>
<!--Players' Stats-->
<Border
Grid.Column="2"
Stroke="brown"
StrokeShape="RoundRectangle 10, 10, 10, 10"
StrokeThickness="5"
Padding="5">
<Label
Text="Players' Stats"
FontSize="36"
FontAttributes="Italic"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Border>
<!--the buttons-->
<VerticalStackLayout
Grid.Column="3"
Padding="5"
Spacing="3">
<Button
Text="End Game" />
<Button
Text="Instructions" />
<Button
Text="Sound" />
</VerticalStackLayout>
98
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<!--Racetrack-->
<Border
Grid.Row="1"
Grid.ColumnSpan="4"
Stroke="brown"
StrokeShape="RoundRectangle 10, 10, 10, 10"
StrokeThickness="5"
Padding="5">
<Label
Text="Racetrack"
FontSize="36"
FontAttributes="Italic"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Border>
<!--Bets/Results panel-->
<Border
Grid.Row="2"
Grid.ColumnSpan="4"
Stroke="brown"
StrokeShape="RoundRectangle 10, 10, 10, 10"
StrokeThickness="5"
Padding="5">
<Label
Text="Bets / Results"
FontSize="36"
FontAttributes="Italic"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Border>
</Grid>
</ContentPage>
99
As you can see, we had to write quite a lot of XAML code to obtain just the placeholders. If we now
implement the particular UI areas in full, the code will become really lengthy. We could do that,
but to keep things clean and tidy, let’s implement each part as a separate content view. Then we’ll
put just the content views in the page. So, we’re going to implement the following content views:
- GameInfo,
- SlugsStats,
- PlayersStats,
- Racetrack,
- Bets
- Results
The buttons will be added directly to the page. Let’s implement the content views one by one. We’ll
also create other content views on the way that will be nested in them. So, let’s start with the
GameInfo control.
100
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Label
Grid.Row="2"
Text="Number of races set:" />
<Label
Grid.Row="2"
Grid.Column="1"
Text="20" />
<Label
Grid.Row="3"
Text="Races finished:" />
<Label
Grid.Row="3"
Grid.Column="1"
Text="2" />
<Label
Grid.Row="4"
Text="Races to go:" />
<Label
Grid.Row="4"
Grid.Column="1"
Text="18" />
</Grid>
</ContentView>
As you can see, it’s just a simple Grid with labels displaying some dummy data. Let’s add this
view to the RacePage. Don’t forget to add the namespace in the top section of the page. Here’s the
code:
<!--Slugs' Stats-->
...
That’s it. Run the app and watch the Game Info panel.
101
Let’s take care of the Slugs‘ Stats panel next.
To add this control to the SlugsStats view, we have to add its namespace in the latter. Here’s the
implementation of the SlugsStats content view:
102
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<controls:SlugStats
Grid.Row="3" />
<controls:SlugStats
Grid.Row="4" />
</Grid>
</ContentView>
...
<ContentPage ...>
...
<!--Slugs' Stats-->
<Border
...>
<controls:SlugsStats />
</Border>
<!--Players' Stats-->
...
Run the app and watch the control (with dummy data for
now) in action.
103
Next, let’s implement the PlayersStats like this:
...
<ContentPage ...>
...
<!--Players' Stats-->
<Border
...>
<controls:PlayersStats />
</Border>
<!--the buttons-->
...
We’re done with the upper part of the UI. Now let’s take care of the part in the middle, which is the
racetrack. In order to implement the racetrack, we’ll need the graphical assets representing the
slugs in top view, their tentacles in top view, their silhouettes and the image of the track. Let’s add
the assets to the app first.
104
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Next, we’ll need the top-view images of the slugs, separately their bodies and tentacles. These
images look like so:
Finally, we’ll need the silhouette images of the slugs. They will be displayed in the WinnerInfo
panel (that we are about to create) after each race. These images look like this:
105
You will find the images in the Github repository for the project.
Download them from there and add to the Images folder inside
the Resources folder.
- SlugImage,
- TrackImage,
- SlugInfo,
- WinnerInfo.
Let’s create the SlugImage view first. This is a purely graphical view. Here’s the code in the
SlugImage.xaml file:
106
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The view contains an AbsoluteLayout with three children, which are images that together form
the slug character. This type of layout enables us to position and size the elements relative to their
parent. This is important because we don’t want the particular parts of the image to fall apart when
the size of the app window changes. We’re using proportional positioning and sizing here.
One thing that may be new to you in the code above is the AnchorX property in the
Image class. AnchorX and AnchorY are used to set the center of a transformation. In
this case the eye images are rotated -30 and 30 degrees. If we don’t set the AnchorX
and AnchorY properties, the default values will be used, which means the image
will be rotated around its central point.
This slug doesn’t look healthy. The AnchorX and AnchorY properties can be set
to values between 0 and 1. If we set AnchorX to 1, the image would be rotated
around the point which is the farthest to the right horizontally and in the center
vertically.
And the slug would look even less healthy than before.
We want the tentacles with the eyes to be rotated around the central point of the
tentacle’s base, which is on the far left of the image.
107
Next, let’s implement the SlugInfo view. This view will be used inside the TrackImage view
along with the SlugImage views. The SlugInfo view will contain information about a particular
slug. We’ll implement it as a Grid. The Text properties of the labels will be hardcoded for now.
Here’s the code:
With the SlugImage and SlugInfo views in place, we can now create the TrackImage content
view. This view will contain the image of the track and on it will be the images of the slugs along
with the information enclosed in the SlugInfo view. Here’s the code:
108
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<!--Speedster-->
<controls:SlugImage
AbsoluteLayout.LayoutBounds=".1, .05, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
AbsoluteLayout.LayoutBounds="0, 0, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
<!--Trusty-->
<controls:SlugImage
AbsoluteLayout.LayoutBounds=".1, .35, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
AbsoluteLayout.LayoutBounds="0, .35, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
<!--Iffy-->
<controls:SlugImage
AbsoluteLayout.LayoutBounds=".1, .65, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
AbsoluteLayout.LayoutBounds="0, .68, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
<!--Slowpoke-->
<controls:SlugImage
AbsoluteLayout.LayoutBounds=".1, .95, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
AbsoluteLayout.LayoutBounds="0, 1.02, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
</AbsoluteLayout>
</ContentView>
Next, let’s implement the WinnerInfo view. It’s going to be a Grid with two labels and an image.
The labels will inform us which slug has won a race and we will see the silhouette of this slug in
the image. Here’s the code:
109
<Label
Grid.Row="1"
Text="Speedster"
FontSize="40"
HorizontalOptions="Center"
FontAttributes="Bold" />
<Image
Grid.Row="2"
Source="speedster.png" />
</Grid>
</ContentView>
Now we have all the building blocks in place. Let’s create the Racetrack view, which consists of
the TrackImage on the left and the WinnerInfo on the right:
Finally, we can place the whole big and complex Racetrack content view in the RacePage:
...
<ContentPage ...>
...
<!--Racetrack-->
<Border
...>
<controls:Racetrack />
</Border>
<!--Bets/Results panel-->
...
110
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
If you now run the app, you should see something like this:
Not bad at all. Last but not least, let’s implement the Bets and Results views.
111
<Label
Grid.Column="2"
Text="$" />
<Entry
Grid.Column="3" />
<Slider
Grid.Column="4"/>
<Label
Grid.Column="5"
Text="on" />
<RadioButton
Grid.Column="6"
Content="Speedster"
GroupName="player1" />
<RadioButton
Grid.Column="7"
Content="Trusty"
GroupName="player1" />
<RadioButton
Grid.Column="8"
Content="Iffy"
GroupName="player1" />
<RadioButton
Grid.Column="9"
Content="Slowpoke"
GroupName="player1" />
</Grid>
</ContentView>
Now let’s put four instances of this control inside the Bets view. Here’s the Bets.xaml file:
<Button
Grid.Row="2"
Text="Go"
HorizontalOptions="Center" />
</Grid>
</ContentView>
112
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
After each race, the Bets view will be replaced by the Results view. So, let’s create it next.
113
<Label
Grid.Column="4"
Text="lost $250,"/>
<Label
Grid.Column="5"
Text="now has $750. " />
<Label
Grid.Column="6"
Text="The odds were 1.64." />
</Grid>
</ContentView>
Instances of this control will be embedded in the Results view. Here’s the Results.xaml file:
<Button
Grid.Row="2"
Text="Next Race"
HorizontalOptions="Center" />
</Grid>
</ContentView>
And now replace the Bets view in the RacePage by the Results view to see what it looks like:
...
<ContentPage ...>
...
<!--Bets/Results panel-->
<Border
...>
<controls:Results />
</Border>
</Grid>
</ContentPage>
114
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Run the app and you’ll see the Results panel now:
Looks like we’re done with the RacePage. There’s one more page to implement for now, the
GameOverPage. This one is going to be pretty easy. Let’s implement it with some dummy data.
GameOverPage
We have to create the GameOverPage first, so right-click the Views folder and add a new
ContentPage. Make sure you select .NET MAUI ContentPage (XAML) and not .NET MAUI
ContentView (XAML). Then change the ContentTemplate in the AppShell.xaml file:
<ShellContent
ContentTemplate="{DataTemplate views:GameOverPage}"
Route="GameOverPage" />
</Shell>
This page is only going to display some labels and buttons. The GameOverPage.xaml file should
look like this:
115
<Label
Grid.Row="2"
Text="The winner is Player 2, having started at $1000, winning at $396."
FontSize="40"
FontAttributes="Bold"
HorizontalOptions="Center" />
<FlexLayout
Grid.Row="3"
JustifyContent="Center">
<Button
Text="Play Again"
FontSize="30"
WidthRequest="300"
Margin="0, 0, 50, 20" />
<Button
Text="Quit"
FontSize="30"
WidthRequest="300"
Margin="0, 0, 50, 20" />
</FlexLayout>
</Grid>
</ContentPage>
As you can see, we’re using a FlexLayout to center the buttons. Run the app. The GameOverPage
should look something like this:
We’re done. All our most important pages are in place. Of course, they aren’t functional and
contain dummy data, which we will fix soon. They don’t look pretty, either. In the next chapter
we’ll be styling them so that they really shine.
116
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the previous chapter we created all the major pages and content views. But we didn’t care so
much about how the particular elements look. We’ll fix this in this chapter by adding styles.
When you run your app, all the views already have some default styles applied. These styles are
defined in the Styles.xaml file in the Resources → Styles folder. But usually you will want to define
your own styles, so let’s see how to do it.
About Styles
If you look at the screenshots from the previous chapter, you will notice two things. On one hand,
there’s a lot of repetitive code that determines how the particular views should look. Here’s an
example from the GameOverPage:
...
<ContentPage ...>
...
<Button
Text="Play Again"
FontSize="30"
WidthRequest="300"
Margin="0, 0, 50, 20" />
<Button
Text="Quit"
FontSize="30"
WidthRequest="300"
Margin="0, 0, 50, 20" />
</FlexLayout>
...
The two buttons have some properties set to identical values, like FontSize, WidthRequest or
Margin. And there are lots of examples like this throughout the application. This means repetitive
code. Lots of repetitive code. Imagine you decide to change the FontSize property to 40 on all
buttons. You would have to do it for every button individually, which is time-consuming and
error-prone. What if there were 75 buttons altogether?
117
On the other hand, if we’re at buttons, have a look at how they differ across the pages:
Different sizes, different font sizes, etc. Maybe they would look better if they had a more uniform
look and feel.
These two issues can be easily addressed by defining styles in one place and applying them to all
the views that should share them. Naturally, sometimes you may want to make one or two views
look different than all the others. It’s also possible, because styles in XAML can be defined for
particular views, layouts, pages or globally, for the entire app.
Let’s have a look at the buttons in the GameOverPage again. Which property values do they share?
They have the same FontSize, WidthRequest and Margin. So, instead of duplicating the code, let’s
create a style and apply it to them. In order to do that, we have to add a ResourceDictionary to
the page.
A ResourceDictionary is an object in which we define one or more styles. It’s the content
property of the ContentPage.Resources object, so it may be omitted.
Each style contains one or more Setter objects. Each Setter object has two properties: Property
and Value. The Property property (awkward as it sounds) is set to the name of the property of the
118
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
object to which the style is to be applied. The Value property is the value we want that property to
be set to.
If this sounds complicated to you, then don’t worry. Let’s create a ResourceDictionary in the
GameOverPage with one style that we will apply to the buttons. Here’s the code:
...
<ContentPage ...>
<ContentPage.Resources>
<ResourceDictionary>
<Style TargetType="Button">
<Setter Property="FontSize" Value="30" />
<Setter Property="WidthRequest" Value="300" />
<Setter Property="Margin" Value="0, 0, 50, 20" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<Grid
...
<FlexLayout
Grid.Row="3"
JustifyContent="Center">
<Button
Text="Play Again" />
<Button
Text="Quit" />
</FlexLayout>
...
As I mentioned before, we can omit the ResourceDictionary because it’s the content property of
ContentPage.Resources. The following code will work just as fine:
...
<ContentPage ...>
<ContentPage.Resources>
<Style TargetType="Button">
<Setter Property="FontSize" Value="30" />
<Setter Property="WidthRequest" Value="300" />
<Setter Property="Margin" Value="0, 0, 50, 20" />
</Style>
</ContentPage.Resources>
<Grid
...
Anyway, here we have one style. One thing to keep in mind is that we always have to set the
TargetType property of the Style object to the name of the VisualElement that the style will be
119
applied to. Here we’re going to apply it to the Button objects.
Have a look at the two buttons inside the FlexLayout again. We removed all the properties that
are defined in the style and only left those that differ for each instance. So, the style we defined in
the ResourceDictionary applies to both the buttons and if there were more buttons, it would
apply to all of them. Let’s now change the style to look like so:
...
<ContentPage ...>
<ContentPage.Resources>
<Style TargetType="Button">
<Setter Property="FontSize" Value="40" />
<Setter Property="WidthRequest" Value="300" />
<Setter Property="Margin" Value="0, 0, 50, 20" />
<Setter Property="HeightRequest" Value="100" />
<Setter Property="BackgroundColor" Value="Black" />
<Setter Property="TextColor" Value="White" />
<Setter Property="CornerRadius" Value="50" />
</Style>
</ContentPage.Resources>
<Grid
...
So, we changed the value of the FontSize property from 30 to 40 and we added some more
properties. You have to write this code only once, right here in the ResourceDictionary, and the
style will be applied to all the buttons on the page. If you run the app (with the GameOverPage set
in AppShell.xaml), the two buttons will look like so:
Fine, but what if we wanted to apply the style only to some elements of a particular type and
another style to the others? Well, this is where implicit and explicit styles come into play.
We can also define explicit types and then apply them only to those instances of the type defined
in the TargetType property that we want to. To do that, we must specify an x:Key attribute in the
Style object and then explicitly apply the styles using the value assigned to that attribute.
120
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Again, things will get clearer when you see it in action. Still in the GameOverPage, have a look at
the three labels:
...
<ContentPage ...>
...
<Grid
RowDefinitions="2*, 2*, 2*, *">
<Label
Text="Game Over"
FontSize="120"
FontAttributes="Bold"
HorizontalOptions="Center" />
<Label
Grid.Row="1"
Text="There's only one player with any money left."
FontSize="40"
FontAttributes="Bold"
HorizontalOptions="Center" />
<Label
Grid.Row="2"
Text="The winner is Player 2, having started at $1000, winning at $396."
FontSize="40"
FontAttributes="Bold"
HorizontalOptions="Center" />
<FlexLayout
...
They all share the values of the FontAttributes and HorizontalOptions properties, but the
FontSize property of the first label is set to 120, whereas the FontSize of the other two is set to 40.
So, let’s create two styles for the labels. These are going to be explicit styles, so we need to specify
the x:Key attribute for them. We can use any value for that attribute we want. A descriptive one
wouldn’t be that bad. So, let’s add the two styles to the ResourceDictionary and then apply them
to the particular labels:
...
<ContentPage ...>
<ContentPage.Resources>
<Style TargetType="Button">
...
</Style>
121
<Grid
RowDefinitions="2*, 2*, 2*, *">
<Label
Text="Game Over"
Style="{StaticResource labelLargeStyle}" />
<Label
Grid.Row="1"
Text="There's only one player with any money left."
Style="{StaticResource labelBaseStyle}" />
<Label
Grid.Row="2"
Text="The winner is Player 2, having started at $1000, winning at $396."
Style="{StaticResource labelBaseStyle}" />
<FlexLayout
...
The labelLargeStyle is applied only to the first label. The labelBaseStyle is applied to the
other two. Watch the syntax that we use to apply a style to an object. We use the Style property
and set it to something that looks weird. You will run into syntax like that quite often in XAML.
The curly braces mean that we have a so-called markup extension here. We’re not going to go into
details here because markup extensions will be the topic of one of the following chapters. Just
remember that we use the StaticResource (for styles that remain unchanged for the duration of
the application) or DynamicResource (if the style may change) markup extension with the key
specified in the ResourceDictionary to apply a style.
You may have noticed that the two label styles in the ResourceDictionary still share some values.
They both have the same values of FontAttributes and HorizontalOptions. This can be
simplified even further. Styles can inherit from other styles.
Style Inheritance
In order to reduce code duplication, styles can inherit from other styles that share some of the
property values. For example, you could create a base style for all labels that will only contain the
properties shared by all labels and then derive other styles from it. To derive a style from another
style, we use the BasedOn property and set it to a StaticResource markup extension that
references the base style. Let’s see how it works on the example of our three labels.
The labelLargeStyle will inherit from labelBaseStyle and only define those properties that
were not defined in the base style:
...
<ContentPage.Resources>
...
<Style x:Key="labelLargeStyle"
TargetType="Label"
BasedOn="{StaticResource labelBaseStyle}">
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="HorizontalOptions" Value="Center" />
122
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
That’s much more concise. OK, but what if we wanted to create a style that can be applied to both
labels and buttons. Let’s say we want the labels and buttons to share the same value of the
Rotation property.
...
<ContentPage.Resources>
<Style x:Key="baseStyle" TargetType="View">
<Setter Property="Rotation" Value="5" />
</Style>
<Style TargetType="Button"
BasedOn="{StaticResource baseStyle}">
<Setter Property="FontSize" Value="40" />
...
</Style>
123
from labelBaseStyle, which, in turn, inherits from baseStyle.
Well, we know how to define styles. All the styles we’ve created so far were defined on page level,
but is this the only place where you can define styles? And what does it have to do with style
scopes?
Style scopes
We defined all our styles on page level. This means they are available everywhere within that page.
This is where we usually define styles if we need them on that page only. But this isn’t the only
place where you can define styles. You can define styles anywhere in the hierarchy. Where you
define them, determines their scope. They are always available for the element they are defined on
and all its children.
Let’s set the ContentTemplate and Route properties in AppShell.xaml to SettingsPage so that we
can see this page when we launch the app. Also, open the SettingsPage.xaml file so that we can
define some styles in it.
This time we’ll create a style on layout level, not on page level. Find the Grid layout where the
header labels are defined. It looks like so:
...
<!--the Players panel-->
...
<Grid
RowDefinitions="*"
ColumnDefinitions="100, 3*, 2*"
>
<Label
Grid.Column="1"
Text="Name (max 10 characters)" />
<Label
Grid.Column="2"
Text="Initial Money ($10 - $5000)" />
</Grid>
<VerticalStackLayout>
...
If we define the styles inside the Grid, they will be accessible only to the Grid and its children, but
not anywhere else on the page. Let’s define an implicit style:
...
<Grid
RowDefinitions="*"
ColumnDefinitions="100, 3*, 2*">
124
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid.Resources>
<Style TargetType="Label">
<Setter Property="TextColor" Value="Green" />
<Setter Property="FontSize" Value="30" />
</Style>
</Grid.Resources>
<Label
...
Here, the style is defined in the ResourceDictionary that is the content property of the
Grid.Resources object. Run the app and you will see that only the labels inside the Grid have
been styled:
Now remove the style you just created and let’s try to define a style on a view level. This definitely
isn’t something you’re going to be doing frequently, but it is possible. Let’s define the style inside
the first label with the Text property set to "Settings":
...
<!--the Settings label-->
<Label
Text="Settings"
VerticalOptions="Center"
FontSize="18">
<Label.Resources>
<Style TargetType="Label">
<Setter Property="TextDecorations" Value="Underline" />
<Setter Property="TextColor" Value="Red" />
</Style>
</Label.Resources>
</Label>
...
125
As you can see, we can’t use self-closing tags for the label annymore. If you run the app, you’ll see
that only this one label has been styled:
To achieve the same effect, you could just have set the properties directly on the Label object like
we did before.
Remove the style from the Label view. We set it there just for demonstrational purposes. OK, we
know how to define styles with different scopes within a page, but what if we want to create a style
that can be used globally, everywhere in the app. For example, let’s say we want all the buttons
across all the pages to have the same look and feel.
Global styles are defined in the app’s resource dictionary, in the App.xaml file. They can be implicit
or explicit. Let’s define an implicit style for the Button. Here’s the code:
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="Red" />
<Setter Property="TextColor" Value="Yellow" />
<Setter Property="FontSize" Value="30" />
<Setter Property="FontAttributes" Value="Bold, Italic" />
</Style>
</ResourceDictionary>
...
126
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Now the style should be applied to all the buttons across the app. But is it? Let’s check it out. We
should see a red button with a yellow text. You have to manually change the page in the
AppShell.xaml file to test it. Here’s the SettingsPage:
The style has been applied correctly. Let’s now run the RacePage:
127
Here it works too. What about the GameOverPage? Lest check it out:
Well, here it doesn’t look that good. What happened? Well, a conflict happened. The styles for the
button are defined in two places: on page level and globally in App.xaml. Looks like the style
defined in the GameOverPage’s ResourceDictionary wins. The font size is 40, because this is what
we set in the page-level style. But the background color is black, which is what we set on page level
too, and so on. In order to understand why it is like that, we have to talk about precedence of styles.
Precedence of Styles
If you have ever worked with CSS, you know that styles can be overridden by styles defined lower
in the hierarchy. In XAML it works exactly the same. App-level styles are overridden by page-level
styles and control-level styles. Page-level styles are overridden by control-level styles, etc. All styles
are overridden by properties set directly on a control. And this is why the buttons in the
GameOverPage looked different than those in the other pages. But let’s have a closer look at what
exactly happened.
Let’s start at the app level. Do we have any styles defined here that are applied to buttons? Turns
out we do:
...
<Application ...>
<Application.Resources>
<ResourceDictionary>
...
128
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="Red" />
<Setter Property="TextColor" Value="Yellow" />
<Setter Property="FontSize" Value="30" />
<Setter Property="FontAttributes" Value="Bold, Italic" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
The BackgroundColor, TextColor, FontSize and FontAttributes properties are set here. At this
level all the buttons across the app are red, with a yellow text with FontSize set to 30 and the
FontAttributes set to Bold and Italic.
Let’s move down the hierarchy. We’re interested in the GameOverPage, so let’s see if there are any
styles defined on page level:
<ContentPage.Resources>
<Style x:Key="baseStyle" TargetType="View">
<Setter Property="Rotation" Value="5" />
</Style>
<Style TargetType="Button"
BasedOn="{StaticResource baseStyle}">
<Setter Property="FontSize" Value="40" />
<Setter Property="WidthRequest" Value="300" />
<Setter Property="Margin" Value="0, 0, 50, 20" />
<Setter Property="HeightRequest" Value="100" />
<Setter Property="BackgroundColor" Value="Black" />
<Setter Property="CornerRadius" Value="50" />
</Style>
...
</ContentPage.Resources>
...
Here we have an implicit style that is applied to all buttons on this page. The following properties
are set here: FontSize, WidthRequest, Margin, HeightRequest, BackgroundColor,
CornerRadius and Rotation, which is inherited from baseStyle. As you can see, two of the
properties, FontSize and BackgroundColor are set again. They will take precedence and override
the same properties set on app level. This is why the font size is 40 and the background color is
black. The other properties were not defined on app level, so they are set here for the first time.
129
Let’s move down the hierarchy to see if any styles for the buttons are defined there. Well, there are
no styles defined in the Grid that encloses the FlexLayout where the buttons are defined, nor in
the FlexLayout itself. Let’s see if any properties were set on the Button objects themselves:
...
<ContentPage ...>
...
<FlexLayout
...>
<Button
Text="Play Again" />
<Button
Text="Quit" />
</FlexLayout>
...
Well, just the Text property. The Text property could also be set higher up in the hierarchy, even
in global styles if for some reason you wanted to have the same text on all buttons. But here it’s
defined directly on the buttons, so this value will be applied. No other properties are set directly on
the buttons. We’re done. The properties that were set lower in the hierarchy overrode the ones set
higher up.
If you want to set a different value for, let’s say, the BackgroundColor property on just one of the
buttons, all you have to do is set it directly on that button:
...
<ContentPage ...>
...
<FlexLayout
...
<Button
Text="Play Again" />
<Button
Text="Quit"
BackgroundColor="Green" />
</FlexLayout>
</Grid>
</ContentPage>
130
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
ContentPages
Let’s start by setting the background color of the ContentPages to a shade of yellow. We’ll use hex
values to set the color this time, not a named color.
First, remove the style we created for the buttons in the App.xaml file and set the SettingsPage in
AppShell.xaml as the page you want to see when you run the app. Then add the following code to
the app’s resource dictionary:
...
<Application ...>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
...
</ResourceDictionary.MergedDictionaries>
<Style TargetType="ContentPage"
ApplyToDerivedTypes="True">
<Setter Property="BackgroundColor" Value="#FFFBDB" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
For this to work, we also have to set the ApplyToDerivedTypes property to True. Normally, this
property is set to True to enable a style to be applied to controls that derive from a base type
assigned to the TargetType property, but sometimes it must be used, like here, even if the style is
to be applied to the base type.
131
Anyway, if you run the app now, the SettingsPage will look like this:
Let’s not only define the styles that will be used in our app, but also let’s take care of the other
visual aspects. Assuming we’re still in the SettingsPage.xaml file, you can see that the main
elements on this page are placed inside a VerticalStackLayout, but I think a Grid would be a
better choice. Fortunately, this is very easy to change. Make sure your code in the SettingsPage.xaml
file looks like this:
132
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Borders
Next, let’s define a style for the Borders. As there are Borders in the RacePage as well, let’s put the
code in the App.xaml file. An implicit style will do because we want to apply the style to all the
Borders:
133
As you can see, I used the same values as were originally set on the Border objects, except for the
Stroke property, which is now set to a darker shade of brown. I also added the Margin property to
add some room around the borders.
Also, remove all the properties except Grid.Row, Grid.Column and Grid.ColumnSpan from the
Border objects in the SettingsPage and in the RacePage. Now the SettingsPage should look like
this:
134
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Buttons
The next element I’d like to style is the Button. There are buttons on each of the three pages, and
they all (except the Sound button) should look the same, so, again, let’s define a global style in
App.xaml:
...
<Application ...>
...
<Style TargetType="Border">
...
</Style>
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="#520000" />
<Setter Property="TextColor" Value="#FFCC1A" />
<Setter Property="FontSize" Value="18" />
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="VerticalOptions" Value="Center" />
<Setter Property="WidthRequest" Value="250" />
<Setter Property="HeightRequest" Value="50" />
</Style>
</ResourceDictionary>
...
135
Now remove all the Button styles from the three pages as well as the properties set directly on the
buttons in all the pages and content views that were defined in the resource dictionary. An
exception is the Sound button in the RacePage, which is supposed to be shorter and right-aligned.
Besides, we want a note image to be displayed on this button instead of the text. Actually two note
images will be needed, one to indicate the sound is on and another to indicate the sound is off. You
can grab the images from the Github repository. Their names are sound_on.png and sound_off.png.
Download them from there and place them in the Images folder like before. We can place an image
on the button using the ImageSource property.
...
<ContentPage ...>
...
<!--the buttons-->
<VerticalStackLayout
...
<Button
Text="Instructions" />
<Button
ImageSource="sound_on.png"
WidthRequest="125"
HorizontalOptions="End" />
</VerticalStackLayout>
...
The buttons on the other pages look pretty much the same. Check it out.
136
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Some controls are used on multiple pages, so I’ll define the styles for them globally. Those controls
that are used only in one location, will be styled locally. I’ll try to keep the code clean and simple.
So, let’s first define some global styles in the App.xaml file:
...
<Application ...>
...
<Style TargetType="Button">
...
</Style>
<Style x:Key="labelSectionTitleStyle"
TargetType="Label"
BasedOn="{StaticResource labelBaseStyle}">
<Setter Property="FontSize" Value="20" />
<Setter Property="FontAttributes" Value="Bold" />
</Style>
<Style TargetType="Entry">
<Setter Property="BackgroundColor" Value="White" />
<Setter Property="FontSize" Value="18" />
<Setter Property="CharacterSpacing" Value="1.5" />
<Setter Property="HorizontalOptions" Value="Start" />
</Style>
<Style TargetType="RadioButton">
<Setter Property="FontSize" Value="18" />
<Setter Property="VerticalOptions" Value="Center" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
Here we have a base style for the Label and another Label style that inherits from it. We don’t
need any more styles on app level because these are the two styles that occur over and over again
137
throughout the entire application. There are some labels that are styled differently, but we’ll define
the styles for them locally.
Let’s have a look at the pages and content views one by one where these styles are applied. You
will find the full code base in my Github repository, so if something is not clear, make sure to check
it out right there. Here’s how the XAML code in the SettingsPage.xaml file ended up looking:
<ContentPage.Resources>
<Style TargetType="RadioButton">
<Setter Property="Margin" Value="0, 0, 0, 10" />
</Style>
</ContentPage.Resources>
<Grid
Margin="10"
RowDefinitions="40, 2.5*, 1.5*, 50">
<Grid>
<Image
Source="all_slugs.png"
Aspect="Fill"
Opacity=".5"/>
<VerticalStackLayout VerticalOptions="Center">
<Label
Text="The Players"
Style="{StaticResource labelSectionTitleStyle}" />
<HorizontalStackLayout>
<RadioButton
Content="1 player"
GroupName="players" />
<RadioButton
Content="2 players"
GroupName="players" />
<RadioButton
Content="3 players"
GroupName="players" />
<RadioButton
Content="4 players"
GroupName="players" />
</HorizontalStackLayout>
138
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid
RowDefinitions="*"
ColumnDefinitions="150, 3*, 2*">
<Label
Grid.Column="1"
Text="Name (max 10 characters)"
Style="{StaticResource labelBaseStyle}"/>
<Label
Grid.Column="2"
Text="Initial Money ($10 - $5000)"
Style="{StaticResource labelBaseStyle}"/>
</Grid>
<VerticalStackLayout>
<controls:PlayerSettings />
<controls:PlayerSettings />
<controls:PlayerSettings />
<controls:PlayerSettings />
</VerticalStackLayout>
</VerticalStackLayout>
</Grid>
</Border>
139
<!--the Ready button-->
<Button
Grid.Row="3"
Text="Ready" />
</Grid>
</ContentPage>
There’s one thing worth mentioning here. I added a background image to the Players panel. To do
that, I wrapped the VerticalStackLayout that was there in a Grid and added the image in the
first cell. I set its Aspect property to Fill and Opacity to 0.5.
The SettingsPage contains four instances of the PlayerSettings content view. The
PlayerSettings.xaml view should now look like so:
<ContentView.Resources>
<Style TargetType="Label"
BasedOn="{StaticResource labelBaseStyle}" />
</ContentView.Resources>
<Grid
RowDefinitions="*"
ColumnDefinitions="150, 3*, 10, 2*"
Margin="0, 10">
<Label
Text="Player Name" />
<Entry
Grid.Column="1"
WidthRequest="300"/>
<Label
Grid.Column="2"
Text="$" />
<Entry
Grid.Column="3"
WidthRequest="250" />
</Grid>
</ContentView>
In the ResourceDictionary of the view we defined a new style for the Label that inherits from
labelBaseStyle and doesn’t define any new properties. So, the new style is actually a copy of the
original one. However, as this is an implicit style, we don’t have to set the style individually on
each and every label. There are also some minor changes in the values of some properties.
I also changed the color of the Border in the App.xaml file to match the color of the buttons. I also
want to fill the circles in the radio buttons with that color, but for now we’re quite limited as far as
styling radio buttons is concerned. This is possible and we’ll do it in the next chapter where we’ll
140
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
be talking about visual states and control templates. For now let’s leave the radio buttons as is.
Next, let’s set the RacePage as the starting page in AppShell.xaml. This page contains several content
views, so let’s have a look at the particular views one by one. The first three content views are very
similar. The first one is GameInfo. Here’s the code in the GameInfo.xaml file:
<ContentView.Resources>
<Style TargetType="Label" BasedOn="{StaticResource labelBaseStyle}" />
</ContentView.Resources>
<Grid
RowDefinitions="*, *, *, *, *"
ColumnDefinitions="3*, *">
<Label
Text="Game Info"
Style="{StaticResource labelSectionTitleStyle}" />
<Label
...
Again, I copied the style in the ResourceDictionary and so don’t have to set the style individually
on each label. The code in the SlugsStats.xaml and PlayersStats.xaml files looks very similar. In the
141
SlugInfo content view there are white labels, but I didn’t create a style for them. The properties
are still set directly on the objects.
I made some minor changes in the WinnerInfo content view. I also defined a style for the labels in
this view. Here’s the code in the WinnerInfo.xaml file:
<ContentView.Resources>
<Style TargetType="Label">
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="HorizontalOptions" Value="Center" />
</Style>
</ContentView.Resources>
<Grid
RowDefinitions=".6*, *, 3*">
<Label
Text="The winner is"
FontSize="28" />
<Label
Grid.Row="1"
Text="Speedster"
FontSize="36" />
<Image
Grid.Row="2"
Source="speedster.png" />
</Grid>
</ContentView>
In the Bets content view I created a style for the sliders. The style is defined for the content view,
so it’s also available for its children all the way down in the hierarchy. The sliders are actually
inside the PlayerBet views. The same works for the labels. I created a copy of labelBaseStyle,
which will be used on the labels in the PlayerBet views. Here’s the code of the Bets.xaml file:
<ContentView.Resources>
<Style TargetType="Label" BasedOn="{StaticResource labelBaseStyle}" />
<Style TargetType="Slider">
<Setter Property="ThumbColor" Value="#520000" />
<Setter Property="MinimumTrackColor" Value="#520000" />
</Style>
</ContentView.Resources>
142
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid
RowDefinitions="*, 4*, *">
<Label
Text="Bets"
Style="{StaticResource labelSectionTitleStyle}" />
<VerticalStackLayout
Grid.Row="1">
<controls:PlayerBet />
<controls:PlayerBet />
<controls:PlayerBet />
<controls:PlayerBet />
</VerticalStackLayout>
<Button
Grid.Row="2"
Text="Go"
Margin="0, 0, 0, 5"/>
</Grid>
</ContentView>
The Results content view is styled in a similar way. If you now run the app, the RacePage will
look like so:
As far as the GameOverPage is concerned, I created an implicit style for the labels there, with the
FontSize property overridden on the first label:
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontAttributes" Value="Bold" />
143
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="FontSize" Value="40" />
</Style>
</ContentPage.Resources>
<Grid
RowDefinitions="2*, 2*, 2*, *">
<Label
Text="Game Over"
FontSize="120" />
<Label
Grid.Row="1"
Text="There's only one player with any money left." />
<Label
Grid.Row="2"
Text="The winner is Player 2, having started at $1000, winning at $396." />
<FlexLayout
Grid.Row="3"
JustifyContent="SpaceEvenly">
<Button
Text="Play Again" />
<Button
Text="Quit" />
</FlexLayout>
</Grid>
</ContentPage>
As you can see, the buttons are spaced a bit differently now. Here’s the effect:
We’re almost, although not entirely, done with styles. As I mentioned before, in the next chapter
we’ll be talking about visual states and control templates.
144
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the previous chapter we created styles and applied them to the particular elements in our app.
We aren’t completely done with the styles yet, though.
First of all, some controls, even if they look OK, don’t look exactly the way we want. Here I mean
the radio buttons, for example. What if I wanted to fill the circles with a color or modify another
aspect of their visual representation?
Secondly, some controls should change their visual appearance if something happens, like here -
there are two entries in the SettingsPage. The first one got focus and changed slightly its visual
representation:
But what if you wanted it to change in a different way? Or what if you wanted to disable this
change? Or what if you wanted to change the appearance when you hover your mouse cursor over
the entry?
In our app, as of now, if you hover your mouse over a button, or even if you click the button, there
is no difference, it looks the same. For some controls this functionality has been implemented, to a
certain extent, by defining a couple visual states in the Styles.xaml file. But we need to define more
visual states.
145
Visual states can be defined on a particular view or in a style. This determines their scope. But how
do we define visual states in the first place?
Well, visual states are handled by the Visual State Manager. They are placed inside visual state
groups. There may be one or more such groups and they are identified by name, just like the visual
states themselves. The Visual State Manager defines a visual state group called CommonStates that
contains the most frequently used states. The CommonStates group contains the following visual
states:
- Normal,
- Disabled,
- Focused,
- PointerOver
These states are supported by all types that derive from VisualElement. What you should keep in
mind is that all states within one visual state group are mutually exclusive, so if the current state
changes to, let’s say, Focused, all the settings for the previous state are cleared.
You get all the above visual states in the CommonStates group out of the box, but you can define
your own groups with other visual states, which we are going to do in a while. But first let’s see
how it works on an example. We’re going to use just the states from the CommonStates group for
now.
To set the visual states we use Setter objects that are grouped inside the Setters property of the
VisualState object.
CommonStates
Without further ado, let’s demonstrate how visual states can be implemented and how they work
on the example of the Button view. At first, we’ll set the visual states on one particular view, which
is the Ready button in the SettingsPage, and then we’ll move it to a style so that it can be used by
all the buttons in the app. Here’s the SettingsPage.xaml file:
...
<ContentPage ...>
...
<!--the Ready button-->
<Button
Grid.Row="3"
Text="Ready">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
146
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Gray" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Property="Scale" Value="1.1" />
<Setter Property="Rotation" Value="180" />
<Setter Property="FontSize" Value="30" />
<Setter Property="TextColor" Value="White" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Button>
</Grid>
</ContentPage>
As you can see, we defined three visual states for the button. The first one, Normal, is an empty
state. We must still include it because otherwise the button wouldn’t revert from another state to its
Normal state. For the Disabled state we defined one Setter. In order to see this state in action, we
would have to make the button disabled. You can do it manually by setting its IsEnabled property
to False:
If you now run the app, the button will be in the Disabled state and its background color will
change:
147
Remove the IsEnabled property from the code. And now let’s test the third state we defined,
PointerOver. Just hover your mouse pointer over the button and you should notice that the four
properties defined in the Setters property have changed:
Well, maybe we don’t want the changes to be so extreme, so let’s modify the code a bit. We’ll just
change the background color to a slightly brighter shade for the PointerOver state and we’ll
reduce the button’s opacity for the Disabled state:
...
<!--the Ready button-->
<Button
...
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value=".3" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#891C20" />
</VisualState.Setters>
...
Hover your mouse cursor over the button, then temporarily disable it by setting the IsEnabled
property like you did before and compare the three states:
148
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Well, we just handled the visual states of the Ready button in the SettingsPage, but what about
the other buttons? Can we define visual states that will be shared by all the buttons? Well, we can
easily accomplish this by moving the visual states to a style.
...
<Application ...>
<Application.Resources>
<ResourceDictionary>
...
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="#520000" />
<Setter Property="TextColor" Value="#FFCC1A" />
<Setter Property="FontSize" Value="18" />
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="VerticalOptions" Value="Center" />
<Setter Property="WidthRequest" Value="250" />
<Setter Property="HeightRequest" Value="50" />
</Style>
...
<Application ...>
<Application.Resources>
<ResourceDictionary>
...
<Style TargetType="Button">
...
<Setter Property="HeightRequest" Value="50" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
149
<VisualState.Setters>
<Setter Property="Opacity" Value=".3" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#891C20" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
Don’t forget to remove this code from the Ready button in the
SettingsPage. It should be defined only once, in the style. If you run the
app now, it will work the same. But this time it should work for all the
buttons. Let’s set the RacePage as the starting page and check it out.
As you can see, it works. We’re hovering the mouse pointer over the Instructions button. But how
do we set the visual state for when the button is pressed? Turns out, there are control-specific states
that we can use.
So, let’s add the Pressed visual state to the CommonStates group in the App.xaml file:
...
<Application ...>
...
<Style TargetType="Button">
...
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
...
<VisualState x:Name="PointerOver">
...
150
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="Scale" Value=".98" />
<Setter Property="BackgroundColor" Value="#891C20" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
...
If you now press the button, it will both shrink and change color.
We’re done with the Button. Next, let’s say we want to change the appearance
of the radio buttons. Well, it’s not that easy using just styles. But don’t worry,
this is where visual states and control templates come to the rescue.
ControlTemplates
Control templates is a tool that enables you to define the visual structure of controls. We’re not
going to discuss them here in great detail, but we’ll use them to modify the appearance of our radio
buttons. We’ll do it in App.xaml because we want uniform radio buttons all throughout the app.
We want the circles of the radio buttons to be filled with the same shade of red or brown -
depending on how you perceive this color - as the buttons in Normal state. When checked, a yellow
circle will appear inside. It’s always easier to understand if you see it, so this is how the radio
buttons should look:
These are the radio buttons in the Players section of the SettingsPage. As you can see, I also
added some space between the particular radio buttons. The third button is checked, so a little
yellow circle can be seen in it. By the way, it’s the same shade of yellow as the text color on the
buttons.
Now, how does it look in code? We’re going to modify the part of the App.xaml file where the style
for the RadioButton is defined, which is right after the definition of the Entry style. The code
should look like this:
...
<Application ...>
<Application.Resources>
<ResourceDictionary>
...
<Style TargetType="Entry">
...
151
<ControlTemplate x:Key="RadioButtonTemplate">
<FlexLayout JustifyContent="Start">
<FlexLayout.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="18"/>
<Setter Property="VerticalOptions" Value="Center" />
<Setter Property="Margin" Value="0, 0, 40, 0" />
</Style>
</FlexLayout.Resources>
<Grid
Margin="0, 0, 5, 0"
WidthRequest="28"
HeightRequest="28"
HorizontalOptions="Center"
VerticalOptions="Center">
<Ellipse
Fill="#520000"
HeightRequest="24"
WidthRequest="24"
HorizontalOptions="Center"
VerticalOptions="Center" />
<Ellipse
x:Name="check"
Fill="#FFCC1A"
Background="Transparent"
HeightRequest="12"
WidthRequest="12"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Grid>
<ContentPresenter />
<VisualStateManager.VisualStateGroups>
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Checked">
<VisualState.Setters>
<Setter TargetName="check" Property="Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unchecked">
<VisualState.Setters>
<Setter TargetName="check" Property="BackgroundColor" Value="#F3F2F1" />
<Setter TargetName="check" Property="Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
</FlexLayout>
</ControlTemplate>
<Style TargetType="RadioButton">
<Setter Property="ControlTemplate" Value="{StaticResource RadioButtonTemplate}" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
It’s pretty daunting, so let’s analyze it piece by piece. At first we define a ContentTemplate and
give it a name, which in this case is RadioButtonTemplate. Then we define the visual
representation of the control using XAML markup. In our case the template will be structured as a
FlexLayout with a Grid inside, which in turn will contain two Ellipse objects. Ellipse is a type
152
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
that you can use to draw an ellipse and we’ll use it to draw the bigger outer circle and the smaller
inner circle. The names of the properties inside the Grid and the two ellipses are self-explanatory.
We also define a style that will be applied to all labels inside our radio button. We only have one -
this is the content of the radio button, which you can see next to it. This content will be placed
inside the ContentPresenter object. If you put the object before the Grid, the label would precede
the circle, but we want it to follow it, hence the position of the ContentPresenter after the Grid.
Next, we define two visual states for the RadioButton, Checked and Unchecked. We use the
Opacity property to make the inner circle fully opaque in the Checked state and fully transparent
in the Unchecked state.
We also use the TargetName property in the code above to reference the smaller Ellipse object. If
you look at the second ellipse, you’ll see that we set its name to check. This very name is used here
in the Setter object.
Finally, we assign the RadioButtonTemplate to the ControlTemplate property in the style for
the RadioButton. I also removed the two Setter objects for the content label (setting the FontSize
and VerticalOptions properties) because they are now defined inside the control template.
If you now run the app, all radio buttons will look the same. We already saw the top row of radio
buttons in the SettingsPage. Let’s try out the radio buttons in the Bets panel of the RacePage:
At this moment we can only select one radio button in total, which isn’t the behavior we want, but
don’t worry, we’re going to fix this in due time.
The states we’ve been using so far were the common states that we can define for all or some of the
controls. But we can also create custom visual states. This is what we’re going to do next.
But before we start, let’s slightly modify the style defined for the Entry control in App.xaml. We
want the text to be in bold type and the placeholder text to be a nice shade of gray:
153
...
<Application ...>
<...
<Style TargetType="Entry">
<Setter Property="BackgroundColor" Value="White" />
<Setter Property="FontSize" Value="18" />
<Setter Property="CharacterSpacing" Value="1.5" />
<Setter Property="HorizontalOptions" Value="Start" />
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="PlaceholderColor" Value="#A0A0A0" />
</Style>
...
And now let’s create the visual states for our Entry controls. As you look at the code of the
SettingsPage, you’ll see that the entries are not used directly there. They are inside the
PlayerSettings content view that we created before. So, let’s define the visual states inside this
content view.
We’re going to create two custom states for the entries where the names of the players will be
entered: NameValid and NameInvalid. They will be inside a visual state group called
NameValidityStates.
For the entries where the initial amount of money each player starts the game with is entered, we’ll
create a visual state group called InitialMoneyValidityStates with three visual states:
InitialMoneyValid, InitialMoneyInvalid and InitialMoneyEmpty.
Let’s have a look at the code and then we’ll discuss what’s new. So, here’s the code in the
PlayerSettings.xaml file:
...
<ContentView ...>
...
<Grid
...
<Entry
x:Name="nameEntry"
Grid.Column="1"
WidthRequest="300"
TextChanged="OnNameTextChanged">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="NameValidityStates">
<VisualState x:Name="NameValid">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White" />
<Setter Property="TextColor" Value="Black" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NameInvalid">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#FFDDEE" />
154
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
As you can see, we gave names to the entries because we’ll have to reference them in the code-
behind. We also added the TextChanged events that we’ll also handle in the code-behind. We’re
going to validate the text as it’s being entered, so these events are necessary. Additionally, I set the
Placeholder property to 1000, which seems a reasonable initial money amount for the game. And
then we have the visual states with the custom names we mentioned above. There’s nothing special
or new about them.
Now, the states should change depending on the current text in the entries. This functionality is
handled in the code-behind.
155
To use a state, we must call the static VisualStateManager.GoToState method. This method
takes two arguments: the name of the object on which the state should be set and the state itself.
Have a look yourself, here’s the code in the PlayerSettings.xaml.cs file:
namespace Slugrace.Controls;
if (initialMoneyEmpty)
{
visualState = "InitialMoneyEmpty";
}
VisualStateManager.GoToState(initialMoneyEntry, visualState);
}
}
As you can see, we created two methods here, GoToNameState and GoToInitialMoneyState.
Each of the methods calls the static VisualStateManager.GoToState method on the respective
Entry object (referenced by name) and passes to it as the second argument the appropriate visual
state.
As you type the players’ names and initial amounts of money in the entries, the
OnNameTextChanged and OnInitialMoneyTextChanged methods are fired and the input is
validated.
156
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Name validation is very simple: The name is valid as long as it’s not longer than 10 characters. The
initial money amount is valid if a numeric value from a specified range is entered. We treated the
case when the input is empty separately. This enables us to define three states for the entries.
And now let’s see how it works. Run your app with the SettingsPage set as the starting page and
enter all kinds of text in the entries. Some values should be valid, some should be invalid and one
of the initial money entries should be empty. Here’s what I got:
The first name entry has valid input, so it’s in the NameValid visual state. The name in the second
one is too long, so the entry is in the NameInvalid state.
The first initial money entry is empty, so it’s in the InitialMoneyEmpty state. The second one has
valid input, so it’s in the InitialMoneyValid state. The third and fourth entries have input from
outside the range (which is 10-5000), so they’re in the InitialMoneyInvalid state.
In a similar way, we can define custom visual states for the other entries in our app. We also have
two Entry object in the Ending Conditions section of the SettingsPage and four in the Bets panel
in the RacePage. Let’s take care of the former first. But, wait a minute… If we use entries in
multiple places, why not move the visual states to the App.xaml file. All entries will be able to use
three visual states: Valid, Invalid and Empty. So, the code in the App.xaml file should look like so:
157
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Empty">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White" />
<Setter Property="TextColor" Value="White" />
<Setter Property="FontAttributes" Value="None" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<ControlTemplate x:Key="RadioButtonTemplate">
...
Here, the names of the states are more general than before because they’re not associated with any
particular Entry object, but rather the entire type.
Now you can remove the code responsible for the visual states from the two Entry objects in
PlayerSettings.xaml. But leave the names of the entries and the TextChanged events. As the states
for particular entries will require different conditions to be met, we’ll implement the state change
logic in the code-behind files. Let’s modify the code in the PlayerSettings.xaml.cs file first. It’s the
same code as before except for the names of the visual states:
...
public partial class PlayerSettings : ContentView
{
...
void GoToNameState(bool nameValid)
{
string visualState = nameValid ? "Valid" : "Invalid";
VisualStateManager.GoToState(nameEntry, visualState);
}
if (initialMoneyEmpty)
{
visualState = "Empty";
}
VisualStateManager.GoToState(initialMoneyEntry, visualState);
}
}
The validation should work as before. But don’t take my word for it.
158
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
How about the other entries? Well, the other entries should all accept numeric input in a certain
range, so, if we implement the code like before we’ll get a lot of almost identical repetitive code
scattered all around the code-behind files. In order to keep our code dry (DRY = Don’t Repeat
Yourself - often used to describe code that is not repetitive), we’ll create a static class with a helper
method that we can use to validate the user input in the entries. So, in the root of the project add a
class and name it Helpers. Make it static and add a method to it. This static method could be
named ValidateNumericInputAndSetState and it should combine the functionality of the
OnInitialMoneyTextChanged method and the GoToInitialMoneyState method defined in the
PlayerSettings.xaml.cs file. Here’s the Helpers class:
namespace Slugrace;
if (isEmpty)
{
visualState = "Empty";
}
VisualStateManager.GoToState(control, visualState);
}
}
As you can see, we pass all the data it needs as arguments. We have the text entered in the entry,
the minimum and maximum limits of the range we want to check, and the control whose input
should be validated.
...
public partial class PlayerSettings : ContentView
{
public PlayerSettings()
{
InitializeComponent();
GoToNameState(true);
VisualStateManager.GoToState(initialMoneyEntry, "Empty");
}
159
private void OnNameTextChanged(object sender, TextChangedEventArgs e)
...
private void OnInitialMoneyTextChanged(object sender, TextChangedEventArgs e)
{
Helpers.ValidateNumericInputAndSetState(e.NewTextValue, 10, 5000, initialMoneyEntry);
}
And now let’s take care of the two entries in the Ending Conditions section. The first one will be
used for entering the maximum number of races after which the game is over. This should be a
number between 1 and 100. In the final version of the app this entry will be visible only if the
second ending condition is selected. If the third condition is selected, the second entry will become
visible and you’ll be able to enter the maximum number of minutes the game should last. This
number should be between 1 and 120.
Our validation code must pick the correct visual state depending on what text we enter. The Valid
state will be chosen if the text is a number between 1 and 100 in the first entry or between 1 and 120
in the second one. We’ll also give names to the entries and add events to them.
The two entries are defined directly in the SettingsPage, so let’s add the following code in the
SettingsPage.xaml and SettingsPage.xaml.cs files, starting with the former:
...
<ContentPage ...>
...
<!--the Ending Conditions panel-->
...
<RadioButton
...
<Entry
x:Name="maxRacesEntry"
Grid.Row="1"
Grid.Column="1"
Placeholder="Set max number of races (1-100)"
WidthRequest="300"
HorizontalOptions="Start"
TextChanged="OnMaxRacesTextChanged" />
<RadioButton
...
<Entry
x:Name="maxTimeEntry"
Grid.Row="2"
Grid.Column="1"
160
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here I also changed the value of the WidthRequest property to accommodate the entire
placeholder text.
namespace Slugrace.Views;
This code doesn’t really differ much from that in the PlayerSettings.xaml.cs file. You can now run the
app and check whether the validation works:
As you can see, the first entry is in Invalid state whereas the second one is in Empty state.
And finally, let’s take care of the entries in the Bets panel in RacePage, or, to be precise, in the
PlayerBet control where the entry actually sits. This is going to be quick and easy, so here’s the
PlayerBet.xaml file:
161
...
<Entry
x:Name="betAmountEntry"
Grid.Column="3"
WidthRequest="200"
Placeholder="1 - 1000"
TextChanged="OnBetAmountTextChanged" />
<Slider
...
namespace Slugrace.Controls;
If you now run the RacePage, you can see that the text you enter is validated:
By the way, the placeholder text is hardcoded for now. In the final version of the game, the fixed
value of 1000 will be replaced by the actual amount of money the player currently has and can bet.
Good, now we can create custom visual states. But what if we want to see the entries in the Ending
Conditions section of the SettingsPage only if a corresponding radio button is checked? To
implement this, we would have to be able to attach visual states to an element and let them set
properties on other elements. And, yes, this is possible.
162
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
SettingsPage. The two entries should be by default invisible and disabled. To ensure that, we
have to set Opacity to 0 and IsEnabled to False on each of them. Let’s do it then. Here’s the code
in the SettingsPage.xaml page:
Then we’ll add visual states to the radio buttons that will change the state not of themselves, but of
the entries. Look how it’s done:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<!--the Ending Conditions panel-->
...
<Grid
...
<RadioButton
...
<RadioButton
Grid.Row="1"
Content="The game is over not later than after a given number of races."
GroupName="endingConditions">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Unchecked" />
<VisualState x:Name="Checked">
<VisualState.Setters>
163
<Setter TargetName="maxRacesEntry" Property="Opacity" Value="1" />
<Setter TargetName="maxRacesEntry" Property="IsEnabled" Value="True" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</RadioButton>
<Entry
x:Name="maxRacesEntry"
...
<RadioButton
Grid.Row="2"
Content="The game is over not later than after the racing time you set has elapsed."
GroupName="endingConditions">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Unchecked" />
<VisualState x:Name="Checked">
<VisualState.Setters>
<Setter TargetName="maxTimeEntry" Property="Opacity" Value="1" />
<Setter TargetName="maxTimeEntry" Property="IsEnabled" Value="True" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</RadioButton>
<Entry
x:Name="maxTimeEntry"
...
There are a couple things worth mentioning here. First, we’re using the Unchecked state as an
empty state, which means the default values (the ones set on the respective Entry object) will be
used when the radio button is unchecked. Next, in the Checked state we set Opacity to 1 and
IsEnabled to True, so the entry will be visible and you will be able to enter text in it when the
radio button is checked.
Now, if you attach visual states to an object, which is not the object on which the properties are
supposed to be set, you must somehow reference the other object. You do it by setting the
TargetName property to its name, like here. The visual states are attached to the radio buttons, but
the properties are set on the entries. The entries are referenced by their names.
Let’s also check the first radio button by setting its IsChecked property to True. This will make the
first ending condition the default one:
...
<ContentPage ...>
...
<!--the Ending Conditions panel-->
...
<RadioButton
Content="The game is over when there is only one player with any money left."
IsChecked="True"
GroupName="endingConditions" />
<RadioButton
...
164
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Before we proceed, we must make sure there aren’t any styles defined for the RadioButton in the
SettingsPage. If there are, they will take precedence. Earlier, we set the Margin property in the
page-level resources:
...
<ContentPage ...>
<ContentPage.Resources>
<Style TargetType="RadioButton">
<Setter Property="Margin" Value="0, 0, 0, 10" />
</Style>
</ContentPage.Resources>
<Grid
...
Just remove this piece of code and it will work as expected. Now if you run the app (with the
SettingsPage as the starting page), the first ending will be checked and you won’t see any of the
two entries:
If you select one of the other two radio buttons, their corresponding entry will show up:
Good. That’s it. We’ve covered the basics of visual states and control templates. You might have
noticed that some of the values in the styles were repeated multiple times. For example, we used
the same color value for the background color of the Button, the color of the Border and the color
of the outer circle of the RadioButton. If we wanted to change the color now, we would have to do
it in multiple places, which is tedious and error-prone. But there is a way to solve this problem - we
can use markup extensions. And there are many other use cases for markup extensions, so let’s talk
about them in the next chapter.
165
Chapter 11 - Markup Extensions
code: https://github.com/prospero-apps/Slugrace/tree/chapter11
In the preceding chapters of the book we were already using markup extensions. You can easily
spot them because with most of them (but not all) we use curly braces. Here’s an example from the
SettingsPage.xaml file:
Style="{StaticResource labelSectionTitleStyle}"
This is the StaticResource markup extension. We use it here to set the Style property.
StaticResource is just one example of markup extensions. In this chapter we’re going to see some
more.
So, what are markup extensions and what do we use them for? Well, we use them to set properties
to objects or values defined somewhere else, like in the example above, where the
labelSectionTitleStyle is referenced. Markup extensions are used to share objects and values,
to reference constants and to bind data. As far as data binding is concerned, we’re going to see to it
in a separate chapter. And now let’s have a look at the other use cases.
Let’s start with the colors. Here’s how the colors are defined inside the implicit Entry style in the
App.xaml file:
...
<Application ...>
<Application.Resources>
<ResourceDictionary>
...
<Style TargetType="Entry">
<Setter Property="BackgroundColor" Value="White" />
...
<Setter Property="PlaceholderColor" Value="#A0A0A0" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="ValidityStates">
<VisualState x:Name="Valid">
<VisualState.Setters>
166
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here we can see colors assigned to different properties, like BackgroundColor or TextColor.
Some of the values are repeated, like the named color White. This is just a small portion of the code,
but there are more repeated values throughout the entire file. If we now wanted to change all white
elements to blue ones, we would have to set the properties one by one. We should avoid such
situations, so let’s define a color resource that we can share. Actually, let’s define color resources
for all repeated colors in the ResourceDictionary:
<!--Colors-->
<Color x:Key="mainButtonColor">#520000</Color>
<Color x:Key="buttonTextColor">#FFCC1A</Color>
<Color x:Key="pressedButtonColor">#891C20</Color>
<Color x:Key="normalEntryColor">White</Color>
<Style TargetType="ContentPage"
...
Then we can set the colors using the StaticResource markup extension and the names defined in
the ResourceDictionary:
167
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application ...>
...
<Style TargetType="Border">
<Setter Property="Stroke" Value="{StaticResource mainButtonColor}" />
...
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource mainButtonColor}" />
<Setter Property="TextColor" Value="{StaticResource buttonTextColor}" />
...
<VisualStateGroupList>
...
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource pressedButtonColor}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
...
<Setter Property="BackgroundColor" Value="{StaticResource pressedButtonColor}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
...
<Style TargetType="Entry">
<Setter Property="BackgroundColor" Value="{StaticResource normalEntryColor}" />
...
<VisualStateGroupList>
<VisualStateGroup x:Name="ValidityStates">
<VisualState x:Name="Valid">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource normalEntryColor}" />
...
</VisualState.Setters>
</VisualState>
...
<VisualState x:Name="Empty">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource normalEntryColor}" />
<Setter Property="TextColor" Value="{StaticResource normalEntryColor}" />
...
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
...
<ControlTemplate x:Key="RadioButtonTemplate">
...
<Ellipse
Fill="{StaticResource mainButtonColor}"
...
<Ellipse
...
Fill="{StaticResource buttonTextColor}"
...
</Grid>
...
We could put all the colors in the ResourceDictionary to keep them centralized, but here I
limited myself to the colors that are repeated at least twice in the code.
So, this is how we can share color values across the app. But we can also share other values, like
layout options or numeric values, for example. For the latter we can use tags like <x:Double> or
168
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<x:Int32>. Let’s now put all repeated values in the ResourceDictionary and then reference them
using the StaticResource markup extension and their names:
...
<Application ...>
<Application.Resources>
<ResourceDictionary>
...
<!--Colors-->
...
<!--Layout options-->
<LayoutOptions x:Key="defaultLayoutOptions" Alignment="Center" />
<!--Numeric values-->
<x:Double x:Key="defaultFontSize">18</x:Double>
<!--Other values-->
<FontAttributes x:Key="emphasized">Bold</FontAttributes>
<Style TargetType="ContentPage"
...
<Style TargetType="Button">
...
<Setter Property="FontSize" Value="{StaticResource defaultFontSize}" />
<Setter Property="FontAttributes" Value="{StaticResource emphasized}" />
<Setter Property="HorizontalOptions" Value="{StaticResource defaultLayoutOptions}" />
...
<Style x:Key="labelBaseStyle" TargetType="Label">
<Setter Property="FontSize" Value="{StaticResource defaultFontSize}" />
<Setter Property="VerticalOptions" Value="{StaticResource defaultLayoutOptions}" />
</Style>
<Style x:Key="labelSectionTitleStyle"
TargetType="Label"
BasedOn="{StaticResource labelBaseStyle}">
<Setter Property="FontSize" Value="20" />
<Setter Property="FontAttributes" Value="{StaticResource emphasized}" />
</Style>
<Style TargetType="Entry">
...
<Setter Property="FontSize" Value="{StaticResource defaultFontSize}" />
...
<Setter Property="FontAttributes" Value="{StaticResource emphasized}" />
...
<ControlTemplate x:Key="RadioButtonTemplate">
<FlexLayout JustifyContent="Start">
<FlexLayout.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="{StaticResource defaultFontSize}" />
<Setter Property="VerticalOptions" Value="{StaticResource defaultLayoutOptions}" />
...
</Style>
</FlexLayout.Resources>
<Grid
...
HorizontalOptions="{StaticResource defaultLayoutOptions}"
VerticalOptions="{StaticResource defaultLayoutOptions}">
<Ellipse
...
HorizontalOptions="{StaticResource defaultLayoutOptions}"
VerticalOptions="{StaticResource defaultLayoutOptions}" />
<Ellipse
...
HorizontalOptions="{StaticResource defaultLayoutOptions}"
VerticalOptions="{StaticResource defaultLayoutOptions}" />
</Grid>
...
169
Again, we could put all the values in the ResourceDictionary and then reference them, but I only
put there those that are repeated throughout the code. If you run the code now, everything will
look exactly as before.
The resources defined in the App.xaml file are available everywhere throughout the app. So, for
example, the ThumbColor and MinimumTrackColor properties in the Bets content view are set to
the same value as the buttons and borders. We defined the color as mainButtonColor, so let’s use
it now. Here’s the Bets.xaml file:
...
<ContentView ...>
<ContentView.Resources>
...
<Style TargetType="Slider">
<Setter Property="ThumbColor" Value="{StaticResource mainButtonColor}" />
<Setter Property="MinimumTrackColor" Value="{StaticResource mainButtonColor}" />
</Style>
...
We can also use some of the other resources that we defined in App.xaml in other pages and content
views. For example, in the GameOverPage we can set the FontSize and HorizontalOptions
properties like so:
...
<ContentPage ...>
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontAttributes" Value="{StaticResource emphasized}" />
<Setter Property="HorizontalOptions" Value="{StaticResource defaultLayoutOptions}" />
...
Or, in the SettingsPage, we can set FontAttributes and VerticalOptions like this:
170
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
There are also values that are repeated inside one particular page or content view. In such cases, we
can define the resources locally in the ResourceDectionary inside that page or view. As we’re at
the SettingsPage now, let’s start there. A couple values are repeated, like for example
WidthRequest, which is set to 300, and Opacity, which is set to 0. Let’s turn them into resources
and then reference them:
...
<ContentPage ...>
<ContentPage.Resources>
<x:Double x:Key="entryWidth">300</x:Double>
<x:Double x:Key="invisible">0</x:Double>
</ContentPage.Resources>
...
<!--the Ending Conditions panel-->
...
<Entry
...
WidthRequest="{StaticResource entryWidth}"
HorizontalOptions="Start"
Opacity="{StaticResource invisible}"
.../>
...
<Entry
...
WidthRequest="{StaticResource entryWidth}"
HorizontalOptions="Start"
Opacity="{StaticResource invisible}"
.../>
</Grid>
...
In the SlugInfo content view we both define a view-scoped resource and reuse some resources
defined globally:
...
<ContentView ...>
<ContentView.Resources>
<Color x:Key="infoTextColor">White</Color>
</ContentView.Resources>
<Grid
...
171
<Label
...
FontAttributes="{StaticResource emphasized}"
TextColor="{StaticResource infoTextColor}" />
<Label
...
TextColor="{StaticResource infoTextColor}" />
<Label
...
VerticalOptions="{StaticResource defaultLayoutOptions}"
...
FontAttributes="{StaticResource emphasized}"
TextColor="{StaticResource infoTextColor}" />
</Grid>
</ContentView>
In all the examples above we were using the StaticResource markup extension. Let’s now have a
look at some others. Let’s start with the x:Static markup extension.
Now make sure the TestPage is the starting page. Then open the TestPage.xaml file and make sure
it looks like this:
So, here we have a BoxView control with some basic settings. Now open the code-behind and make
sure it looks like this:
172
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
namespace Slugrace.Views;
Watch the namespace. It’s Slugrace.Views, not Slugrace like before, because we moved the file
into the Views folder.
We set three properties on the view using literal values. Instead, we could create some resources
and reference them using the StaticResource markup extension. But we can also define some
static fields or properties somewhere in the code and then reference them using the x:Static
extension. Let’s temporarily add a field and a property to the Helpers class that we defined before:
namespace Slugrace
{
public static class Helpers
{
public static readonly double BoxSize = 600;
public static Color BoxColor { get; set; } = Colors.Red;
173
In order to be able to reference these static values in the TestPage, we must add a namespace with
a prefix there:
...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Slugrace"
x:Class="Slugrace.Views.TestPage"
Title="TestPage">
<Grid>
...
This way the program will know where to look for the values. Don’t worry if you don’t understand
how it works. We’re going to discuss namespaces in XAML in one of the following chapters.
Anyway, now we can reference the field and property like so:
And now let’s see how we can use enumeration members. Modify the XAML code so that it looks
like this:
...
<ContentPage ...>
<Grid>
<BoxView
Color="{x:Static Colors.Red}"
WidthRequest="400"
HeightRequest="400"
VerticalOptions="{x:Static LayoutOptions.Start}"
HorizontalOptions="{x:Static LayoutOptions.Center}" />
</Grid>
</ContentPage>
As you can see, we’re using the x:Static markup extension to set the Color, VerticalOptions
and HorizontalOptions properties.
When you’re done experimenting, remove the field and property from the Helpers class because
we don’t want to clutter it with stuff we won’t be using in the app. And now let’s move on to the
next markup extensions.
174
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Color="{x:Static local:Helpers.BoxColor}"
We don’t clearly see the property name here, just the value. This is because it’s the value of the
content property, and the name of a content property can be omitted provided it’s the first or the
only property. The content property of the x:Static markup extension is Member, so we could
rewrite the code like this:
Color="{x:Static Member=local:Helpers.BoxColor}"
We’re going to use the TestPage.xaml file again, because the collection that we are about to see will
not be part of our app. To create a CollectionView, we must provide two pieces of information to
the object: the collection itself and the template for a single item in the collection. We use the
ItemsSource property to populate the view. To define the appearance of each item in the
collection, we set the ItemTemplate property to DataTemplate.
Let’s create a CollectionView in the TestPage.xaml file then so that we have something to look at
as we discuss the details. Make sure your code looks like this:
...
<ContentPage ...>
<CollectionView Margin="50">
<CollectionView.ItemsSource>
<x:Array Type="{x:Type x:Int32}">
<x:Int32>128</x:Int32>
<x:Int32>526</x:Int32>
<x:Int32>311</x:Int32>
<x:Int32>781</x:Int32>
<x:Int32>982</x:Int32>
</x:Array>
</CollectionView.ItemsSource>
175
<CollectionView.ItemTemplate>
<DataTemplate>
<Border HeightRequest="80" WidthRequest="500">
<Grid>
<Image
Source="racetrack.png"
Scale="1"/>
<Button
Text="{Binding}"
FontSize="20" />
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
You can clearly see the two properties in the code. First, we set the ItemsSource property to an
array of integer numbers and then we set the ItemTemplate property to a DataTemplate which
defines each individual item in the array. Let’s start with the ItemsSource property.
In the example above we hardcoded the items of the array. We use the x:Array markup extension
to define an array in XAML. Unlike with all the other markup extensions that we discussed before,
with the x:Array extension we don’t use curly braces, but rather the items are listed between the
opening and closing tags.
And there’s the x:Type, another markup extension that we often use with the x:Array extension.
This one is used with curly braces. Here it’s used to define the type of the items. So, our
CollectionView contains five integer numbers. These could be strings, colors, images or any other
objects.
Next, we must define the template for a single item. This is what DataTemplate is for. We could
present the data as labels, as images with text, as custom content views, or anything else. Here each
item is displayed inside a border and consists of an image with a button on it. The Text property of
the button is set to the appropriate number from the array. As you can see, we’re using yet another
markup extension to set the Text property, Binding. It just binds the Text property to each item
from the collection one by one. We’re not going to discuss it in more detail here, because there’s
going to be a separate chapter about data binding.
Now let’s run the app (with the TestPage set as the starting page) and we should see the five items
displayed like this:
176
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Naturally, there are more markup extensions, but we’ve covered the most important ones. Now
that our app looks so great… But wait, does it? That’s the problem. We’ve been running it
exclusively on Windows, but what about Android? Does it look as great on your cell phone? And if
not, what can we do about it? In the next chapter we’ll be talking about platform differences.
177
Chapter 12 - Platform Differences
code: https://github.com/prospero-apps/Slugrace/tree/chapter12
Let’s test the app on Android. Make sure the TestPage is set as
the starting page because we’re going to check this page first.
Also, make sure you run the app on Android. Look right to see
what it looks like.
It doesn’t look bad, but it’s not perfect. For example, the borders
have been cut off at both ends. This is because we set their
WidthRequest property to 500, which is too much to fit on the
screen. We could set it to a different value, like 300, but then it
would look too small on Windows.
There is one more markup extension that may come in handy, OnIdiom. This one enables us to set
the properties to different values depending on the device idiom (whether it’s a phone, a tablet, a
desktop, and so on). Let’s have a look at the OnPlatform markup extension first.
178
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentPage ...>
<CollectionView Margin="50">
<CollectionView.ItemsSource>
...
</CollectionView.ItemsSource>
<CollectionView.ItemTemplate>
<DataTemplate>
<Border
HeightRequest="80"
WidthRequest="{OnPlatform WinUI=500, Android=280}">
<Grid>
...
Now, if you run the app on Windows, it looks like before. But on
Android devices the borders are narrower.
As the name suggests, it’s used to set a default value for all
platforms except those which are explicitly specified.
...
<ContentPage ...>
...
<Border
HeightRequest="100"
WidthRequest="{OnPlatform Default=500, Android=300}">
<Grid>
...
As Default is the content property and it’s the first property, we can skip the name:
...
<ContentPage ...>
...
<Border
HeightRequest="100"
WidthRequest="{OnPlatform 500, Android=300}">
<Grid>
...
179
This notation is more concise and I’m going to use it in the app. So, the default property will be
used for Windows.
Before we actually start using the OnPlatform markup extension in our app, let’s have a look at a
slightly different approach.
...
<ContentPage ...>
...
<CollectionView.ItemTemplate>
<DataTemplate>
<Border
HeightRequest="{OnIdiom Desktop=80, Phone=70}"
...
<Grid>
...
There’s also the Default property, which works the same way as
with the OnPlatform extension, so we could rewrite the code
like this:
...
<ContentPage ...>
...
<CollectionView.ItemTemplate>
<DataTemplate>
<Border
HeightRequest="{OnIdiom 80, Phone=70}"
...
180
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
SettingsPage on Android
Let’s start with the SettingsPage and the PlayerSettings
content view. It looks OK on Windows, but if you run it on
Android (look left), you may be disappointed.
Half the stuff we should see here is missing. Let’s fix this. We’re
going to modify three files, SettingsPage.xaml, PlayerSettings.xaml
and App.xaml. Actually, let’s start with the last one. In App.xaml
our global styles are defined.
...
<Application ...>
...
<Style TargetType="Entry">
<Setter Property="BackgroundColor" Value="{StaticResource normalEntryColor}" />
<Setter Property="FontSize"
Value="{OnPlatform {StaticResource defaultFontSize}, Android=12}" />
<Setter Property="CharacterSpacing" Value="1.5" />
...
Next, let’s have a look at the PlayerSettings.xaml file. We’ll make some modifications there:
...
<ContentView ...>
...
<Grid
RowDefinitions="*"
Margin="0, 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 150, Android=80}" />
<ColumnDefinition Width="{OnPlatform 3*, Android=2.5*}" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="2*" />
181
</Grid.ColumnDefinitions>
<Label>
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="Player Name" />
<On Platform="Android" Value="Player X" />
</OnPlatform>
</Label.Text>
</Label>
<Entry
x:Name="nameEntry"
Grid.Column="1"
WidthRequest="{OnPlatform 300, Android=130}"
TextChanged="OnNameTextChanged">
</Entry>
<Label
...
<Entry
x:Name="initialMoneyEntry"
Placeholder="1000"
Grid.Column="3"
WidthRequest="{OnPlatform 250, Android=100}"
TextChanged="OnInitialMoneyTextChanged">
</Entry>
</Grid>
</ContentView>
We can see some interesting changes here. Let’s have a look at them going from top to bottom.
So, we defined ColumnDefinitions using property elements and setting different widths to the
first two columns on Android. The last two columns will be the same on both platforms.
We also used property element syntax for the label below to set the Text property. Here you can
see a different syntax for specifying platform-dependent values. We use the OnPlatform and On
objects where we set the values for Windows and Android. As you remember, we don’t use
quotation marks inside curly braces, so we must do it like this. We mustn’t forget to set the
x:TypeArguments property to the type of the data.
Finally, we made the two entries narrower on Android by setting their WidthRequest property to
platform-specific values.
...
<ContentPage ...>
...
<Grid
Margin="{OnPlatform 10, Android=2}">
182
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform 40, Android=30}" />
<RowDefinition Height="2.5*" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<HorizontalStackLayout>
<RadioButton
GroupName="players">
<RadioButton.Content>
<Label>
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="1 player" />
<On Platform="Android" Value="1" />
</OnPlatform>
</Label.Text>
</Label>
</RadioButton.Content>
</RadioButton>
...
</HorizontalStackLayout>
<Grid
RowDefinitions="*"
ColumnDefinitions="150, 3*, 2*">
<Label
Grid.Column="1"
Style="{StaticResource labelBaseStyle}">
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="Name (max 10 characters)" />
<On Platform="Android" Value="Name" />
</OnPlatform>
</Label.Text>
</Label>
183
<Label
Grid.Column="2"
Style="{StaticResource labelBaseStyle}">
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="Initial Money ($10 - $5000)" />
<On Platform="Android" Value="Money" />
</OnPlatform>
</Label.Text>
</Label>
</Grid>
...
<!--the Ending Conditions panel-->
<Border
Grid.Row="2">
<VerticalStackLayout VerticalOptions="{StaticResource defaultLayoutOptions}">
<Label
Text="Ending Conditions"
Style="{StaticResource labelSectionTitleStyle}"
Margin="0, 0, 0, 10"/>
<Grid
RowDefinitions="*, *, *"
ColumnDefinitions="4*, 2*">
<RadioButton
IsChecked="True"
GroupName="endingConditions">
<RadioButton.Content>
<Label>
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="The game is over when
there is only one player with any money left." />
<On Platform="Android" Value="last player left" />
</OnPlatform>
</Label.Text>
</Label>
</RadioButton.Content>
</RadioButton>
...
<Entry
x:Name="maxRacesEntry"
Grid.Row="1"
Grid.Column="1"
WidthRequest="{OnPlatform {StaticResource entryWidth}, Android=100}"
HorizontalOptions="Start"
Opacity="{StaticResource invisible}"
IsEnabled="False"
TextChanged="OnMaxRacesTextChanged">
<Entry.Placeholder>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="Set max number of races (1-100)" />
<On Platform="Android" Value="1-100 races" />
</OnPlatform>
</Entry.Placeholder>
</Entry>
...
184
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
First, we set platform-specific values for the Margin property on the main Grid. We also use
property element notation for the RowDefinitions to make the first row different on each
platform:
<Grid
Margin="{OnPlatform 10, Android=2}">
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform 40, Android=30}" />
<RowDefinition Height="2.5*" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<Image
Source="all_slugs.png"
Aspect="{OnPlatform Fill, Android=AspectFill}"
Opacity=".5"/>
<Label
Style="{StaticResource labelSectionTitleStyle}">
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="The Players" />
<On Platform="Android" Value="How many players?" />
</OnPlatform>
</Label.Text>
</Label>
This way we’ll be able to put just numbers next to the radio buttons in that part of the UI. Here’s
the first radio button:
<RadioButton
GroupName="players">
<RadioButton.Content>
<Label>
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="1 player" />
<On Platform="Android" Value="1" />
</OnPlatform>
</Label.Text>
</Label>
</RadioButton.Content>
</RadioButton>
185
The others look almost the same, so I omitted them in the code
above.
The other changes are pretty much of the same type, so I’m not
going to bore you with each and every one of them. You will find
the complete code on Github. And here’s the final effect - the
SettingsPage on Android.
As you can see, most of the labels are slightly different. Also the
placeholder text in the Ending Conditions section is shorter. You
can also see that when you enter data in the entries, the
validation works just like on Windows.
Styles
As far as the other pages are concerned, I’m not going to show
you each and every change I made, because this would be too
lengthy. You will find the entire modified code on Github, so go
ahead, open each XAML file one by one and search for the
occurrences of the OnPlatform markup extension. There also
some other minor modifications.
I also made some changes in the App.xaml file where the global styles are defined. These changes
affect the entire app. There are some new styles and some of the old ones are now platform-specific:
...
<Application ...>
...
<!--Numeric values-->
<x:Double x:Key="defaultFontSize">18</x:Double>
<x:Double x:Key="androidDefaultFontSize">14</x:Double>
...
<Style TargetType="Border">
...
<Setter Property="StrokeThickness"
Value="{OnPlatform 5, Android=1}" />
<Setter Property="Padding" Value="5" />
<Setter Property="Margin"
Value="{OnPlatform 5, Android=1}" />
</Style>
<Style TargetType="Button">
...
<Setter Property="WidthRequest"
Value="{OnPlatform 250, Android=150}" />
<Setter Property="HeightRequest"
Value="{OnPlatform 50, Android=40}" />
...
<Style x:Key="labelBaseStyle" ...
186
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Style TargetType="Entry">
...
<Setter Property="FontSize"
Value="{OnPlatform {StaticResource defaultFontSize}, Android=12}" />
...
<ControlTemplate x:Key="RadioButtonTemplate">
...
<Ellipse
Fill="{OnPlatform {StaticResource mainButtonColor},
Android=White}"
Stroke="{StaticResource mainButtonColor}"
HeightRequest="{OnPlatform 24, Android=20}"
WidthRequest="{OnPlatform 24, Android=20}"
...
<Ellipse
x:Name="check"
Fill="{OnPlatform {StaticResource buttonTextColor},
Android={StaticResource mainButtonColor}}"
HeightRequest="{OnPlatform 12, Android=8}"
WidthRequest="{OnPlatform 12, Android=8}"
...
187
RacePage on Android
Let’s have a look at the RacePage as it looks now.
First of all, let’s rearrange the elements on the page. Let’s move
the Bets / Results panel to the top so that it doesn’t get hidden
behind the keyboard when you type in the values in the entries.
Let’s also move the game info, the stats panels, and the buttons,
to the bottom. We can easily do that by setting the Grid.Row
and Grid.Column properties to different values for each
platform.
...
<ContentPage ...>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform 1.3*, Android=2.6*}" />
<RowDefinition Height="{OnPlatform 2*, Android=.7*}" />
<RowDefinition Height="{OnPlatform 2*, Android=*}" />
<RowDefinition Height="{OnPlatform 0, Android=*}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 3*, Android=*}" />
<ColumnDefinition Width="{OnPlatform 3*, Android=*}" />
<ColumnDefinition Width="{OnPlatform 3*, Android=0}" />
<ColumnDefinition Width="{OnPlatform 2*, Android=0}" />
</Grid.ColumnDefinitions>
<!--Game Info-->
<Border
Grid.Row="{OnPlatform 0, Android=3}"
Grid.Column="{OnPlatform 0, Android=1}"
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:GameInfo/>
</Border>
<!--Slugs' Stats-->
<Border
Grid.Row="{OnPlatform 0, Android=2}"
Grid.Column="{OnPlatform 1, Android=0}"
188
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:SlugsStats />
</Border>
<!--Players' Stats-->
<Border
Grid.Row="{OnPlatform 0, Android=2}"
Grid.Column="{OnPlatform 2, Android=1}"
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:PlayersStats />
</Border>
<!--the buttons-->
<VerticalStackLayout
Grid.Row="{OnPlatform 0, Android=3}"
Grid.Column="{OnPlatform 3, Android=0}"
HorizontalOptions="{OnPlatform Android=Fill}"
VerticalOptions="{OnPlatform Android=Center}"
WidthRequest="{OnPlatform 260, Android=150}"
Padding="5"
Spacing="3">
<Button
Text="End Game" />
<Button
Text="Instructions" />
<Button
ImageSource="sound_on.png"
WidthRequest="{OnPlatform 125, Android=150}"
HorizontalOptions="{OnPlatform Default=End, Android=Center}"/>
</VerticalStackLayout>
<!--Racetrack-->
<Border
Grid.Row="1"
Grid.ColumnSpan="4"
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:Racetrack />
</Border>
<!--Bets/Results panel-->
<Border
Grid.Row="{OnPlatform 2, Android=0}"
Grid.ColumnSpan="4"
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:Bets />
</Border>
</Grid>
</ContentPage>
189
If we run the app now, we can see this new arrangement:
We now have to polish the individual content views. Let’s start with the Bets panel. Here’s the
Bets.xaml file:
...
<ContentView ...>
...
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform *, Android=.3*}" />
<RowDefinition Height="{OnPlatform 4*, Android=3*}" />
<RowDefinition Height="{OnPlatform *, Android=.5*}" />
</Grid.RowDefinitions>
<Label
Text="Bets"
Style="{OnPlatform {StaticResource labelSectionTitleStyle},
Android={StaticResource androidLabelSectionTitleStyle}}" />
<VerticalStackLayout
...
190
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
For the effect to be visible, we also have to modify the PlayerBet content view:
...
<ContentView ...>
<ContentView.Resources>
<Color x:Key="backgroundColor">#FFF4E5</Color>
<Style TargetType="Label"
BasedOn="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}" />
</ContentView.Resources>
<Grid
BackgroundColor="{OnPlatform Android={StaticResource backgroundColor}}"
Margin="0, 0, 0, 2">
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform *, Android=*}" />
<RowDefinition Height="{OnPlatform 0, Android=*}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 150, Android=80}" />
<ColumnDefinition Width="{OnPlatform .3*, Android=.8*}" />
<ColumnDefinition Width="{OnPlatform .1*, Android=.1*}" />
<ColumnDefinition Width="{OnPlatform 1.5*, Android=1.4*}" />
<ColumnDefinition Width="{OnPlatform 1.5*, Android=1.9*}" />
<ColumnDefinition Width="{OnPlatform .3*, Android=.5*}" />
<ColumnDefinition Width="{OnPlatform 4*, Android=0}" />
</Grid.ColumnDefinitions>
<Label
Text="Player 1" />
...
<Entry
x:Name="betAmountEntry"
Grid.Column="3"
WidthRequest="{OnPlatform 200, Android=80}"
Placeholder="1 - 1000"
BackgroundColor="{OnPlatform Android={StaticResource backgroundColor}}"
Keyboard="Numeric"
TextChanged="OnBetAmountTextChanged"/>
<Slider
...
<Grid
Grid.Row="{OnPlatform 0, Android=1}"
Grid.Column="{OnPlatform 6, Android=0}"
Grid.ColumnSpan="{OnPlatform Android=6}"
RowDefinitions="*"
ColumnDefinitions="*, *, *, *">
<RadioButton
GroupName="player1">
<RadioButton.Content>
191
<Label
Text="Speedster"
Style="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}">
<Label.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="WinUI" Value="0" />
<On Platform="Android" Value="-12, 0, 0, 0" />
</OnPlatform>
</Label.Margin>
</Label>
</RadioButton.Content>
</RadioButton>
<RadioButton
Grid.Column="1"
GroupName="player1">
<RadioButton.Content>
<Label
Text="Trusty"
Style="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}">
<Label.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="WinUI" Value="0" />
<On Platform="Android" Value="-12, 0, 0, 0" />
</OnPlatform>
</Label.Margin>
</Label>
</RadioButton.Content>
</RadioButton>
<RadioButton
Grid.Column="2"
GroupName="player1">
<RadioButton.Content>
<Label
Text="Iffy"
Style="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}">
<Label.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="WinUI" Value="0" />
<On Platform="Android" Value="-12, 0, 0, 0" />
</OnPlatform>
</Label.Margin>
</Label>
</RadioButton.Content>
</RadioButton>
<RadioButton
Grid.Column="3"
GroupName="player1">
<RadioButton.Content>
<Label
Text="Slowpoke"
Style="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}">
192
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Label.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="WinUI" Value="0" />
<On Platform="Android" Value="-12, 0, 0, 0" />
</OnPlatform>
</Label.Margin>
</Label>
</RadioButton.Content>
</RadioButton>
</Grid>
</Grid>
</ContentView>
...
<ContentView ...>
...
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform *, Android=.3*}" />
<RowDefinition Height="{OnPlatform 4*, Android=3*}" />
<RowDefinition Height="{OnPlatform *, Android=.5*}" />
</Grid.RowDefinitions>
<Label
Text="Results"
Style="{OnPlatform {StaticResource labelSectionTitleStyle},
Android={StaticResource androidLabelSectionTitleStyle}}" />
<VerticalStackLayout
Grid.Row="1"
Spacing="{OnPlatform 0, Android=15}">
<controls:PlayerResult />
...
</VerticalStackLayout>
<Button
...
And the PlayerResult content view:
...
<ContentView ...>
<ContentView.Resources>
<Color x:Key="backgroundColor">#FFF4E5</Color>
<Style TargetType="Label"
BasedOn="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}" />
</ContentView.Resources>
<Grid
BackgroundColor="{OnPlatform Android={StaticResource backgroundColor}}"
193
RowSpacing="{OnPlatform 0, Android=10}"
Margin="0, 0, 0, 5">
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform *, Android=*}" />
<RowDefinition Height="{OnPlatform 0, Android=*}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform *, Android=1.2*}" />
<ColumnDefinition Width="{OnPlatform *, Android=1.4*}" />
<ColumnDefinition Width="{OnPlatform *, Android=*}" />
<ColumnDefinition Width="{OnPlatform *, Android=1.2*}" />
<ColumnDefinition Width="{OnPlatform *, Android=0}" />
<ColumnDefinition Width="{OnPlatform *, Android=0}" />
<ColumnDefinition Width="{OnPlatform *, Android=0}" />
</Grid.ColumnDefinitions>
<Label
Text="Player 1" />
...
<Label
Grid.Row="{OnPlatform 0, Android=1}"
Grid.Column="{OnPlatform 4, Android=0}"
Text="lost $250,"/>
<Label
Grid.Row="{OnPlatform 0, Android=1}"
Grid.Column="{OnPlatform 5, Android=1}" >
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="now has $750. " />
<On Platform="Android" Value="has $750. " />
</OnPlatform>
</Label.Text>
</Label>
<Label
Grid.Row="{OnPlatform 0, Android=2}"
Grid.Column="{OnPlatform 6, Android=2}"
Grid.ColumnSpan="{OnPlatform 1, Android=2}"
Text="The odds were 1.64." />
</Grid>
</ContentView>
Let’s modify the Game Info panel next. There seems to be too little space to fit all the text. Let’s
make the text shorter on Android. Here’s the code:
194
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentView ...>
<Grid
RowDefinitions="*, *, *, *, *"
ColumnDefinitions="3*, *">
<Grid.Resources>
<Style TargetType="Label"
BasedOn="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}" />
</Grid.Resources>
<Label
Text="Game Info"
Style="{OnPlatform {StaticResource labelSectionTitleStyle},
Android={StaticResource androidLabelSectionTitleStyle}}" />
<Label
...
<Label
Grid.Row="2">
<Label.Text>
<OnPlatform x:TypeArguments="x:String">
<On Platform="WinUI" Value="Number of races set:" />
<On Platform="Android" Value="Total races:" />
</OnPlatform>
</Label.Text>
</Label>
<Label
...
Here’s the Android version of the Game Info panel:
There’s also too little room in the stats panels. Let’s start with the Slugs’ Stats panel. Here’s the
SlugsStats.xaml file:
...
<ContentView ...>
<Grid
RowDefinitions="*, *, *, *, *"
ColumnDefinitions="*">
<Label
Text="Slugs' Stats"
Style="{OnPlatform {StaticResource labelSectionTitleStyle},
Android={StaticResource androidLabelSectionTitleStyle}}" />
<controls:SlugStats
...
195
And the SlugStats.xaml file:
...
<ContentView ...>
<ContentView.Resources>
<Style TargetType="Label"
BasedOn="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}" />
</ContentView.Resources>
<Grid
RowDefinitions="*">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 2*, Android=1.8*}" />
<ColumnDefinition Width="{OnPlatform *, Android=1.4*}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
...
You can now see all the text. Let’s take care of the Players’ Stats panel then. Here’s the
PlayersStatsPanel.xaml file:
...
<ContentView ...>
...
<Grid
RowDefinitions="*, *, *, *, *"
ColumnDefinitions="*">
<Label
Text="Players' Stats"
Style="{OnPlatform {StaticResource labelSectionTitleStyle},
Android={StaticResource androidLabelSectionTitleStyle}}" />
<controls:PlayerStats
...
...
<ContentView ...>
<ContentView.Resources>
<Style TargetType="Label"
BasedOn="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}" />
</ContentView.Resources>
196
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid
RowDefinitions="*">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 2.5*, Android=*}" />
<ColumnDefinition Width="{OnPlatform 1.5*, Android=*}" />
</Grid.ColumnDefinitions>
<Label
...
We’re not going to change anything in the Racetrack.xaml, TrackImage.xaml or SlugImage.xaml files.
But there are going to be some changes in SlugInfo.xaml:
...
<ContentView ...>
...
<Grid
Padding="5, 2, 0, 2"
RowDefinitions="*, *"
ColumnDefinitions="6.8*, 2.2*">
<Label
Text="Speedster"
FontSize="18"
FontAttributes="{StaticResource emphasized}"
TextColor="{StaticResource infoTextColor}"
Opacity="{OnPlatform 1, Android=0}" />
<Label
Grid.Row="1"
Text="0 wins"
FontSize="14"
TextColor="{StaticResource infoTextColor}"
Opacity="{OnPlatform 1, Android=0}" />
<Label
Grid.Column="1"
Grid.RowSpan="2"
VerticalOptions="{StaticResource defaultLayoutOptions}"
Text="1.40"
FontSize="{OnPlatform 40, Android=12}"
FontAttributes="{StaticResource emphasized}"
TextColor="{StaticResource infoTextColor}"/>
</Grid>
</ContentView>
...
<ContentView ...>
...
<Grid
RowDefinitions=".6*, *, 3*">
197
<Label
Text="The winner is"
FontSize="{OnPlatform 28, Android=10}" />
<Label
Grid.Row="1"
Text="Speedster"
FontSize="{OnPlatform 36, Android=12}" />
<Image
Grid.Row="2"
Source="speedster.png" />
</Grid>
</ContentView>
As you can see, the white labels near the slug images on the left are invisible on Android. It would
be hard to read them anyway.
So, if we now run the app on Android, the RacePage looks a whole lot better than before:
198
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Don’t worry about the Winner Info panel being visible before the
race even begins. We’ll handle the logic later on.
GameOverPage on Android
Finally, let’s take care of the GameOverPage. If you run it now,
it’s not very attractive.
...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.GameOverPage">
<ContentPage.Resources>
<Style TargetType="Label">
...
<Setter Property="FontSize"
Value="{OnPlatform 40, Android=30}" />
</Style>
</ContentPage.Resources>
<Grid
RowDefinitions="2*, 2*, 2*, *">
<Grid.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="WinUI" Value="0" />
<On Platform="Android" Value="20" />
</OnPlatform>
</Grid.Margin>
<Label
Text="Game Over"
FontSize="{OnPlatform 120, Android=65}" />
<Label
Grid.Row="1"
Text="There's only one player with any money left." />
<Label
...
199
Now it’s much more readable.
The changes in the code were made to target the two different
platforms. Let’s now have a look at some interesting places in the
code.
Numeric Keyboard
Let’s use the SettingsPage for
demonstrational purposes.
When an entry gets focus on
Android (A), a keyboard pops
up (B).
200
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentView xmlns...>
...
<Entry
x:Name="initialMoneyEntry"
Placeholder="1000"
Grid.Column="3"
WidthRequest="{OnPlatform 250, Android=100}"
Keyboard="Numeric"
TextChanged="OnInitialMoneyTextChanged">
...
The entry in the PlayerBet view has this property set too.
Layouts
The layouts on the two platforms differ, especially in the RacePage. This can be achieved by setting
the row heights and column widths to different values. To keep the same number of rows and
columns, we can set some of them to 0, which has the same effect as if they weren’t there at all.
Then we set the Grid.Row and Grid.Column attached properties to different values where needed.
Here’s an example from the RacePage:
...
<ContentPage ...>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="{OnPlatform 1.3*, Android=2.6*}" />
<RowDefinition Height="{OnPlatform 2*, Android=.7*}" />
<RowDefinition Height="{OnPlatform 2*, Android=*}" />
<RowDefinition Height="{OnPlatform 0, Android=*}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 3*, Android=*}" />
<ColumnDefinition Width="{OnPlatform 3*, Android=*}" />
<ColumnDefinition Width="{OnPlatform 3*, Android=0}" />
<ColumnDefinition Width="{OnPlatform 2*, Android=0}" />
</Grid.ColumnDefinitions>
<!--Game Info-->
<Border
Grid.Row="{OnPlatform 0, Android=3}"
Grid.Column="{OnPlatform 0, Android=1}"
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:GameInfo/>
</Border>
...
201
The Game Info content view, for example, is positioned in the first row and first column on
Windows, but in the fourth row and second column on Android. It works the same for the other
views.
Margin is used to set the distance between an element and its adjacent elements.
Padding is used to set the distance between and element and its child elements (its content).
Both Margin and Padding are of type Thickness, which is a structure. Depending on which
constructor is called, we can define Thickness by one, two or four values.
If only one value is defined, it’s applied to all sides of the element uniformly.
If two values are defined, the first value is applied to the left and right sides of the element and the
second value is applied to the top and bottom sides.
If four values are defined, they’re applied to the left, top, right and bottom sides of the element, in
this exact order.
...
<ContentView ...>
...
<RadioButton
GroupName="player1">
<RadioButton.Content>
<Label
Text="Speedster"
Style="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}">
<Label.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="WinUI" Value="0" />
<On Platform="Android" Value="-12, 0, 0, 0" />
</OnPlatform>
</Label.Margin>
</Label>
...
202
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
So, on Windows the label will have the same Margin on all four sides, on Android the distance on
the left side will be decreased (hence the negative value), but it will be 0 on the other sides.
Now we have all the styles in place, our app looks good on both Windows and Android. There’s
just one little topic as far as the visual appearance of our app is concerned, themes. We’ll be talking
about themes in the next chapter.
203
Chapter 13 - Themes
code: https://github.com/prospero-apps/Slugrace/tree/chapter13
Mobile devices usually include light and dark themes which can be set manually by the user or
change automatically when some conditions are met. For example, your cell phone will probably
switch to dark mode when it gets dark in the evening. This way the screen will be easier on the
eyes.
If you want to implement this functionality in your app, you have to set the properties that are
supposed to depend on the current theme to different values, so one value for the light theme and
another for the dark theme. To do that, we use the AppThemeBinding markup extension. Let’s first
demonstrate it using the TestPage.
204
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Let’s modify the code and deliver distinct values for some of the
properties.
First, let’s temporarily define two static properties in the Helpers class:
namespace Slugrace
{
public static class Helpers
{
public static string LightThemeText { get; set; } = "Hey, what a beautiful day!";
public static string DarkThemeText { get; set; } = "Hey, what a beautiful night!";
We’re going to use them in the TestPage. Here’s the modified code:
205
<Button
Text="Press"
BackgroundColor="{AppThemeBinding Light=Black, Dark=White}"
TextColor="{AppThemeBinding Light=White, Dark=Black}" />
</FlexLayout>
</ContentPage>
...
<Application ...>
...
<!--Colors-->
...
<Color x:Key="normalEntryColor">White</Color>
<Color x:Key="lightPrimaryColor">#FFFFCC</Color>
...
206
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<!--Other values-->
<FontAttributes x:Key="emphasized">Bold</FontAttributes>
<Style TargetType="ContentPage"
ApplyToDerivedTypes="True">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource lightPrimaryColor},
Dark=Black}" />
</Style>
<Style TargetType="Border">
<Setter Property="Stroke"
Value="{AppThemeBinding
Light={StaticResource mainButtonColor},
Dark={StaticResource lightPrimaryColor}}" />
<Setter Property="StrokeShape" Value="RoundRectangle 10, 10, 10, 10" />
...
<Style TargetType="Button">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding
Light={StaticResource mainButtonColor},
Dark={StaticResource buttonTextColor}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding
Light={StaticResource buttonTextColor},
Dark={StaticResource mainButtonColor}}" />
<Setter Property="FontSize" Value="{StaticResource defaultFontSize}" />
...
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{AppThemeBinding
Light={StaticResource pressedButtonColor},
Dark={StaticResource lightPrimaryColor}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="Scale" Value=".98" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding
Light={StaticResource pressedButtonColor},
Dark={StaticResource lightPrimaryColor}}" />
</VisualState.Setters>
...
As you can see, I extracted the background color into a resource and used it to set the background
color of ContentPage in the light theme. In the dark theme the background color is set to black.
The colors of the BackgroundColor and TextColor properties of Button are switched in the two
themes.
I also used the new lightPrimaryColor resource to set the Stroke property of Border and the
background color of Button in the PointerOver and Pressed visual states.
207
In the RacePage a different image is used in dark theme for the Sound button. Otherwise it would
be hardly visible in this theme. You can grab the sound_on_dark.png image and the other one,
sound_off_dark.png (which we’re going to need later) from my Github repository. Paste them to the
Resources/Images folder. Here’s how this is implemented in the RacePage.xaml file:
...
<ContentPage ...>
...
<!--the buttons-->
<VerticalStackLayout
...
<Button
Text="Instructions" />
<Button
ImageSource="{AppThemeBinding
Light=sound_on.png,
Dark=sound_on_dark.png}"
WidthRequest="{OnPlatform 125, Android=150}"
...
...
<ContentView ...>
<ContentView.Resources>
<Style TargetType="Label" BasedOn="{StaticResource labelBaseStyle}" />
<Style TargetType="Slider">
<Setter Property="ThumbColor"
Value="{AppThemeBinding
Light={StaticResource mainButtonColor},
Dark={StaticResource buttonTextColor}}" />
<Setter Property="MinimumTrackColor"
Value="{AppThemeBinding
Light={StaticResource mainButtonColor},
Dark={StaticResource buttonTextColor}}" />
</Style>
...
I also defined new color resources in the PlayerBet and PlayerResult content views. They’re
used to set the background color of the Grid and they look the same:
...
<ContentView ...>
<ContentView.Resources>
<Color x:Key="backgroundColorLight">#FFF4E5</Color>
<Color x:Key="backgroundColorDark">Black</Color>
<Style TargetType="Label"
...
208
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid
BackgroundColor="{OnPlatform
Android={AppThemeBinding
Light={StaticResource backgroundColorLight},
Dark={StaticResource backgroundColorDark}}}"
...
And that’s it. Let’s now compare all the pages in light theme and dark theme:
209
Here’s the RacePage with the Results panel:
If you’re wondering why the labels are white in the dark theme, even if we didn’t define their color
for that theme, this is because default values for either theme are defined in the
Resources/Styles/Style.xaml file.
Our application is now beautifully styled, it supports the light and dark themes on Android and
the only thing it lacks is data. In the next chapter we’ll be briefly talking about namespaces in
XAML and then, in the following chapters, we’ll see how to provide data and bind to it.
210
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the previous chapter we implemented the light and dark themes in our app. Before that, we
styled the app. The next step is to start working with real data, because at this moment all the labels
display hardcoded data. But before we dive into data and data binding, let’s briefly discuss another
topic, namespaces in XAML. It’s a pretty straightforward topic, but let’s systematize our
knowledge of namespaces before we move on.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"...
or this:
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"...
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"...
And the Styles.xaml file in the Resources folder also starts in a similar way:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
Whatever the root element is, it always contains these two lines of code. At least these two, but
there may be more. So, what are these lines of code actually for?
Well, these are namespace declarations. The first line defines the default namespace. All elements
in the XAML file that are used without any prefix are instances of .NET MAUI classes. Here belong
ContentPage, Label, Button, Slider, Grid, and many, many more. This namespace declaration
enables us to use the elements in code like so:
<Grid>
<Label>
<ContentView>
211
and so on, so without any prefix. Any other namespaces are non-default namespaces and they do
require a prefix. We use the prefix both in the namespace declaration and then inside the code. In
the second line of each code fragment above you can see the x prefix. This particular namespace
contains classes intrinsic to XAML, and specifically, to the 2009 XAML specification.
To reference items from this namespace in code, we must always use the x prefix. We’ve already
used such objects and attributes in our app. Here are some examples:
<Color x:Key="mainButtonColor">#520000</Color>
<OnPlatform x:TypeArguments="Thickness">
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Slugrace.Controls"
x:Class="Slugrace.Views.RacePage"
Padding="5">
In the last example you can see that the argument is defined on
the root element. This attribute specifies the namespace and class
name for a class defined in XAML. In this particular case it tells
us that the class’s name is RacePage and that it’s defined in the
Views folder, which is inside the root Slugrace folder.
If you open the code-behind file you will see that the class name
of the class defined there matches the one defined in XAML:
namespace Slugrace.Views;
You can also see this exact same namespace defined in C#.
There also markup extensions that are used with the x prefix, like x:Static, x:Array or x:Type
that we talked about before.
212
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
You can use any prefixes you like, but there are some conventions, like, for example, the local
prefix. Here you can see it in App.xaml:
xmlns:local="clr-namespace:Slugrace"
By convention we use the local prefix to reference types local to the app, so types added directly
to the root. When we created the .NET MAUI project, the MainPage.xaml file was added to the root.
We could use the prefix to reference the MainPage like so:
local:MainPage
Another convention is that we often use the names of the folders as prefixes. This is not mandatory,
but it’s pretty common. Let’s have a look at the AppShell.xaml file:
Here we use the views prefix to declare the namespace. It corresponds to the name of the folder it
references. Then, below, in the same file, we use the prefix to reference a type that we defined
there, SettingsPage:
We also used another prefix, controls, to reference types defined in the Controls folder, like for
example in the SettingsPage:
...
<ContentPage ...
xmlns:controls="clr-namespace:Slugrace.Controls"
x:Class="Slugrace.Views.SettingsPage">
...
<VerticalStackLayout>
<controls:PlayerSettings />
<controls:PlayerSettings />
<controls:PlayerSettings />
<controls:PlayerSettings />
</VerticalStackLayout>
...
213
We’re not using types from a different assembly in our app, but if we were, we would have to
declare the namespace like this:
<ContentPage ...
xmlns:controls="clr-namespace:Controls;assembly=ExternalLibraryName" ...>
That’s all you need to know about namespaces in XAML, at least for now. We can finally make our
app do something, not only look decent. In order for an app to do something, it must consume data
that we deliver to it. So, in the next chapter we’ll be talking about data and data binding.
214
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the preceding parts of the book we created the main visual elements of our application, like
pages and content views. They are filled with smaller visual elements called views or controls. This
all looks good, but the app doesn’t do anything. All the data it displays is hardcoded and never
changes. This is not how applications work. What would an application be without data? It would
be no good. So, it’s time we took care of it.
Data in an application can come from different sources. It can come from properties defined in the
code-behind or from a visual component. If you enter something in an entry, it becomes data that
may be consumed by other elements, like for instance be displayed by a label.
How do we handle data in a .NET MAUI application? Well, one way is to use events. For example
you enter something in an entry like in the example above, its TextChanged event fires and the
Text property of the label is set to a new value, which is handled in the code-behind. This
approach works, but if there’s a lot of data, there would have to be lots of events, the code would
quickly become lengthy. Fortunately, there’s a more efficient approach, data binding.
If we used data binding instead of an event, the Text property of the label would be set
automatically when you entered text in the entry. This is because in data binding properties of two
objects are linked so that if one changes, the other changes too.
In this chapter, we’ll be talking about data binding. It’s a complex topic, but first things first. Let’s
start with some basic terminology.
In this chapter I’ll be demonstrating stuff using simple code in the TestPage. We’re going to
implement data binding in the actual app when we discuss the MVVM pattern.
215
<VerticalStackLayout>
<Entry
x:Name="entry"
FontSize="30" />
<Label
BindingContext="{x:Reference Name=entry}"
Text="{Binding Path=Text}"
FontSize="30" />
</VerticalStackLayout>
</ContentPage>
So, here we have a working data binding. The Text properties of the two views are linked. And
now the terminology.
We have two objects here, an entry and a label. The entry is the source, the label is the target.
The data flows from source to target, so the value of the target property is set to the value of the
source property. This is how it works here, but sometimes data can flow in the opposite direction
or in both directions, as we’re going to see.
Then we have the binding context, another term. The binding context is set on the target and
references the source so that the target object knows where to pull the data from.
The target property must be a bindable property. This means the target object must inherit from
BindableObject. Element, VisualElement, View and View derivatives inherit from
BindableObject. The Text property of the Label class is associated with the bindable property
TextProperty.
We also use two markup extensions here. The first one is x:Reference. As its name suggests, it’s
used to reference an object by its name. The second one is Binding. Its Path property is set to the
property of the source object we want to bind to.
The Name property of x:Reference and the Path property of Binding are content properties, so
they may be omitted if they’re the first properties (or the only ones like here). So, we could rewrite
the code like this:
216
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentPage ...>
...
<Label
BindingContext="{x:Reference entry}"
Text="{Binding Text}"
...
We usually set the bindings in XAML, but, naturally, we could do it in C# as well. Let’s see how.
...
<ContentPage ...>
<VerticalStackLayout>
<Entry
x:Name="entry"
FontSize="30" />
<Label
x:Name="label"
FontSize="30" />
</VerticalStackLayout>
</ContentPage>
namespace Slugrace.Views;
public partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
label.BindingContext = entry;
label.SetBinding(Label.TextProperty, "Text");
}
}
If you now run the app, it’ll work just like before.
So, we use the BindingContext property to specify the source object and the SetBinding method
to specify the target and source properties that should be linked. The SetBinding method is called
on the target.
Now look how the source and target properties are specified. The target property is specified as a
BindableProperty object, Label.TextProperty. The source property is specified as a string.
217
As I mentioned before, we’re going to set our bindings primarily in XAML, so remove the two lines
of code where the binding was set from the code-behind and go back to the XAML file. The
example we discussed above with the Text properties of an entry and a label linked together is an
example of view-to-view binding. Let’s have a look at another example of that type.
View-to-view Binding
Each control can have its BindingContext set to only one source (although there is a way around
it, as we’re going to learn), but you can bind as many targets to the source as you wish. To keep
things simple, let’s bind two targets to a single source. Modify the TestPage.xaml file so that it looks
like this:
...
<ContentPage ...>
<VerticalStackLayout>
<Slider x:Name="slider"
Maximum="200" />
<Label
x:Name="label"
FontSize="30"
BindingContext="{x:Reference slider}"
Text="{Binding Value}" />
<BoxView
x:Name="boxView"
Color="Green"
BindingContext="{x:Reference slider}"
WidthRequest="{Binding Value}"
HeightRequest="{Binding Value}" />
</VerticalStackLayout>
</ContentPage>
Here the slider is the source and the label and the box view are the targets. So, both targets have the
same binding context and they both bind to the same property of the source, Value. If you run the
app, you will see the slider and the label, but not the box view below:
The Text property of the label is set to 0 and you can’t see the box view because its WidthRequest
and HeightRequest properties are set to 0. Why? Because the Text property of the label and the
WidthRequest and HeightRequest properties of the box view are all bound to the Value property
of the slider and the default value of the slider is 0.
218
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Now try dragging the slider to the right. You should see the label text change and the box view
grow:
You can also bind to different properties of the source object. Let’s bind the box view’s
WidthRequest property to the Maximum property of the slider. As the Maximum property doesn’t
change over time, the width of the box view won’t change either. Here’s the code:
...
<ContentPage ...>
...
<BoxView
...
WidthRequest="{Binding Maximum}"
HeightRequest="{Binding Value}" />
...
If you now run the app, the label’s Text property and the box view’s HeightRequest property will
change, but the WidthRequest property of the latter will remain constant:
So, everything works as expected. But what if we don’t like how the label text is formatted? Well,
we can easily fix it.
StringFormat
You can easily format strings by setting the StringFormat property. This only works if the target
property is a string, which is the case with the Text property of Label. Let’s display the text as a
number with some additional info. If you want to add additional text, you must enclose it in single
quotes. The actual data is placed in curly braces and you can format it any way you want. In our
case we’ll format it to be a float number with two decimal places:
219
...
<ContentPage ...>
...
<Label
...
Text="{Binding Value, StringFormat='Current slider value: {0:F2}'}" />
<BoxView
...
Here’s another example. Let’s add another label that will display the current date:
...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<VerticalStackLayout>
...
<BoxView
...
<Label
BindingContext="{x:Static sys:DateTime.Now}"
FontSize="20"
Text="{Binding StringFormat='Today is {0:D}.'}" />
</VerticalStackLayout>
</ContentPage>
In this example we’re not binding to another view, but rather to the static DateTime.Now property.
As you know, we use the x:Static markup extension to reference a static property. This property
is defined in the System namespace, in a different assembly, so, as we learned in the previous
chapter, we must add the appropriate namespace in the XAML code. Here we’re using the sys
prefix.
220
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the example above we used a binding context. But is it always necessary? Turns out, it isn’t.
...
<ContentPage ...>
...
<Label
x:Name="label"
FontSize="30"
Text="{Binding Value,
Source={x:Reference slider},
StringFormat='Current slider value: {0:F2}'}" />
<BoxView
...
This is an alternative syntax. But there’s yet another alternative syntax, not so common, but still
worth knowing.
Object-Element Syntax
We usually use the Binding markup extension to set a binding, but using object elements is also
possible. Let’s rewrite the binding on the first label again using this notation:
...
<ContentPage ...>
...
<Label
x:Name="label"
FontSize="30">
<Label.Text>
<Binding Path="Value"
Source="{x:Reference slider}"
StringFormat="Current slider value: {0:F2}" />
</Label.Text>
</Label>
<BoxView
...
Or, even, we can express the x:Reference markup extension as an object element:
...
<ContentPage ...>
...
221
<Label
x:Name="label"
FontSize="30">
<Label.Text>
<Binding Path="Value"
StringFormat="Current slider value: {0:F2}">
<Binding.Source>
<x:Reference Name="slider" />
</Binding.Source>
</Binding>
</Label.Text>
...
This syntax may be useful with complex objects. But in such a simple example as ours, it’s not
necessary. Let’s modify the code so that the binding context is set again:
...
<ContentPage ...>
...
<Label
x:Name="label"
FontSize="30"
BindingContext="{x:Reference slider}"
Text="{Binding Value,
StringFormat='Current slider value: {0:F2}'}" />
<BoxView
...
The first label and the box view use the same binding context. This means we have to set it twice,
on each object individually. Or do we?
...
<ContentPage ...>
<VerticalStackLayout>
<Slider ... />
222
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
</VerticalStackLayout>
<Label
...
Now, remember how I told you that each object can only have one BindingContext and that there
is a way around it? Well, let’s have a closer look at this next.
Binding Modes
A single view can have data binding on multiple properties. In our example the box view has two
bindings: one set on the WidthRequest property and another set on the HeightRequest property.
The former is bound to the value of the Maximum property of the slider and the latter is bound to its
Value property. But still, both Maximum and Value are properties of the slider. But what if we
wanted to bind the two properties of the box view to properties of two different objects? Well, we
would need two binding contexts on the box view, which isn’t possible because a single view can
only have one BindingContext.
Let’s modify our code slightly to illustrate the problem. Here’s the code:
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout>
<Label Text="Width" />
<Slider x:Name="slider1"
Maximum="200" />
223
<BoxView x:Name="boxView"
BindingContext="{x:Reference slider1}"
Color="Green"
WidthRequest="{Binding Value}"
HeightRequest="{Binding Value}" />
</VerticalStackLayout>
</ContentPage>
Now we have three sliders with accompanying labels to inform us what each slider is meant for.
We also have the box view. We now want to use the first slider to change the box view’s width, the
second slider to change its height and the third one to rotate it. But we can’t set the
BindingContext property of the box view to all three sliders, we have to pick one. Here slider1
was picked to set the width, but what about the others? Run the app and try dragging the sliders.
Only the first one does anything:
So, what can we do? Well, we can’t set more binding contexts, but we could use another feature of
data binding, which is modes. Let’s first rewrite the code so that it works as intended and then let’s
think about how it works:
...
<ContentPage ...>
...
<VerticalStackLayout>
<Label Text="Width" />
<Slider x:Name="slider1"
Maximum="200"
BindingContext="{x:Reference boxView}"
Value="{Binding WidthRequest, Mode=OneWayToSource}" />
224
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Slider x:Name="slider3"
Maximum="360"
BindingContext="{x:Reference boxView}"
Value="{Binding Rotation, Mode=OneWayToSource}" />
<BoxView x:Name="boxView"
Color="Green" />
</VerticalStackLayout>
</ContentPage>
Looks as if everything had been reversed. Now the binding context is no longer set on the box view
to reference a slider, but rather the box view is set as the binding context for each slider. The
bindings are set on the Value properties of the sliders. We also set the Mode property to
OneWayToSource. This is one of a couple possible values (we’re going to discuss them in a while).
As the name of this binding mode suggests, the Value properties of the sliders set the appropriate
values of the box view, so WidthRequest, HeightRequest and Rotation respectively. In other
words, values are transferred from the target (each particular slider) to the source (the box view).
Now run the app and try it out:
So, what binding modes are there? The Mode property can be set to a member of the BindingMode
enumeration. And the members are:
- Default
- OneWay
- OneWayToSource
- TwoWay
- OneTime
The OneWay mode is the default mode of most bindable properties and it’s used to transfer values
from source to target. The OneWayToSource mode is used to transfer values in the opposite
direction.
225
In the OneTime mode values are transferred from source to target, but only if the BindingContext
changes. We’re not going to discuss this mode in more detail.
What about TwoWay? Well, TwoWay is used to transfer values in both directions between source and
target. Let’s modify our code like this:
...
<ContentPage ...>
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout>
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value, StringFormat='Width: {0:F2}'}" />
<Slider x:Name="slider"
BindingContext="{x:Reference boxView}"
Minimum="50"
Maximum="200"
Value="{Binding WidthRequest, Mode=TwoWay}" />
<BoxView x:Name="boxView"
Color="Green"
HeightRequest="100" />
</VerticalStackLayout>
</ContentPage>
Here we have a label, a slider and a box view. The label’s Text property is bound to the slider’s
Value property. This is a OneWay binding. The data flows from source (slider) to target (label).
The slider’s Value property is bound to the box view’s WidthRequest property. This is a TwoWay
binding, which we explicitly specified, although we didn’t have to because the default binding
mode for the Slider’s Value property is TwoWay. So, we could leave it out:
<BoxView ...
226
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
As you can see, the value of the Slider’s Value property is 50, and the width of the box view is
also 50. Drag the slider in both directions and watch the label and the box view change.
Some properties, like the Slider’s Value property, are TwoWay by default. Some other properties
like that are the Date property of DatePicker, the Text property of Entry and Editor and some
more. But it doesn’t mean we can’t set a different binding mode. We can override the binding
mode. Let’s try it out by explicitly setting the binding mode of the slider’s Value property to
OneWay:
...
<ContentPage ...>
...
<Slider x:Name="slider"
...
Value="{Binding WidthRequest, Mode=OneWay}" />
<BoxView ...
Run the app again. Now the box view occupies all available width because we didn’t set the
WidthRequest property on it and if you now drag the slider and thus change the Value property,
the WidthRequest isn’t updated because the flow now is in one direction only:
Let’s change the binding mode back to TwoWay. As this binding works in both directions, we could
set it on the other object instead:
...
<ContentPage ...>
...
<Slider x:Name="slider"
Minimum="50"
Maximum="200" />
227
<BoxView x:Name="boxView"
BindingContext="{x:Reference slider}"
Color="Green"
HeightRequest="100"
WidthRequest="{Binding Value, Mode=TwoWay}" />
</VerticalStackLayout>
</ContentPage>
Here, however, we must specify the binding mode explicitly because the default mode for the
WidthRequest property of BoxView is OneWay. Now it works as before.
Great. But what if we wanted the data to be used in a different way? Let’s say, we want the box
view to be visible only if the slider’s Value is greater than 150? Or maybe we want the label to
represent the Value of the slider using integer size categories like 1 for very small values, 2 for a bit
greater values and so on, moving to the next category every 50 units? To implement scenarios like
these, we need value converters.
Value Converters
In the examples above the values that were transferred from source to target, from target to source
or in both directions were either of the same type, like the Slider’s Value and the BoxView’s
WidthRequest properties, which are both of type double, or of different types, but with one type
easily convertible to the other type using an implicit conversion, like the Value property of Slider
(double) and the Text property of Label (string). But sometimes an explicit type conversion is
necessary.
You can convert any type into a string using the StringFormat property, as we saw before. But
what about other types? Well, for other type conversions we need value converters, also known as
binding converters. These are classes that implement the IValueConverter interface. Let’s see
how to use them in practice.
So, suppose we want the label to represent the Value of the slider using integer size categories like
I mentioned before. The Value property is of type double and we need categories of type int.
What we need is a value converter that converts doubles to ints. We usually define also a method
that does the conversion in the opposite direction. We wouldn’t have to do it if we were sure that
the converter is only going to be used in OneWay bindings, but you never know. You may want to
reuse the converter in a different part of the app where a TwoWay or OneWayToSource conversion is
required, so it’s a good idea to make the converters more universal. This is why we’re always going
to define two methods in our converters, one for the conversion in one direction and the other for
the conversion in the opposite direction. These two methods are called Convert and ConvertBack
respectively.
Let’s start with our example. We’re going to set the Text property of the label. If the Value of the
slider is between 50 and 100, the width category should be 1. Category 2 will be for widths between
228
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
100 and 150, and category 3 for widths between 150 and 200. We could use just the StringFormat
property for that, but let’s also use a value converter.
We need to convert doubles to ints, so we’ll create a class named DoubleToIntConverter. To keep
things organized, let’s create a new folder in the root of our app named Converters. In this folder we
can now create our first converter, so add the aforementioned class to it and implement the two
methods in it:
using System.Globalization;
namespace Slugrace.Converters;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return (double)((int)value! * 50);
}
}
Now we must instantiate the converter in the TestPage’s resource dictionary and then reference it
by key in the XAML code. We must also remember to add the converter’s namespace using a
prefix:
...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:Slugrace.Converters"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
<converters:DoubleToIntConverter x:Key="doubleToInt" />
</ContentPage.Resources>
<VerticalStackLayout>
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value,
Converter={StaticResource doubleToInt},
StringFormat='Width Category: {0}'}" />
<Slider x:Name="slider"
...
229
As you can see, here we have both the converter and the StringFormat property. In such cases, the
converter is invoked first and the result is formatted as a string. If we now run the app and drag the
slider, the width category will change every 50 units:
The Convert method takes quite a few parameters. We only used one so far, value, which is of
type object. This is the object or value from the data-binding source, so, in our case, from the
slider’s Value property.
The method must return a value of the type of the data-binding target or a type that can be
implicitly converted to the target’s type. Here it returns an int, which is then converted using the
StringFormat property.
In the example the width category changes every 50 units. But what if we wanted to make the
converter more universal and let the user decide what value should be used. We can do it using the
parameter parameter, which is of type object, too.
Let’s modify the Convert method and then add another label and set the parameter to 50 on one
label and 10 on the other. Here’s the Convert method:
...
public class DoubleToIntConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (!double.TryParse(parameter as string, out double granularity))
{
granularity = 1;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (!double.TryParse(parameter as string, out double granularity))
{
granularity = 1;
}
...
<ContentPage ...>
...
230
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<VerticalStackLayout>
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value,
Converter={StaticResource doubleToInt},
ConverterParameter=50,
StringFormat='Width Category (granularity 50): {0}'}" />
<Slider ...
Next, let’s create another converter in the Converters folder. This time we want to make the box
view invisible if the Value property of the slider is greater than 150. To make the box view
invisible, we have to set its IsVisible property to False, which is a boolean value. So, we need a
converter that converts doubles to bools:
using System.Globalization;
namespace Slugrace.Converters;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (!double.TryParse(parameter as string, out double limit))
{
limit = 100;
}
231
...
<ContentPage ...>
<ContentPage.Resources>
...
<converters:DoubleToIntConverter x:Key="doubleToInt" />
<converters:DoubleToBoolConverter x:Key="doubleToBool" />
</ContentPage.Resources>
<VerticalStackLayout>
...
<BoxView x:Name="boxView"
...
WidthRequest="{Binding Value, Mode=TwoWay}"
IsVisible="{Binding Value,
Converter={StaticResource doubleToBool},
ConverterParameter=150}" />
</VerticalStackLayout>
</ContentPage>
If you now run the app and drag the slider, the box view will disappear and reappear depending
on the slider’s Value.
In the examples so far, we’ve been binding simple properties to other simple properties, like the
Slider’s Value property and the Label’s Text property. You know that we use the Path property
to set a binding to a particular property. The Path property is the content property of Binding and
may be omitted when it’s the first property. But we haven’t demonstrated more complex paths yet.
Binding Path
The Path property may be set to a simple property, a subproperty or to a member of a collection.
Let’s check it out:
...
<ContentPage ...
Padding="50"
x:Name="page">
...
<BoxView x:Name="boxView"
...
<Label Text="{Binding Source={x:Reference page},
Path=Content.Children[0],
StringFormat='The first child is of type: {0}.'}" />
</VerticalStackLayout>
</ContentPage>
232
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
First we specified the name of the ContentPage so that we can reference it. Then we created three
labels.
In the first label the Text property is bound to the first child of Content. Here Content is the
VerticalStackLayout. It has 7 children: 5 labels, a slider and a box view. The first child is a label.
In the second label the Text property is bound to the Length property of Text, which is a property
of the second child of Content.
In the third label the Text property is bound to the Count subproperty of Children, which itself is
a subproperty of Content.
We can bind to properties of other objects that we specify by name. But we can also use relative
bindings.
Relative Bindings
Relative bindings enable us to set the binding source relative to the position of the binding target.
We use the RelativeSource markup extension to create relative bindings. The content property of
RelativeSource is Mode. It can be set to a couple values, one of which is Self. This value is used if
we want to bind one property of an object to another property of the same object. Let’s simplify the
TestPage:
233
<VerticalStackLayout>
<BoxView x:Name="boxView"
Color="Green"
WidthRequest="120"
HeightRequest="{Binding Source={RelativeSource Self},
Path=WidthRequest}"
Rotation="{Binding Source={RelativeSource Self},
Path=WidthRequest}" />
</VerticalStackLayout>
</ContentPage>
Here the HeightRequest and Rotation properties of the box view are bound to its WidthRequest
property. If you run it, all three properties will have the same value:
Another value Mode can be set to is FindAncestor. It’s used to bind to a parent element. To use this
mode, the AncestorType property must be set to a type. If the type derives from Element, Mode
will be implicitly set to FindAncestor. Let’s again rewrite the code in the TestPage:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout Rotation="5">
<VerticalStackLayout Rotation="-15">
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type VerticalStackLayout}},
Path=Rotation,
StringFormat='inner layout is rotated {0} degrees' }" />
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type VerticalStackLayout},
AncestorLevel=2},
Path=Rotation,
StringFormat='outer layout is rotated {0} degrees' }" />
</VerticalStackLayout>
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}},
Path=Title}" />
</VerticalStackLayout>
</ContentPage>
Here we didn’t even give names to the elements because we’ll be binding to their properties by
type, not by name. Let’s see the result first and then discuss it:
234
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The first label’s Text property is bound to an ancestor of type VerticalStackLayout. This label
has a direct parent of this type and an indirect one, which is its grandparent. By default it binds its
Text property to the direct parent, which is the VerticalStackLayout with Rotation set to -15.
The third label is bound to an ancestor of type ContentPage. There’s only one ancestor of this type.
So far, all the bindings have worked correctly, but what if a binding fails or returns a null value?
Let’s have a look at such situations next.
Binding Fallbacks
Sometimes we try to bind to a property that doesn’t exist. How to handle this? Let’s rewrite our
code in the TestPage like so:
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout>
<Label x:Name="label1"
Text="A Label has the Text property." />
<Slider x:Name="slider" />
<Label BindingContext="{x:Reference label1}"
Text="{Binding Text}" />
<Label BindingContext="{x:Reference slider}"
Text="{Binding Text, FallbackValue='Text property not defined'}" />
</VerticalStackLayout>
</ContentPage>
235
Here we have three labels and a slider. The last label’s Text property is bound to the slider’s Text
property, which… doesn’t exist. We can use the FallbackValue property to specify what value
should be used instead. If we didn’t specify this property, the last label’s Text property would be
an empty string.
If we now run the app, we’ll see the fallback value in the last label:
This example is pretty artificial, because we know that a Slider doesn’t have the Text property
and we just don’t bind to it. A more realistic example would be binding to properties of
heterogeneous objects in a collection where the given property may not exist on all objects.
There’s just one more topic for now as far as data binding is concerned that I’d like to discuss,
multi-bindings.
Multi-bindings
Multi-bindings are the way to go if you want to attach multiple Binding objects to one target
property. Suppose we want to create a page with an entry, a stepper, a slider and a button (plus
some labels with info about the main views) and the button should be enabled only if the entry isn’t
empty, and the Value properties of the stepper and the slider are greater than zero. These three
conditions must be met simultaneously for the button to be enabled.
A multi-binding is created by instantiating the MultiBinding class. The class has a Bindings
property that is a collection of the individual bindings. Bindings is the class’s content property, so
we don’t have to use it explicitly in XAML. We also must provide an IMultiValueConverter
instance that evaluates all the Binding objects inside the Bindings collection and returns a single
value that can be used by the target property.
In our case we have a Bindings collection of three Binding objects. The target property is the
IsEnabled property of the button, which is of type boolean. The three source properties are:
- the entry’s Text property’s Length property (if it’s zero, the entry is empty),
236
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
So, we must create a converter that evaluates multiple double values and returns a boolean value.
Let’s create an AllGreaterThanZeroMultiConverter class in the Converters folder. Its name is
pretty self-explanatory. It’s supposed to return true only if all source properties are set to a value
greater than 0. Here’s the code:
using System.Globalization;
namespace Slugrace.Converters;
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The class implements the IMultiValueConverter interface. We’re not going to implement the
ConvertBack method, because we don’t need conversions in the opposite direction. Let’s have a
closer look at the Convert method, though.
In the first step we check whether the values array, which in our case contains the three values
from the source properties, is not null. We also check if a boolean value can be assigned to the
target type. If one or both of these conditions are met, the method returns false. In our case,
however, the values array will contain three values and the target IsEnabled property can be
assigned a boolean value, so the next step can begin.
237
In the next step we check, in a loop, whether all values that we delivered are of type double or int,
and if so, whether they’re less or equal to zero. If any of the values is not a double or int, or if any of
the values is less than or equal to 0, the method returns false. Otherwise, so if all values are
positive doubles or ints, the method returns true.
With the converter in place, let’s replace the code in TestPage.xaml by the following:
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
<converters:AllGreaterThanZeroMultiConverter x:Key="AllGreaterThanZeroConverter" />
</ContentPage.Resources>
<VerticalStackLayout>
<Label Text="At least 1 character" />
<Entry x:Name="entry" WidthRequest="100" Margin="0, 0, 0, 20" />
<Button Text="Proceed">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AllGreaterThanZeroConverter}">
<Binding Source="{x:Reference entry}" Path="Text.Length" />
<Binding Source="{x:Reference stepper}" Path="Value" />
<Binding Source="{x:Reference slider}" Path="Value" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</VerticalStackLayout>
</ContentPage>
We instantiated the converter in the resource dictionary and referenced it to set the
MultiBinding.Converter property. If we now run the app, the entry will be empty (which it is by
default) and the two other controls will have the default value of the Value property, which is 0.
The button should be disabled:
238
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
If you now start playing with the three controls, the button will remain disabled as long as at least
one of the source properties is less than or equal to zero:
But if all three values are greater than zero, the button will be enabled:
It will remain enabled until any of the three values is again set to 0. Try it out.
That’s it, as far as data binding is concerned, at least for now. We’ve covered the basics, but where
data binding really shines is in an app where the MVVM pattern is implemented. Our Slugrace
application is going to be such an app, so let’s start implementing the pattern now.
239
Chapter 16 – Introduction to MVVM
code: https://github.com/prospero-apps/Slugrace/tree/chapter16
As I mentioned in the previous chapter, we’re going to structure our application using the MVVM
pattern. This pattern is often used in XAML-based frameworks like WPF or Xamarin.Forms. It’s
also used in .NET MAUI.
VM - ViewModel (used like a code-behind, but completely decoupled, to provide data for the
View).
We’re going to talk about all these elements in this book. But let’s take it slowly.
Before we create any models and view models, let’s have a look at how MVVM actually works.
240
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The model knows nothing about the view model and the view model knows nothing about the
view. The view model separates the other two components so that the model and the view can be
developed independently.
What about the solid arrows? They indicate that there is some data flow in the opposite direction as
well. In case of TwoWay binding or commands (we’re going to talk about commands soon), data
flows from the view to the view model and the view model may update the model.
The model represents the app’s domain model. It contains classes that represent data models and
contain business logic.
The view is responsible for the visual representation of data. It defines the structure of a page. It
also may contain some logic in the code-behind, although this should be limited to visual
behaviors, not the business logic of the app.
The view model implements properties and commands the view can bind to. It also notifies the
view if any property changes so that it can update itself. The view model prepares data it takes
from the model for the view to consume. It sometimes requires data conversions.
MVVM is pretty flexible. It means you don’t need a model for each and every view model and you
don’t need a separate view model for each view. A view model may use multiple models and each
model can be used in multiple view models. If all your view needs is a string, you can just define
the string in the view model - there’s no need to create a model class for that. Also, if more than one
view requires the same data, they can share one view model. Besides, if a view only displays static
data that doesn’t require any data binding, there’s no need to create a view model for this view.
241
In our app, we’re going to be using the .NET Community MVVM Toolkit framework. There are
also some other MVVM frameworks. They are used to facilitate our work by, for example,
simplifying the implementation of property change notification or handling commands, navigation,
dependency injection, etc. But before we start using the toolkit framework, let’s have a look at a
basic implementation of the pattern, without any frameworks. This way you will know what is
going on behind the scenes and you will be even more grateful for what the framework does for
you when we start using it.
Basic MVVM
So, let’s practice the basic, traditional approach on the TestPage. The TestPage is already in the
Views folder and it’s going to be our view. Let’s simplify the code in the TestPage.xaml file like so:
<VerticalStackLayout>
<Label Text="What is your favorite color?" />
<Entry WidthRequest="100" />
<Label Text="" />
</VerticalStackLayout>
</ContentPage>
Here we have an entry and two labels. We don’t actually need a model, but let’s create a view
model that will contain the data our TestPage requires. It definitely requires a string the second
label will bind to. In case of the entry there’s two-way binding, so the entry’s Text property will
both consume the string from the view model and update it there.
namespace Slugrace.ViewModels;
The view model is going to contain some data and also notify the view when this data changes. We
can achieve that by implementing the INotifyPropertyChanged interface:
242
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
using System.ComponentModel;
namespace Slugrace.ViewModels;
As you can see, the interface provides an event. Let’s create a method that will invoke the event
when a property changes:
...
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
To see how it works, let’s create the FavoriteColor property of type string that the entry in the
view will bind to. We’ll also create a backing field for it. The getter will just return the value of the
backing field. The setter will first check if the value really has changed and if so, it will set the new
value and call the OnPropertyChanged method. The method takes the name of the property as an
argument:
...
public class TestViewModel : INotifyPropertyChanged
{
string favoriteColor = string.Empty;
243
We can simplify the code slightly by using the CallerMemberName attribute. Then we won’t have
to pass the name of the property to the OnPropertyChanged method:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Slugrace.ViewModels;
The CallerMemberName attribute can only be applied to parameters with default values, so we set
the propertyName parameter to null. As you can see, we can now call the OnPropertyChanged
method without the name of the property. This may be useful if there are many properties in the
class.
The OnPropertyChange method will notify the view of the change and the entry’s text will be
automatically updated. And vice versa, if we set the Text property in the entry, it will be
automatically updated in the view model due to its two-way binding mode.
But first, we must set the binding context of the view to the view model and bind the entry’s Text
property to the FavoriteColor property defined in the view model:
...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.BindingContext>
<viewmodels:TestViewModel />
</ContentPage.BindingContext>
244
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<VerticalStackLayout>
<Label Text="What is your favorite color?" />
<Entry WidthRequest="100" Text="{Binding FavoriteColor}" />
<Label Text="{Binding FavoriteColor}" />
</VerticalStackLayout>
</ContentPage>
Sometimes one property in the view model depends on another property. Then, when one of them
changes, the other should change too. Let’s modify our view model:
...
public class TestViewModel : INotifyPropertyChanged
{
string favoriteColor;
Here we have the LetterCount property that simply returns the length of the string that we enter
in the entry. This property will change whenever the FavoriteColor property changes. We can
achieve this by calling the OnPropertyChange method as many times as there are properties that
should be affected. In our case it means twice. We don’t have to pass the name of the property on
which the method is called, but we have to provide the names of other properties, which is why we
pass LetterCount to it.
245
Now let’s add another label in the view and bind it to the newly created property:
...
<ContentPage ...>
...
<VerticalStackLayout>
...
<Label Text="{Binding FavoriteColor}" />
<Label Text="{Binding LetterCount}" />
</VerticalStackLayout>
</ContentPage>
If we now enter some text in the entry, the label will display
its length.
Let’s say we want to add a button to our view, which, when pressed, will set the FavoriteColor
property to a fixed value, like for instance “red”. The problem is, we can’t use events anymore.
Events work in the code-behind and we now put the logic in the view model. So, there must be
another way to do it. And there is - commands.
Commands
Let’s start by creating a public property of type ICommand in the view model. This is the property
we can bind to. Then, in the constructor, let’s create a new Command instance and pass to it the
method that we want to be invoked when the button is pressed. We don’t have the method yet, so
let’s create it. The method is named UseFixedColor and it just sets the FavoriteColor property to
a fixed value. Here’s the code:
...
public class TestViewModel : INotifyPropertyChanged
{
...
public int? LetterCount => FavoriteColor?.Length;
public TestViewModel()
{
UseColorCommand = new Command(UseFixedColor);
}
void OnPropertyChanged...
}
246
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Now we have to create the button in the view and bind its Command property to the property we
just created in the view model:
...
<ContentPage ...>
...
<VerticalStackLayout HorizontalOptions="Start">
...
<Label Text="{Binding LetterCount}" />
<Button
Text="RED"
Command="{Binding UseColorCommand}" />
</VerticalStackLayout>
</ContentPage>
That’s it. If you now run the app and press the
button, the FavoriteColor property will change
to “red”. This change will be immediately
reflected in all controls in the view that bind to
FavoriteColor.
...
<ContentPage ...>
...
<VerticalStackLayout HorizontalOptions="Start">
...
<Button
Text="RED"
Command="{Binding UseColorCommand}"
CommandParameter="red" />
<Button
Text="BLUE"
Command="{Binding UseColorCommand}"
CommandParameter="blue" />
<Button
Text="YELLOW"
Command="{Binding UseColorCommand}"
CommandParameter="yellow" />
</VerticalStackLayout>
</ContentPage>
As now the method takes a parameter, we have to use the generic version of Command in the view
model with the type corresponding to the type of the parameter. Let’s modify the code like this:
247
...
public class TestViewModel : INotifyPropertyChanged
{
...
public TestViewModel()
{
UseColorCommand = new Command<string>(UseFixedColor);
}
...
}
Binding Context
We set the binding context in XAML, but we
often do it in the code-behind using dependency injection. In the TestPage.xaml.cs file we’ll set the
binding context in the constructor:
using Slugrace.ViewModels;
namespace Slugrace.Views;
public partial class TestPage : ContentPage
{
public TestPage(TestViewModel testViewModel)
{
InitializeComponent();
BindingContext = testViewModel;
}
}
We’re using here an instance of TestViewModel through dependency injection. We need to register
the page and the view model with the dependency service in the MauiProgram.cs file:
using Microsoft.Extensions.Logging;
using Slugrace.Views;
using Slugrace.ViewModels;
248
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
namespace Slugrace;
builder.Services.AddSingleton<TestPage>();
builder.Services.AddSingleton<TestViewModel>();
return builder.Build();
}
}
We’re using the AddSingleton method here, which means the same instance will be used
throughout the application. If we used AddTransient instead, a new instance of TestPage and
TestViewModel would be delivered whenever requested. It’s important to register the classes for
dependency injection before the Build method on builder is called.
Finally, remove the part of the markup in the TestPage.xaml file where the binding context was set. I
mean the following part:
<ContentPage.BindingContext>
<viewmodels:TestViewModel />
</ContentPage.BindingContext>
We don’t need it anymore because now the binding context is set in the code-behind.
The app works as before, but we have a problem with Intellisense in the XAML file. It looks like the
properties defined in the view model are not listed in it. We can fix it by setting the x:DataType
property on the page to the view model class:
...
<ContentPage ...
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:TestViewModel"
x:Class="Slugrace.Views.TestPage"
...
This is it as far as the basics of MVVM are concerned. We could implement our Slugrace
application in the same way - just add view models and models to the already existing views.
However, if you look at the TestViewModel class, you will see that we need quite a lot of code to
249
implement a single property. We also have to implement the INotifyPropertyChanged interface
for each view model.
We could simplify it a bit by creating a base view model where the interface would be
implemented and then deriving the other view models from this base class. But there is even a
better solution. We can use a framework that will do some of the tedious work for us. I already
mentioned the .NET Community MVVM Toolkit framework that we are going to use. It relies on
source generators, so it will generate optimized C# code with all the required functionality for you.
We’re going to implement it in the next chapter. We’ll first use it for the TestPage so that you can
see the difference and then we’ll start implementing it for the actual application.
250
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the previous chapter we introduced the MVVM pattern in its traditional form. As you
remember, there are three components, the model, the view, and the view model. The view model
delivers data to the view, so whenever a property defined in the view model changes, the view is
notified of this change and updates itself. With two-way bindings it works also in the opposite
direction. The model, on the other hand, contains the domain classes that are used by the view
model and is not required at all if only simple data is supposed to be displayed.
We used the TestPage as our view and we created the TestViewModel as the view model. We only
defined two properties in the view model and we implemented the INotifyPropertyChanged
interface. One of the properties we defined is a one-liner that depends on the other property, but
even so there’s quite a lot of typing. We’re going to have much more properties in our app, so let’s
make our lives easier by using an MVVM framework that will do some of the tedious work for us.
In this book, we’ll be using the MVVM Toolkit, which is part of the .NET Community Toolkit. It
uses source generators to generate optimized C# code that is additive to our own code. This just
means that the view model class we create consists of two parts - the code we write ourselves and
the code generated by the source generators. As such, the class must be marked as partial.
So, without further ado, let’s jump right in and install the framework.
If you open the project file (by double-clicking on the project name), you will see the toolkit there:
<Project Sdk="Microsoft.NET.Sdk">
...
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
...
</ItemGroup>
...
251
Naturally, depending on when you install it, you may end up with a different version of the
package.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace Slugrace.ViewModels;
public TestViewModel()
{
UseColorCommand = new Command<string>(UseFixedColor);
}
We need 13 lines of code to implement the FavoriteColor property. But not if we use the MVVM
Toolkit. Let’s actually implement the framework and see the difference.
252
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
So, first let’s mark the class as partial and make it inherit from ObservableObject. Also, we
don’t need to implement the INotifyPropertyChanged interface anymore because it will be
implemented for us by the ObservableObject. If you right-click on it and select Go To Definition,
you’ll see it implements the interface:
using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace Slugrace.ViewModels;
...
public partial class TestViewModel : ObservableObject
{
[ObservableProperty]
string favoriteColor;
...
}
We’re not defining the FavoriteColor property anymore, and yet, there’s no error in the code
below where we define the LetterCount property, which in turn uses the FavoriteColor
property. This is because the public FavoriteColor property is generated for us behind the scenes
and its name is automatically capitalized, so we can use it as if we had defined it ourselves.
But don’t take my word for it. You can see the generated code.
Go to Dependencies, select one of the platforms, for instance Android, and under:
Analyzers
→ CommunityToolkit.Mvvm.SourceGenerators
→ CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
253
you’ll find the Slugrace.ViewModels.TestViewModel.g.cs generated file:
Open the file and you will see that the public property is indeed created for us:
// <auto-generated/>
...
namespace Slugrace.ViewModels
{
/// <inheritdoc/>
partial class TestViewModel
{
...
public string FavoriteColor
{
...
}
...
254
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
But, as you can see, the length of the string is not displayed anymore, even though we defined the
LetterCount property ourselves. This is because, as you may remember, the original version of the
FavoriteColor property called the OnPropertyChanged method not only on itself, but also on the
LetterCount property. This is now gone. But don’t worry, we just have to tell the generator to
generate this functionality for us. To this end, we have to add another attribute to the backing field,
NotifyPropertyChangedFor, and specify the name of the property we want it to work for:
...
public partial class TestViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(LetterCount))]
string favoriteColor = string.Empty;
We’re using commands to call methods defined in the view model. These are built-in .NET MAUI
commands. But we’re going to use MVVM Toolkit commands instead.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Slugrace.ViewModels;
[RelayCommand]
void UseFixedColor(string color)
{
FavoriteColor = color;
}
}
255
If you open the file with the generated code (the file ends with RelayCommandGenerator), you’ll
see the command has been generated for us:
// <auto-generated/>
...
namespace Slugrace.ViewModels
{
/// <inheritdoc/>
partial class TestViewModel
{
...
public global::CommunityToolkit.Mvvm.Input.IRelayCommand<string> UseFixedColorCommand =>
useFixedColorCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand<string>(new
global::System.Action<string>(UseFixedColor));
}
}
As you can see, the command is named after the method. We have to use this name,
UseFixedColorCommand, in the view:
...
<ContentPage ...>
...
<Button
Text="RED"
Command="{Binding UseFixedColorCommand}"
CommandParameter="red" />
<Button
Text="BLUE"
Command="{Binding UseFixedColorCommand}"
CommandParameter="blue" />
<Button
Text="YELLOW"
Command="{Binding UseFixedColorCommand}"
CommandParameter="yellow" />
...
The code in the view model is now much more concise. And, what’s important, the app works just
like before. Try it out.
Now, we’ve covered the most important stuff related to the MVVM Toolkit. Naturally, there’s
much more to it, but this is enough to start implementing it in our application. We’ll be creating the
models and view models progressively, adding properties and command as we proceed and as we
need them. And we’ll be modifying the views accordingly. Let’s start by adding some models.
The Player class will contain properties and methods related to the player, so a human being
playing the game. Properties like Name, InitialMoney, CurrentMoney, etc. probably come to your
mind. A player must also be able to place a specified amount of money on a slug, etc.
256
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The Slug class will contain properties and methods related to the slug. Each slug needs a name,
runs at a specified speed, can win or lose a race.
These are just some examples. In fact, our models are going to need more properties and methods
as we proceed, but we’ll be updating the models as we go. For now, let’s create the three classes in
the Models folder and add some basic properties to them.
namespace Slugrace.Models;
For now, we just have a bunch of properties with self-explanatory names. The Slug.cs file looks like
this:
namespace Slugrace.Models;
Yes, I know, this isn’t much, but don’t worry, we’ll add quite a few properties and methods in this
class. All in due time. And here’s the Game.cs file:
namespace Slugrace.Models;
257
We’re keeping it simple for now. I defined an enumeration here that will be used only in this class
and later in the view model. By default the game is supposed to end when there’s only one player
with any money left or when there’s none.
Let’s now move on to views and view models. When we were creating the pages, we included
content views in some of them. We’ll create a separate view model for the PlayerSettings content
view, but generally we’ll create a separate view model for each page. Actually, some of the pages
will share the same view model. Anyway, let’s start with the PlayerSettings content view.
Anyway, PlayerSettings will be our view. Let’s now create a view model for the view to bind to.
In the ViewModels folder create a new class and name it PlayerSettingsViewModel. Here’s the
code:
using CommunityToolkit.Mvvm.ComponentModel;
using Slugrace.Models;
namespace Slugrace.ViewModels;
258
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
OnPropertyChanged(nameof(NameIsValid));
OnPropertyChanged(nameof(PlayerIsValid));
}
}
}
public PlayerSettingsViewModel()
{
player = new Player();
}
}
259
The Player model is stored as a private field. We instantiate a Player object in the constructor and
use its properties inside the view model’s properties.
We don’t have any methods here, just a bunch of properties. We also defined some constants that
are used by the properties.
The PlayerId property is an integer number. It will be set to 1, 2, 3 or 4 because there are going to
be up to four players.
The PlayerName property, if changed, will also notify of changes in other properties where it’s
used. This way, not only all bindings to PlayerName will be updated, but also bindings to
NameIsValid and PlayerIsValid. Later, it will also send a message to SettingsViewModel
(which we are going to create soon) to make sure the other view model knows about this change.
PlayerInitialMoney is very similar. If changed, it will notify of the change, as well as of the
changes in two other properties, and it will sent a message (yet to be implemented) to
SettingsViewModel.
PlayerIsInGame will be used to mark each potential player as taking or not taking part in the
game. So, for example, if we decide that only two players should play in the game, the first two
players will have this property set to true, and the other two to false.
There are two properties used to ensure the PlayerName and PlayerInitialMoney properties are
valid. The first one is NameIsValid. We don’t have to set the name. If we don’t, the name will be
later set to a generic one like Player 1 or Player 2. If we decide to set the name, it shouldn’t be too
long. The other one is InitialMoneyIsValid. In this property we’re using a method defined in the
Helpers class. It will be also used in other classes in the app. I implemented the
Helpers.ValueIsInRange method like so:
...
public static class Helpers
{
...
public static bool ValueIsInRange(int value, int min, int max) => value >= min && value <= max;
}
Finally, the PlayerIsValid property will be set to true in two cases: if the player is not in the
game (like players 3 and 4 in the example above) and if they are in the game and have a valid name
and initial money. This property will be used to make sure the button in the SettingsPage is
enabled only if all players have valid data. This doesn’t have to be true about players who will not
take part in the game because they will not be used in the following pages in the app.
Now we can set the view model as the binding context of the view and bind to its properties.
Here’s the modified PlayerSettings content view:
260
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:PlayerSettingsViewModel"
IsVisible="{Binding PlayerIsInGame}"
x:Class="Slugrace.Controls.PlayerSettings">
<ContentView.BindingContext>
<viewmodels:PlayerSettingsViewModel />
</ContentView.BindingContext>
<ContentView.Resources>
...
<Grid
RowDefinitions="*"
Margin="0, 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 150, Android=80}" />
<ColumnDefinition Width="{OnPlatform 3*, Android=2.5*}" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Entry
x:Name="nameEntry"
Grid.Column="1"
WidthRequest="{OnPlatform 300, Android=130}"
Text="{Binding PlayerName}"
TextChanged="OnNameTextChanged">
</Entry>
<Label
Grid.Column="2"
Text="$" />
<Entry
x:Name="initialMoneyEntry"
Placeholder="1000"
Grid.Column="3"
WidthRequest="{OnPlatform 250, Android=100}"
Keyboard="Numeric"
Text="{Binding PlayerInitialMoney}"
TextChanged="OnInitialMoneyTextChanged">
</Entry>
</Grid>
</ContentView>
In MVVM we usually tend to leave as little code in the code-behind as possible. Ideally, we should
only initialize the component and set the binding context. In our case we don’t have to do the latter,
because we set the binding context in XAML. All or most of the logic should go to the view model.
But there’s one exception to this rule, which I already mentioned before. We should keep in the
code-behind all the logic that is responsible for the visual aspect of the view. Here we want to set
261
different visual states on the entries depending on the text entered, so we’ll leave the two
TextChanged events and handle them in the PlayerSettings.xaml.cs file:
using Slugrace.ViewModels;
namespace Slugrace.Controls;
Now we’re using the properties defined in the view model to set the entries’ visual state. We’re also
using a static method, HandleNumericEntryState, that I defined in the Helpers class:
...
public static class Helpers
{
...
public static bool ValueIsInRange(...
if (entry != null)
{
bool isEmpty = entry.Text == string.Empty;
if (isEmpty)
{
visualState = "Empty";
}
VisualStateManager.GoToState(entry, visualState);
}
}
}
262
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The code-behind file is pretty short and it takes care just of the visual aspect of the view. But we are
going to add some interesting new elements to the XAML file. We’ll add a converter and a
behavior. You know what converters are, but behaviors are probably not so familiar to you.
Anyway, let’s have a look at both.
ZeroToEmptyStringConverter
In some entries we’ll be entering text, like for example in the nameEntry. In others, like in the
initialMoneyEntry (but not only) we’ll be entering numbers. We want the entry to be empty if
the value is zero. So, if you see nothing except the placeholder text in the entry, it means the value
is zero, not null. We’ll need a converter for that. In the Converters folder add a new class and name
it ZeroToEmptyStringConverter. Here’s the code:
using System.Globalization;
namespace Slugrace.Converters;
if (enteredNumber == 0)
{
return string.Empty;
}
else
{
return enteredNumber.ToString();
}
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
string enteredValue = value?.ToString() ?? string.Empty;
if (string.IsNullOrEmpty(enteredValue))
{
return 0;
}
else
{
bool isNumber = int.TryParse(enteredValue, out int number);
if (isNumber)
{
return number;
}
else
{
enteredValue = enteredValue[..^1];
return enteredValue.Length == 0 ? 0 : int.Parse(enteredValue);
}
}
}
}
263
The converter is used in a two-way binding, so we must implement both methods. And here’s how
the converter is used in the view:
...
<ContentView ...
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
xmlns:converters="clr-namespace:Slugrace.Converters"
x:DataType="viewmodels:PlayerSettingsViewModel"
...
<ContentView.Resources>
<Style TargetType="Label"...
<converters:ZeroToEmptyStringConverter x:Key="zeroToEmptyConverter" />
</ContentView.Resources>
...
<Entry
x:Name="initialMoneyEntry"
...
Text="{Binding PlayerInitialMoney, Converter={StaticResource zeroToEmptyConverter}}"
TextChanged="OnInitialMoneyTextChanged">
...
So, we must add the namespace first. Then we add the converter to the content view’s resources
and reference it in the binding.
And now it’s time for something new, a behavior. What exactly is a behavior? We’re about to create
one on the initialMoneyEntry.
Behaviors
Behaviors are a nice feature in .NET MAUI that you can use to extend your control classes.
Normally, to extend a class, you inherit from it and add some functionality in the derived class.
With behaviors it’s not necessary. When you attach a behavior to a control, the functionality
defined in it acts as if it was part of the control itself.
We could implement additional functionality in the code-behind as well. But then it would be
coupled with that particular view. As we want it to be reusable, we’ll implement it as a behavior
that we can then attach to other controls.
The functionality that we want to add is ensuring that only numeric characters can be entered in
the entry. So, if you try to type any other character, like a letter or a special character, this character
will not show up.
We wouldn’t have to implement this functionality if our app was deployed only to Android,
because we set the Keyboard property to Numeric. This will prevent you from entering other
characters. But it doesn’t work on Windows, where you can still enter anything.
So, let’s create a new folder and name it Behaviors. In the folder let’s create a new class and name it
NumericInputBehavior. Here’s the code:
264
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
namespace Slugrace.Behaviors;
if (!string.IsNullOrEmpty(e.NewTextValue))
{
bool isNumeric = int.TryParse(e.NewTextValue, out int value);
if (!isNumeric)
{
entry.Text = e.OldTextValue;
}
}
}
}
}
As you can see, the behavior is meant to be attached to Entry controls. In particular, it’s added to
the TextChanged event. The implementation is simple. If the last character you entered is not a
number, the Text property of the entry is set to the old text, so the text before you typed that
character.
And, once again, let’s have a look at how this behavior is added to the initialMoneyEntry inside
the PlayerSettings.xaml file:
...
<ContentView ...
xmlns:converters="clr-namespace:Slugrace.Converters"
xmlns:behaviors="clr-namespace:Slugrace.Behaviors"
x:DataType="viewmodels:PlayerSettingsViewModel"
...
<Entry
x:Name="initialMoneyEntry"
...
TextChanged="OnInitialMoneyTextChanged">
<Entry.Behaviors>
<behaviors:NumericInputBehavior />
</Entry.Behaviors>
</Entry>
...
265
You won’t be able to type non-numeric characters anymore.
The PlayerSettings view is embedded in the SettingsPage. So, let’s take care of this view and
create a corresponding view model.
SettingsViewModel
We’ll need a view model for the SettingsPage (which is a view). Go to the ViewModels folder and
create a new class. Name it SettingsViewModel.
Let’s start by registering the page and the view model with the dependency service in the
MauiProgram class, just like we did with the TestPage:
...
public static class MauiProgram
...
builder.Services.AddSingleton<TestPage>();
builder.Services.AddSingleton <TestViewModel>();
builder.Services.AddTransient<SettingsPage>();
builder.Services.AddTransient<SettingsViewModel>();
return builder.Build();
...
This time we’re adding them as transient. Now we can set the binding context in the code-behind
using dependency injection. Here’s the SettingsPage.xaml.cs file:
using Slugrace.ViewModels;
namespace Slugrace.Views;
namespace Slugrace.ViewModels;
266
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AllSettingsAreValid))]
private ObservableCollection<PlayerSettingsViewModel> players;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AllSettingsAreValid))]
[NotifyPropertyChangedFor(nameof(OnlyOnePlayer))]
[NotifyPropertyChangedFor(nameof(RacesEndingConditionSet))]
private int currentNumberOfPlayers = 2;
267
public bool RacesEndingConditionSet
=> (CurrentNumberOfPlayers == 1 && GameEndingCondition != EndingCondition.Time)
|| GameEndingCondition == EndingCondition.Races;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AllSettingsAreValid))]
private string? changedPlayerName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AllSettingsAreValid))]
private int? changedPlayerInitialMoney;
public SettingsViewModel()
{
game = new Game();
Players =
[
new() { PlayerId = 1, PlayerIsInGame = true },
new() { PlayerId = 2, PlayerIsInGame = true },
new() { PlayerId = 3, PlayerIsInGame = false },
new() { PlayerId = 4, PlayerIsInGame = false }
];
}
[RelayCommand]
void CreatePlayerList(int numberOfPlayers)
{
for (int i = 0; i < Players.Count; i++)
{
Players[i].PlayerIsInGame = i < numberOfPlayers;
}
CurrentNumberOfPlayers = numberOfPlayers;
if (OnlyOnePlayer)
{
GameEndingCondition = EndingCondition.Races;
}
}
[RelayCommand]
void SetEndingCondition(string condition)
{
GameEndingCondition = condition switch
{
"money" => EndingCondition.Money,
268
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
There are a couple constants, a couple observable properties and a couple regular properties. There
are a couple methods, too.
At the beginning of the file, we define a private game variable that we set to a new instance of Game
in the constructor. We also define the observable collection called players, which we populate in
the constructor, too. There are going to be up to four players in the game, so we need four instances
of PlayerSettingsViewModel to allow the user to set the names and initial money of the players.
Here, in the constructor, we set the PlayerId property of each player, which will be used to
display the player’s generic name (Player 1, Player 2, etc.). We also set the PlayerIsInGame
property of the first two players to true, and of the other two players to false. This is because we
assume the game is by default for two players, which can be naturally changed in the
SettingsPage.
As we’re at the constructor, we’re going to register two messages here. We’ll be talking about the
messaging system in a moment.
And now let’s have a look at the properties that we have in the view model.
The MaxRacesIsValid and MaxTimeIsValid are responsible for ensuring that the user of the app
sets the number of races or the time of the game, depending on which ending condition is chosen,
to a value within a certain range.
The currentNumberOfPlayers observable property is an important one. Look how many change
notifications are involved with it. Its value will be set by the radio buttons in the upper part of the
SettingsPage.
The RacesEndingConditionSet property will be used to ensure that if there is only one player
selected and the first ending condition (Money) is unavailable (because it wouldn’t make sense to
end the game when there’s only one player with any money left in this situation), the default
ending condition will be set to Races.
The last property is AllSettingsAreValid. There are a couple conditions defined in it to ensure
that that everything is set to a valid value in the SettingsPage. Only if this property is true, will
the Ready button be enabled.
Next, we have the CreatePlayerList method that will handle the players when we check one of
the radio buttons. It will set the players’ PlayerIsInGame property and the
269
CurrentNumberOfPlayers property. If the one-player game mode is selected and the current
ending condition is Money, it will be changed to Races, because Money is unavailable in this mode.
Finally, the SetEndingCondition method will do exactly what its name suggests.
We have the view model, so let’s take care of the view. The SettingsPage.xaml file is pretty lengthy,
but we’re going to discuss it piece by piece.
SettingsPage View
So, beware! Here comes the SettingsPage:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:controls="clr-namespace:Slugrace.Controls"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
xmlns:behaviors="clr-namespace:Slugrace.Behaviors"
xmlns:converters="clr-namespace:Slugrace.Converters"
x:DataType="viewmodels:SettingsViewModel"
x:Class="Slugrace.Views.SettingsPage">
<ContentPage.Resources>
<x:Double x:Key="entryWidth">300</x:Double>
<x:Double x:Key="invisible">0</x:Double>
<converters:ZeroToEmptyStringConverter x:Key="zeroToEmptyConverter" />
<toolkit:InvertedBoolConverter x:Key="invertedBoolConverter" />
</ContentPage.Resources>
...
<!--the Players panel-->
...
<HorizontalStackLayout>
<RadioButton
...
<On Platform="Android" Value="1" />
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding CreatePlayerListCommand}">
<toolkit:EventToCommandBehavior.CommandParameter>
<x:Int32>1</x:Int32>
</toolkit:EventToCommandBehavior.CommandParameter>
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
</RadioButton>
<RadioButton
x:Name="players2"
IsChecked="True"
GroupName="players">
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding CreatePlayerListCommand}">
<toolkit:EventToCommandBehavior.CommandParameter>
<x:Int32>2</x:Int32>
</toolkit:EventToCommandBehavior.CommandParameter>
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
</RadioButton>
<RadioButton
x:Name="players3"
GroupName="players">
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding CreatePlayerListCommand}">
<toolkit:EventToCommandBehavior.CommandParameter>
<x:Int32>3</x:Int32>
</toolkit:EventToCommandBehavior.CommandParameter>
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
</RadioButton>
270
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<RadioButton
x:Name="players4"
GroupName="players">
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding CreatePlayerListCommand}">
<toolkit:EventToCommandBehavior.CommandParameter>
<x:Int32>4</x:Int32>
</toolkit:EventToCommandBehavior.CommandParameter>
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
</RadioButton>
</HorizontalStackLayout>
<Grid
...
<VerticalStackLayout>
<controls:PlayerSettings>
<controls:PlayerSettings.BindingContext>
<Binding Path="Players[0]" />
</controls:PlayerSettings.BindingContext>
</controls:PlayerSettings>
<controls:PlayerSettings>
<controls:PlayerSettings.BindingContext>
<Binding Path="Players[1]" />
</controls:PlayerSettings.BindingContext>
</controls:PlayerSettings>
<controls:PlayerSettings>
<controls:PlayerSettings.BindingContext>
<Binding Path="Players[2]" />
</controls:PlayerSettings.BindingContext>
</controls:PlayerSettings>
<controls:PlayerSettings>
<controls:PlayerSettings.BindingContext>
<Binding Path="Players[3]" />
</controls:PlayerSettings.BindingContext>
</controls:PlayerSettings>
</VerticalStackLayout>
...
<!--the Ending Conditions panel-->
...
<Grid
RowDefinitions="*, *, *"
ColumnDefinitions="4*, 2*">
<RadioButton
IsChecked="True"
IsVisible="{Binding OnlyOnePlayer, Converter={StaticResource invertedBoolConverter}}"
GroupName="endingConditions">
...
<On Platform="Android" Value="last player left" />
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding SetEndingConditionCommand}"
CommandParameter="money">
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
</RadioButton>
<RadioButton
Grid.Row="1"
IsChecked="{Binding RacesEndingConditionSet}"
GroupName="endingConditions">
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding SetEndingConditionCommand}"
CommandParameter="races">
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
<VisualStateManager.VisualStateGroups>
...
</RadioButton>
<Entry
x:Name="maxRacesEntry"
...
IsEnabled="False"
Text="{Binding NumberOfRacesSet, Converter={StaticResource zeroToEmptyConverter}}"
TextChanged="OnMaxRacesTextChanged">
...
</Entry.Placeholder>
<Entry.Behaviors>
<behaviors:NumericInputBehavior />
</Entry.Behaviors>
</Entry>
271
<RadioButton
Grid.Row="2"
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding SetEndingConditionCommand}"
CommandParameter="time">
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
<VisualStateManager.VisualStateGroups>
...
</RadioButton>
<Entry
x:Name="maxTimeEntry"
...
IsEnabled="False"
Text="{Binding GameTimeSet, Converter={StaticResource zeroToEmptyConverter}}"
TextChanged="OnMaxTimeTextChanged">
...
</Entry.Placeholder>
<Entry.Behaviors>
<behaviors:NumericInputBehavior />
</Entry.Behaviors>
</Entry>
</Grid>
</VerticalStackLayout>
</Border>
Let’s start with the PlayerSettings controls. Look how they’re bound to the particular players
defined in ObservableCollection in the view model. Here’s the first player as an example:
<controls:PlayerSettings>
<controls:PlayerSettings.BindingContext>
<Binding Path="Players[0]" />
</controls:PlayerSettings.BindingContext>
</controls:PlayerSettings>
This is where the binding context is set, so let’s remove the following code from PlayerSettings.xaml:
<ContentView.BindingContext>
<viewmodels:PlayerSettingsViewModel />
</ContentView.BindingContext>
Next, have a look at the first radio button in the Ending Conditions section. Its IsVisible property
is bound to the OnlyOnePlayer property defined in the view model:
We’re using the InvertedBoolConverter that we get from the Community Toolkit. We added it to
the page’s resources:
<ContentPage.Resources>
...
<toolkit:InvertedBoolConverter x:Key="invertedBoolConverter" />
</ContentPage.Resources>
We want the first radio button to be visible only when there are at least two players.
272
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The two entries in the Ending Conditions section should behave like the entries in the
PlayerSettings controls. The input must be valid in order to proceed. That’s why we added the
TextChanged events. In the code-behind, the SettingsPage.xaml.cs file, they’re implemented like so:
...
public partial class SettingsPage : ContentPage
{
public SettingsPage(SettingsViewModel settingsViewModel)
{
InitializeComponent();
BindingContext = settingsViewModel;
}
Helpers.HandleNumericEntryState(maxRacesValid, maxRacesEntry);
}
}
Helpers.HandleNumericEntryState(maxTimeValid, maxTimeEntry);
}
}
}
EventToCommandBehavior
Let’s start with the player radio buttons, so the four radio buttons used to select the number of
players in the game. They all look similar. The second button has the IsChecked property set to
True, because the default number of players is two. Here’s the first button:
<RadioButton
GroupName="players">
...
</RadioButton.Content>
<RadioButton.Behaviors>
<toolkit:EventToCommandBehavior
EventName="CheckedChanged"
Command="{Binding CreatePlayerListCommand}">
<toolkit:EventToCommandBehavior.CommandParameter>
<x:Int32>1</x:Int32>
</toolkit:EventToCommandBehavior.CommandParameter>
</toolkit:EventToCommandBehavior>
</RadioButton.Behaviors>
</RadioButton>
273
We want to implement the method that we’ll use to add and remove players from the Players
observable collection. As you remember, the CreatePlayerList method has the RelayCommand
attribute, which means it can be used in commanding. In the TestPage we bound the button’s
Command property to a method, but now we have to bind the radio buttons. The problem is that,
unlike buttons, radio buttons don’t support commands. This means we have to stick with events…
unless we use the .NET MAUI Community Toolkit and its EventToCommandBehavior.
First, we have to install a NuGet package. Right-click the project and select Manage NuGet
Packages… Search for the CommunityToolkit.Maui package (A) and install it (B):
When the package is installed, you will see some instructions to follow. We have to initialize the
package. Go to the MauiProgram.cs file, add the appropriate using statement at the top of the file
and call the UseMauiCommunityToolkit extension method on the builder object:
using Microsoft.Extensions.Logging;
using Slugrace.Views;
using Slugrace.ViewModels;
using CommunityToolkit.Maui;
namespace Slugrace;
Also in the instructions, you can see how to use the toolkit in XAML. You have to add the following
namespace:
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
274
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
After adding this namespace in the SettingsPage, we can make use of the
EventToCommandBehavior. It allows us to invoke a command through an event.
We’re passing a parameter to the command, which is the number of players this button is supposed
to set.
We also added the EventToCommandBehavior to the ending condition radio buttons below where
the Command property is set to the SetEndingCondition method in the view model.
There’s one more piece of the puzzle we have to talk about, the messaging system. We’re sending
and receiving messages between view models.
In our app, the button in the SettingsPage must respond to what happens in each particular
PlayerSettings control. More precisely, it has to be disabled if the player’s name or initial money
gets invalid. Let’s take the player’s name as an example. In this property’s setter a couple
notifications are sent so that other properties can be updated if necessary. Here’s the
PlayerSettingsViewModel.cs file:
...
public partial class PlayerSettingsViewModel : ObservableObject
{
...
public string PlayerName
{
get => player.Name;
set
{
if (player.Name != value)
{
player.Name = value;
OnPropertyChanged();
OnPropertyChanged(nameof(NameIsValid));
OnPropertyChanged(nameof(PlayerIsValid));
...
But the button in the SettingsPage also needs to be notified in order to adjust its IsEnabled
property. We can bind this property to a property in the SettingsViewModel, but not to a property
in the PlayerSettingsViewModel. Fortunately, a property in one view model can send a message
that will be received by another view model and a property in the other view model, to which the
button can bind, will be set appropriately. Looks a bit complicated, so let’s analyze it step by step.
275
The first step is to create the message. We’re going to need two messages: one will be sent by the
PlayerName property when it changes, the other by the PlayerInitialMoney when it changes, so
let’s create a folder in the root of our app and name it Messages. In the folder let’s create two classes
and name them PlayerNameChangedMessage and PlayerInitialMoneyChangedMessage. Here’s
how the former is implemented:
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Slugrace.Messages;
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Slugrace.Messages;
The MVVM Toolkit supports two types of messengers. The one we’re interested in is
WeakReferenceMessenger. As the name suggests, it uses weak references internally. It offers
automatic memory management for the recipients, so you don’t have to remember to unsubscribe
the recipients.
Now, with our messages created, the two properties of the PlayerSettingsViewModel class can
send them:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Slugrace.Messages;
using Slugrace.Models;
namespace Slugrace.ViewModels;
WeakReferenceMessenger.Default.Send(new PlayerNameChangedMessage(value));
}
}
}
276
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
WeakReferenceMessenger.Default.Send(new PlayerInitialMoneyChangedMessage(value));
}
}
}
...
Sending is one thing. Another thing is receiving. Which object is supposed to receive the messages?
Well, definitely the SettingsViewModel. The message must be registered there and, naturally,
handled. So, let’s open the SettingsViewModel.cs file and register the messages in the constructor:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Slugrace.Messages;
using Slugrace.Models;
using System.Collections.ObjectModel;
...
public partial class SettingsViewModel : ObservableObject
...
public SettingsViewModel()
{
...
Players =
...
As you can see, there are two methods that will handle the messages with pretty long names:
OnPlayerNameChangedMessageReceived and OnPlayerInitialMoneyChangedMessageReceived.
Before we implement them, let’s add two observable properties they will make use of,
changedPlayerName and changedPlayerInitialMoney:
...
public partial class SettingsViewModel : ObservableObject
...
public bool RacesEndingConditionSet
...
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AllSettingsAreValid))]
private string changedPlayerName;
277
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AllSettingsAreValid))]
private int? changedPlayerInitialMoney;
...
public partial class SettingsViewModel : ObservableObject
...
public SettingsViewModel()
...
private void OnPlayerNameChangedMessageReceived(string value)
{
ChangedPlayerName = value;
}
[RelayCommand]
void CreatePlayerList(int numberOfPlayers)
...
As you can see, whenever one of these properties changes, the AllSettingsAreValid property is
notified and this is the property
the button’s IsEnabled
property binds to. The button
can now react to any changes in
the properties that sent the
messages from a different object.
278
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
OK, we’ve spent so much time trying to enable or disable the button, depending on the data
delivered in the entries, that we didn’t even have time to think about what this button is for in the
first place…
And the button is for navigation. If you click it, it will navigate to the RacePage where the actual
game begins. Navigation is going to be the subject of the next chapter.
279
Chapter 18 - Navigation
code: https://github.com/prospero-apps/Slugrace/tree/chapter18
In the previous chapter we were implementing the SettingsPage view, its corresponding view
model and the PlayerSettings view and view model. This is where we set the number of players,
their names and initial money, as well as the ending condition. When this is all done correctly, the
Ready button is enabled and if we hit it, we navigate to the RacePage. Well, this is what we want
the button to do. For now it does nothing.
So, in this chapter we’ll implement the navigation to the RacePage, and navigation in general.
There are going to be several places in our app, where navigation between pages will be required,
in particular:
- from RacePage to InstructionsPage when the Instructions button is pressed, and back when a
Back button in the InstructionsPage is pressed (we haven’t created this page yet),
- from GameOverPage to SettingsPage when the Play Again button in the former is pressed.
We’ll implement all this functionality in this chapter. We’ll also create the missing view models and
a basic InstructionsPage as we proceed.
280
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Also, we don’t need the properties we defined before in the TestViewModel. Make sure this class
looks for now like so:
using CommunityToolkit.Mvvm.ComponentModel;
namespace Slugrace.ViewModels;
There are different ways navigation can be implemented in .NET MAUI. We’re going to use URI
Shell-based navigation, which looks like navigation to a website. The .NET MAUI Shell gives a
structure to the application. We used it to set a starting page that is displayed when the app runs.
In order to be able to navigate to the TestPage, we have to register this page with the routing
system of .NET Shell. Open the AppShell.xaml.cs file and add the following code:
using Slugrace.Views;
namespace Slugrace;
The RegisterRoute method takes two parameters. The first one is the route itself, the second one
is the type that should be associated with this route. We’re using here the name of the page as the
route, but you can use any name you like.
Now, we want to navigate to from SettingsPage to TestPage when the Ready button is pressed.
Let’s create a method in the SettingsViewModel and add a RelayCommand attribute to it:
...
public partial class SettingsViewModel : ObservableObject
{
...
[RelayCommand]
void SetEndingCondition(string condition)
281
...
[RelayCommand]
async Task StartGame()
{
await Shell.Current.GoToAsync("TestPage");
}
}
This is an asynchronous method used for Shell navigation. Here we passed the route as a string
literal, but we could also do it using nameof:
...
using Slugrace.Views;
...
public partial class SettingsViewModel : ObservableObject
{
...
await Shell.Current.GoToAsync(nameof(TestPage));
...
Now go to the SettingsPage and bind the button’s Command property to the method:
...
<ContentPage ...>
...
<!--the Ready button-->
<Button
Grid.Row="3"
Text="Ready"
IsEnabled="{Binding AllSettingsAreValid}"
Command="{Binding StartGameCommand}">
</Button>
...
282
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here the arrow is practically invisible because it’s white, just like the
background. But if you click it, it works.
In the actual game we shouldn’t be able to navigate from the RacePage back
to the SettingsPage, so we have to take care of it. But first let’s see how to
pass data from page to page.
If you want to pass any query parameters in your web browser, they’re separated by a question
mark. We also use a question mark in .NET MAUI.
So, suppose we want to pass the value of maxRaces to the TestPage. This is how we do it:
...
public partial class SettingsViewModel : ObservableObject
{
...
[RelayCommand]
async Task StartGame()
{
await Shell.Current.GoToAsync($"{nameof(TestPage)}?Limit={maxRaces}");
}
}
So, we’re using an interpolated string where the route is followed by ?, then the query identifier
(Limit) and the value that we want to pass (maxRaces).
Next, we have to receive the data in the page we navigate to. As we’re using the MVVM pattern,
we’ll do it in the TestViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
namespace Slugrace.ViewModels;
[QueryProperty("MaxLimit", "Limit")]
283
We’re using the QueryProperty attribute with two arguments. The first argument (MaxLimit) is
the name of the property defined in the view model that we will be able to bind to, and the second
argument (Limit) is the name of the query identifier that we used in the query inside the
GoToAsync method. We could use any name for the first parameter, also the same as for the second
one. Anyway, then we have to define the property with the same name as the first argument in the
class. In our case we’re using an observable property, so the actual property with the capitalized
name will be generated for us. Here’s the code:
...
[QueryProperty("MaxLimit", "Limit")]
public partial class TestViewModel : ObservableObject
{
[ObservableProperty]
int maxLimit;
}
...
[QueryProperty(nameof(MaxLimit), "Limit")]
...
Let’s now bind to this property. We’ll create a new label in the TestPage view and bind its Text
property to MaxLimit:
...
<ContentPage ...>
You can also pass multiple query parameters. Let’s modify the StartGame method:
284
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class SettingsViewModel : ObservableObject
{
...
[RelayCommand]
async Task StartGame()
{
await Shell.Current.GoToAsync(
$"{nameof(TestPage)}?Limit={maxRaces}&Word={GameEndingCondition}");
}
}
Here we added a second parameter. Now, let’s receive both parameters in the TestViewModel:
...
[QueryProperty(nameof(MaxLimit), "Limit")]
[QueryProperty(nameof(Word), "Word")]
public partial class TestViewModel : ObservableObject
{
[ObservableProperty]
int maxLimit;
[ObservableProperty]
string? word;
}
...
<ContentPage ...>
...
<Label Text="{Binding MaxLimit... />
<Label Text="{Binding Word, StringFormat='favorite word: {0}'}"
FontSize="20" />
...
285
If you want to pass complex objects, you should add another argument to the GoToAsync method,
which is a Dictionary<string, object>, like for example:
...
public partial class SettingsViewModel : ObservableObject
{
...
[RelayCommand]
async Task StartGame()
{
await Shell.Current.GoToAsync($"{nameof(TestPage)}",
new Dictionary<string, object>
{
{"Team", Players }
});
}
}
How do we receive navigation data in the TestViewModel now? Exactly the same as before:
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace Slugrace.ViewModels;
[QueryProperty(nameof(Team), "Team")]
public partial class TestViewModel : ObservableObject
{
[ObservableProperty]
ObservableCollection<PlayerSettingsViewModel>? team;
}
...
<ContentPage ...>
<VerticalStackLayout WidthRequest="600">
<Label Text="Test Page"
FontSize="40" />
<Label Text="{Binding Team.Count,
StringFormat='team size: {0} players'}"
FontSize="30" />
<Label Text="{Binding Team[0].PlayerInitialMoney,
StringFormat='The captain has ${0}.'}"
FontSize="20" />
</VerticalStackLayout>
</ContentPage>
286
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Navigating Back
Suppose we don’t want the user to navigate back. Then we have to remove the back button. The
back button can be manipulated by setting the BackButtonBehavior attached property to a
BackButtonBehavior object in the page you navigate to. There are a couple properties in the
BackButtonBehavior class, but the two that are of interest to us now are IsEnabled and
IsVisible. We just have to set them to False. Add the following code to the TestPage.xaml file:
...
<ContentPage ...>
<Shell.BackButtonBehavior>
<BackButtonBehavior IsEnabled="False" IsVisible="False" />
</Shell.BackButtonBehavior>
If you now run the app, you won’t see the back button anymore.
As we’re at it, let’s add these same three lines of XAML code to the pages we’ll be navigating to.
This way we won’t be able to navigate back by hitting the arrow. In particular, let’s add them to the
RacePage:
...
<ContentPage ...
x:Class="Slugrace.Views.RacePage"
Padding="5">
<Shell.BackButtonBehavior>
<BackButtonBehavior IsEnabled="False" IsVisible="False" />
</Shell.BackButtonBehavior>
<Grid>
...
287
...
<ContentPage ...
x:Class="Slugrace.Views.GameOverPage">
<Shell.BackButtonBehavior>
<BackButtonBehavior IsEnabled="False" IsVisible="False" />
</Shell.BackButtonBehavior>
<ContentPage.Resources>
...
Let’s get back to the TestPage and implement backward navigation programmatically, though.
First let’s add a button in the TestPage to navigate back to the SettingsPage and bind its Command
property to a method that we have to create in the view model. Actually, let’s start with the
method:
...
[QueryProperty(nameof(Team), "Team")]
public partial class TestViewModel : ObservableObject
{
[ObservableProperty]
ObservableCollection<PlayerSettingsViewModel> team;
[RelayCommand]
async Task NavigateBack()
{
await Shell.Current.GoToAsync("..");
}
}
We don’t have to specify the page we want to navigate to by its name. If we use the two dots, it just
means we want to move one level up the stack, so to the page we navigated from.
...
<ContentPage ...>
...
<Label Text="{Binding Team[0].PlayerInitialMoney, ...
<Button Text="Back to settings"
Command="{Binding NavigateBackCommand}"
HorizontalOptions="Start" />
</VerticalStackLayout>
</ContentPage>
If you run the app, you’ll see the button. If you hit
it, you’ll go back to where you came from.
288
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
GameViewModel
We’ll create a view model that will be used by the RacePage and later also by the
InstructionsPage. This view model will end up pretty complex because it will include
information about the entire game.
We’ll need a RaceStatus enumeration to keep track of whether we’re waiting for a race to begin,
or whether a race is currently going on, or whether it just finished. So, add a RaceStatus enum to
the root of the app and implement it like so:
namespace Slugrace;
And now add a GameViewModel class to the ViewModels folder and implement it like so:
using CommunityToolkit.Mvvm.ComponentModel;
namespace Slugrace.ViewModels;
Let’s register the page and the view model with the dependency service in MauiProgram.cs:
...
public static class MauiProgram
{
...
builder.Services.AddTransient<SettingsViewModel>();
builder.Services.AddTransient<RacePage>();
builder.Services.AddTransient<GameViewModel>();
return builder.Build();
}
}
Next, in RacePage.xaml.cs, let’s inject the view model in the constructor and set the binding context
of the view to the view model:
289
using Slugrace.ViewModels;
...
public partial class RacePage : ContentPage
{
public RacePage(GameViewModel gameViewModel)
{
InitializeComponent();
BindingContext = gameViewModel;
}
}
...
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(RacePage), typeof(RacePage));
}
}
The navigation should start when the Ready button is pressed. We’ll create a StartGame method in
the SettingsViewModel and use it as a command.
In the method we’ll populate the Game object with the slugs and players. To do that, let’s add some
more properties to the Slug model class. It now should look like this:
...
public class Slug
{
public string Name { get; set; } = string.Empty;
public double Odds { get; set; }
public double PreviousOdds { get; set; }
public int WinNumber { get; set; }
public string ImageUrl { get; set; } = string.Empty;
public string EyeImageUrl { get; set; } = string.Empty;
public string BodyImageUrl { get; set; } = string.Empty;
public double BaseOdds { get; set; }
public bool IsRaceWinner { get; set; }
}
The properties we want to set in the SettingsPage are: Name, BaseOdds, ImageUrl, EyeImageUrl
and BodyImageUrl. Each slug will have a fixed name and BaseOdds. Speedster will have the
lowest BaseOdds, because this slug will be the most probable to win. Similarly, as the least
probable to win, Slowpoke will have the highest BaseOdds.
290
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The ImageUrl refers to the silhouette image of the slug. The EyeImageUrl and BodyImageUrl
properties will be used for the images in top view.
Before we move on, let’s also add some properties to the other model classes. We’re going to need
them soon. So, here’s the Player class:
...
public class Player
{
...
public bool IsInGame { get; set; }
public int PreviousMoney { get; set; }
public bool IsBankrupt { get; set; }
}
...
public class Game
{
...
public int GameTimeSet { get; set; } = 0;
public string GameOverReason { get; set; } = string.Empty;
public List<Player> Winners { get; set; } = [];
public int RaceNumber { get; set; }
public int TimeElapsed { get; set; }
}
We’ll discuss all the new properties when we need them. With that in place, let’s create the real
StartGame method (rather that the one we created before to test navigation using the TestPage) in
the SettingsViewModel:
...
public partial class SettingsViewModel : ObservableObject
{
...
void SetEndingCondition(string condition)
...
[RelayCommand]
async Task StartGame()
{
// Populate the Game object
game.Slugs =
[
new Slug
{
Name = "Speedster",
BaseOdds = 1.33,
ImageUrl = "speedster.png",
EyeImageUrl = "speedster_eye.png",
BodyImageUrl = "speedster_body.png"
},
291
new Slug
{
Name = "Trusty",
BaseOdds = 1.59,
ImageUrl = "trusty.png",
EyeImageUrl = "trusty_eye.png",
BodyImageUrl = "trusty_body.png"
},
new Slug
{
Name = "Iffy",
BaseOdds = 2.5,
ImageUrl = "iffy.png",
EyeImageUrl = "iffy_eye.png",
BodyImageUrl = "iffy_body.png"
},
new Slug
{
Name = "Slowpoke",
BaseOdds = 2.89,
ImageUrl = "slowpoke.png",
EyeImageUrl = "slowpoke_eye.png",
BodyImageUrl = "slowpoke_body.png"
}
];
game.Players = [];
// Navigate to RacePage
await Shell.Current.GoToAsync($"{nameof(RacePage)}",
new Dictionary<string, object>
{
{"Game", game }
});
}
}
292
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here we’re populating the Game object with the data collected from the SettingsPage. This object
is then passed to the RacePage.
We already bound the method to the Command property of the Ready button in the SettingsPage:
...
<ContentPage ...>
...
<!--the Ready button-->
<Button
Grid.Row="3"
Text="Ready"
IsEnabled="{Binding AllSettingsAreValid}"
Command="{Binding StartGameCommand}">
</Button>
...
Next, in the GameViewModel, we have to add the QueryProperty attribute and create a property to
store the data passed from the SettingsPage:
using CommunityToolkit.Mvvm.ComponentModel;
using Slugrace.Models;
...
[QueryProperty(nameof(Game), "Game")]
public partial class GameViewModel : ObservableObject
{
[ObservableProperty]
private Game? game;
[ObservableProperty]
RaceStatus raceStatus;
...
If we now run the app, set the players (their names and initial money) and the ending condition,
and then hit the Ready button, we’ll navigate to the RacePage. We’ll take all the game information
with us. Now we can consume it in the RacePage.
There are several content views nested in the RacePage that need information about the Game
object, the Player objects and the Slug objects. All this information will be contained in the
GameViewModel.
GameInfo
Let’s start with the GameInfo view. As a child of the RacePage, it will bind to the GameViewModel,
just like its parent. We have to add some properties to the view model that this content view will
bind to. Here’s the GameViewModel class:
293
...
[QueryProperty(nameof(Game), "Game")]
public partial class GameViewModel : ObservableObject
{
...
RaceStatus raceStatus;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(EndingConditionIsRaces))]
[NotifyPropertyChangedFor(nameof(EndingConditionIsTime))]
private EndingCondition gameEndingCondition;
public bool EndingConditionIsRaces => GameEndingCondition == EndingCondition.Races;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(RacesToGo))]
private int numberOfRacesSet;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TimeRemaining))]
private TimeSpan gameTimeSet;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TimeRemaining))]
private TimeSpan timeElapsed;
Here we have all the properties that our view needs. Now we can bind to them. The race number
will be visible independent of the ending condition, but other information will be displayed
depending on which ending condition is set. Here’s the GameInfo.xaml file:
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Controls.GameInfo">
294
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid
...
<Label
Grid.Row="1"
Grid.Column="1"
Text="{Binding RaceNumber}" />
295
<Label
IsVisible="{Binding EndingConditionIsTime}"
Grid.Row="3"
Text="Time elapsed:" />
<Label
IsVisible="{Binding EndingConditionIsTime}"
Grid.Row="3"
Grid.Column="1"
Text="{Binding TimeElapsed}" />
<Label
IsVisible="{Binding EndingConditionIsTime}"
Grid.Row="4"
Text="Time remaining:" />
<Label
IsVisible="{Binding EndingConditionIsTime}"
Grid.Row="4"
Grid.Column="1"
Text="{Binding TimeRemaining}" />
</Grid>
</ContentView>
We have to modify the GameViewModel now because we need information from the Game object
passed from the SettingsPage. However, we can’t do it in the constructor because there the Game
object isn’t available. That’s why we have to add a partial method OnGameChanged and do it there:
...
public partial class GameViewModel : ObservableObject
{
...
public TimeSpan TimeRemaining => GameTimeSet - TimeElapsed;
RaceStatus = RaceStatus.NotYetStarted;
RaceNumber = 1;
NumberOfRacesSet = value.NumberOfRacesSet;
GameTimeSet = TimeSpan.FromMinutes(value.GameTimeSet);
}
...
}
296
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
With this in place, let’s move on to the Slugs’ Stats and Players’ Stats panels. These panels need
information about the slugs and players. There are more content views in the app that need
information about the slugs and players, so let’s create separate view models to serve them all.
SlugViewModel
Let’s start with the slugs. So, add a new class to the ViewModels folder and name it SlugViewModel.
Here’s the code:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Slugrace.Messages;
using Slugrace.Models;
namespace Slugrace.ViewModels;
297
public string ImageUrl
{
get => slug.ImageUrl;
set
{
if (slug.ImageUrl != value)
{
slug.ImageUrl = value;
OnPropertyChanged();
}
}
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(WinPercentage))]
private int currentRaceNumber;
298
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
[ObservableProperty]
private int winPercentage;
public SlugViewModel()
{
slug = new Slug();
CurrentRaceNumber = 1;
PreviousOdds = Odds;
299
Odds = IsRaceWinner
? Math.Round(Math.Max(1.01, Math.Min(Odds * .96, 20)), 2)
: Math.Round(Math.Max(1.01, Math.Min(Odds * 1.03, 20)), 2);
}
}
if (IsRaceWinner)
{
WinNumber++;
}
}
}
This is the complete code that is required by all the views where slug information is required. Let’s
break it down.
So, we have the Name property, followed by three properties related to the slug images. We also
have a WinNumber property that will increase by 1 every time the slug wins a race.
There are some more properties that are related to the winning position of the slug after each race.
The IsRaceWinner property will be set to true if the slug wins. The WinNumberText property will
be used to display the number of wins correctly.
Another property is WinPercentage. This property will tell us what part (expressed as a
percentage) of the total number of races the slug has won. To calculate this property, we have to
know how many races have already finished, hence the CurrentRaceNumber property. We’ll use a
message to notify the WinPercentage property each time a race finishes. Let’s create the message
first.
In the Messages folder add a new class and name it RaceFinishedMessage. Here’s the code:
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Slugrace.Messages;
This message will be sent each time the race status changes to Finished. Let’s modify the
RaceStatus property in the GameViewModel:
300
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class GameViewModel : ObservableObject
{
...
private RaceStatus raceStatus;
if (raceStatus == RaceStatus.Finished)
{
WeakReferenceMessenger.Default.Send(new RaceFinishedMessage(value));
}
}
}
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(EndingConditionIsRaces))]
...
We also have the RecalculateStats method that we will call later from inside the
GameViewModel for each slug.
PlayerViewModel
Add a PlayerViewModel class. It will contain all player-related data:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Slugrace.Messages;
using Slugrace.Models;
namespace Slugrace.ViewModels;
301
public partial class PlayerViewModel : ObservableObject
{
private readonly Player player;
WeakReferenceMessenger.Default.Send(
new PlayerBetAmountChangedMessage(value));
}
}
}
302
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
[ObservableProperty]
private List<SlugViewModel> slugs = [];
303
set
{
if (selectedSlug != value)
{
selectedSlug = value;
OnPropertyChanged();
OnPropertyChanged(nameof(SelectedSlugIsValid));
OnPropertyChanged(nameof(PlayerIsValid));
WeakReferenceMessenger.Default.Send(
new PlayerSelectedSlugChangedMessage(value!));
}
}
}
[ObservableProperty]
private string resultMessage = string.Empty;
public PlayerViewModel()
{
player = new Player();
}
[RelayCommand]
void SelectSlug(string name)
{
var slug = Slugs.Find(s => s.Name == name);
if (slug != null)
{
SelectedSlug = slug;
}
}
WonOrLostMoney = (int)(wonRace
? BetAmount * (SelectedSlug!.Odds - 1)
: -BetAmount);
CurrentMoney += WonOrLostMoney;
ResultMessage = wonRace
? (WonOrLostMoney == 0
? $"- won less than $1"
: $"- won ${WonOrLostMoney}")
: $"- lost ${Math.Abs(WonOrLostMoney)}";
}
}
304
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Again, this is the complete code. Here we have some basic properties like Name, InitialMoney or
CurrentMoney. We have the BetAmount property to store the amount of money a player bets on a
slug. We store the current money in the PreviousMoney property so that we can see how much
money the player had before the race. After the race the CurrentMoney property changes
depending on the value of WonOrLostMoney, which, in turn, depends on whether the player won or
lost.
The player also holds a reference to the slugs and before each race a slug is selected and stored in
the SelectedSlug property.
When the BetAmount and SelectedSlug properties change, messages are sent. Let’s implement
them. In the Messages folder add the PlayerBetAmountChangedMessage and
PlayerSelectedSlugChangedMessage. The former should be implemented like this:
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Slugrace.Messages;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Slugrace.ViewModels;
namespace Slugrace.Messages;
These messages must be registered in the GameViewModel’s constructor, but we’ll see to it in a
moment. For now, let’s stay in the PlayerViewModel class.
The player must be valid in order for the Go button in the Bets panel to be enabled. To check the
validity, we have three properties: BetAmountIsValid, SelectedSlugIsValid and
PlayerIsValid.
We also have the SelectSlug method that will be called when a radio button representing a slug is
checked. Finally, after each race, the money properties are recalculated and a result message is
created. For this we need the CalculateMoney method and the ResultMessage property.
And now let’s go to the GameViewModel, add the slugs and players, and register the messages:
305
...
public partial class GameViewModel : ObservableObject
{
...
public TimeSpan TimeRemaining => GameTimeSet - TimeElapsed;
[ObservableProperty]
private List<SlugViewModel> slugs = [];
[ObservableProperty]
private List<PlayerViewModel> players = [];
[ObservableProperty]
private ObservableCollection<PlayerViewModel> playersStillInGame = [];
[ObservableProperty]
private List<PlayerViewModel> winners = [];
[ObservableProperty]
private SlugViewModel? raceWinnerSlug;
if (changedBetAmount != value)
{
changedBetAmount = value;
}
}
}
if (changedSelectedSlug != value)
{
changedSelectedSlug = value;
}
}
}
public GameViewModel()
{
WeakReferenceMessenger.Default.Register<PlayerBetAmountChangedMessage>(
this, (r, m) => OnBetAmountChangedMessageReceived(m.Value));
306
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
WeakReferenceMessenger.Default.Register<PlayerSelectedSlugChangedMessage>(
this, (r, m) => OnSelectedSlugChangedMessageReceived(m.Value));
}
Winners = [];
Slugs =
[
new()
{
Name = value.Slugs![0].Name,
ImageUrl = value.Slugs[0].ImageUrl,
EyeImageUrl = value.Slugs[0].EyeImageUrl,
BodyImageUrl = value.Slugs[0].BodyImageUrl,
WinNumber = 0,
Odds = Math.Round(value.Slugs[0].BaseOdds
+ new Random().Next(0, 10) / 100, 2),
PreviousOdds = value.Slugs[0].BaseOdds
},
new()
{
Name = value.Slugs[1].Name,
ImageUrl = value.Slugs[1].ImageUrl,
EyeImageUrl = value.Slugs[1].EyeImageUrl,
BodyImageUrl = value.Slugs[1].BodyImageUrl,
WinNumber = 0,
Odds = Math.Round(value.Slugs[1].BaseOdds
+ new Random().Next(0, 10) / 100, 2),
PreviousOdds = value.Slugs[1].BaseOdds
},
new()
{
Name = value.Slugs[2].Name,
ImageUrl = value.Slugs[2].ImageUrl,
EyeImageUrl = value.Slugs[2].EyeImageUrl,
BodyImageUrl = value.Slugs[2].BodyImageUrl,
WinNumber = 0,
Odds = Math.Round(value.Slugs[2].BaseOdds
+ new Random().Next(0, 10) / 100, 2),
PreviousOdds = value.Slugs[2].BaseOdds
},
new()
{
Name = value.Slugs[3].Name,
ImageUrl = value.Slugs[3].ImageUrl,
EyeImageUrl = value.Slugs[3].EyeImageUrl,
307
BodyImageUrl = value.Slugs[3].BodyImageUrl,
WinNumber = 0,
Odds = Math.Round(value.Slugs[3].BaseOdds
+ new Random().Next(0, 10) / 100, 2),
PreviousOdds = value.Slugs[3].BaseOdds
},
];
Players = players;
PlayersStillInGame = Players.ToObservableCollection();
}
}
In the OnGameChanged method we create the slugs and players. We’ll be talking about the
PlayersStillInGame, Winners and RaceWinnerSlug properties a bit later. In the constructor we
register the two messages we just created. We also define the AllPlayersAreValid property to
check whether we can start the next race.
Now that we have the SlugViewModel and PlayerViewModel classes, let’s bind to them. Let’s start
with the Slugs’ Stats content view.
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Slugrace.Controls"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
308
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Controls.SlugsStats">
<ContentView.Resources>
<Style TargetType="Label" BasedOn="{StaticResource labelBaseStyle}" />
</ContentView.Resources>
<Grid
RowDefinitions="*, *, *, *, *"
ColumnDefinitions="*">
<Label
Text="Slugs' Stats"
Style="{OnPlatform {StaticResource labelSectionTitleStyle},
Android={StaticResource androidLabelSectionTitleStyle}}" />
<controls:SlugStats
BindingContext="{Binding Slugs[0]}"
Grid.Row="1" />
<controls:SlugStats
BindingContext="{Binding Slugs[1]}"
Grid.Row="2" />
<controls:SlugStats
BindingContext="{Binding Slugs[2]}"
Grid.Row="3" />
<controls:SlugStats
BindingContext="{Binding Slugs[3]}"
Grid.Row="4" />
</Grid>
</ContentView>
So, let’s have a look at a single slug now. The binding context for the SlugStats view is
SlugViewModel. We bind to the properties we defined there:
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:SlugViewModel"
x:Class="Slugrace.Controls.SlugStats">
<ContentView.BindingContext>
<viewmodels:SlugViewModel />
</ContentView.BindingContext>
<ContentView.Resources>
...
<Grid
RowDefinitions="*">
<Grid.ColumnDefinitions>
...
<Label
Text="{Binding Name}" />
309
<Label
Grid.Column="1"
Text="{Binding WinNumberText}" />
<Label
Grid.Column="2"
Text="{Binding WinPercentage, StringFormat='{0}%'}" />
</Grid>
</ContentView>
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Slugrace.Controls"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Controls.PlayersStats">
<ContentView.Resources>
<Style TargetType="Label" BasedOn="{StaticResource labelBaseStyle}" />
</ContentView.Resources>
<Grid
RowDefinitions="*, *, *, *, *"
ColumnDefinitions="*">
<Label
Text="Players' Stats"
Style="{OnPlatform {StaticResource labelSectionTitleStyle},
Android={StaticResource androidLabelSectionTitleStyle}}" />
<CollectionView
Grid.Row="1"
Grid.RowSpan="4"
ItemsSource="{Binding Players}">
310
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<CollectionView.ItemTemplate>
<DataTemplate>
<controls:PlayerStats Margin="0, 2.5"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentView>
And now let’s have a look at a single player. The binding context of the PlayerStats control is
PlayerViewModel:
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:PlayerViewModel"
x:Class="Slugrace.Controls.PlayerStats">
<ContentView.BindingContext>
<viewmodels:PlayerViewModel />
</ContentView.BindingContext>
<ContentView.Resources>
<Style TargetType="Label"
BasedOn="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}" />
<toolkit:InvertedBoolConverter x:Key="invertedBoolConverter" />
</ContentView.Resources>
<Grid
RowDefinitions="*">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 2.5*, Android=*}" />
<ColumnDefinition Width="{OnPlatform 1.5*, Android=*}" />
</Grid.ColumnDefinitions>
<Label
IsVisible="{Binding IsBankrupt, Converter={StaticResource invertedBoolConverter}}"
Text="{Binding Name}"
Margin="0, 3" />
<Label
IsVisible="{Binding IsBankrupt}"
Text="{Binding Name}"
TextColor="Red"
Opacity=".4"
Margin="0, 3" />
<Label
IsVisible="{Binding IsBankrupt, Converter={StaticResource invertedBoolConverter}}"
Grid.Column="1"
Text="{Binding CurrentMoney, StringFormat='has ${0}'}"
Margin="0, 3" />
<Label
IsVisible="{Binding IsBankrupt}"
Grid.Column="1"
Text="is bankrupt"
TextColor="Red"
Opacity=".4"
Margin="0, 3" />
</Grid>
</ContentView>
311
Here we use the InvertedBoolConverter from the Toolkit. If a player is not bankrupt, their data
will be displayed in black. Otherwise, it will be displayed in red. We’ll handle going bankrupt
soon.
Let’s move on to the racetrack now. There we have the slug images and some slug info.
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:SlugViewModel"
x:Class="Slugrace.Controls.SlugImage">
<AbsoluteLayout>
<Image
Source="{Binding EyeImageUrl}"
...
<Image
Source="{Binding EyeImageUrl}"
...
<Image
Source="{Binding BodyImageUrl}"
...
</AbsoluteLayout>
</ContentView>
The SlugInfo content view is used to display the slugs’ names, wins and odds:
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:SlugViewModel"
x:Class="Slugrace.Controls.SlugInfo">
312
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<ContentView.Resources>
...
<Grid
Padding="5, 2, 0, 2"
RowDefinitions="*, *"
ColumnDefinitions="6.8*, 2.2*">
<Label
Text="{Binding Name}"
...
<Label
Grid.Row="1"
Text="{Binding WinNumberText}"
...
<Label
Grid.Column="1"
Grid.RowSpan="2"
VerticalOptions="{StaticResource defaultLayoutOptions}"
Text="{Binding Odds, StringFormat='{0:F2}'}"
...
...
<ContentView ...
xmlns:controls="clr-namespace:Slugrace.Controls"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Controls.TrackImage">
<AbsoluteLayout>
<!--Racetrack-->
<Image
Source="racetrack.png"
...
<!--Speedster-->
<controls:SlugImage
BindingContext="{Binding Slugs[0]}"
AbsoluteLayout.LayoutBounds=".1, .05, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
BindingContext="{Binding Slugs[0]}"
AbsoluteLayout.LayoutBounds="0, 0, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
<!--Trusty-->
<controls:SlugImage
BindingContext="{Binding Slugs[1]}"
AbsoluteLayout.LayoutBounds=".1, .35, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
BindingContext="{Binding Slugs[1]}"
AbsoluteLayout.LayoutBounds="0, .35, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
313
<!--Iffy-->
<controls:SlugImage
BindingContext="{Binding Slugs[2]}"
AbsoluteLayout.LayoutBounds=".1, .65, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
BindingContext="{Binding Slugs[2]}"
AbsoluteLayout.LayoutBounds="0, .68, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
<!--Slowpoke-->
<controls:SlugImage
BindingContext="{Binding Slugs[3]}"
AbsoluteLayout.LayoutBounds=".1, .95, .15, .15"
AbsoluteLayout.LayoutFlags="All"/>
<controls:SlugInfo
BindingContext="{Binding Slugs[3]}"
AbsoluteLayout.LayoutBounds="0, 1.02, 1, .25"
AbsoluteLayout.LayoutFlags="All"/>
</AbsoluteLayout>
</ContentView>
If we now run the app and navigate to the RacePage, we’ll see all four slugs on the racetrack:
In the Bets content view we’ll use a CollectionView for the individual players:
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Slugrace.Controls"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Controls.Bets">
314
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<ContentView.Resources>
...
<Grid>
<Grid.RowDefinitions>
...
<Label
...
<VerticalStackLayout
x:Name="betControls"
Grid.Row="1">
<Button
Grid.Row="2"
Text="Go"
Margin="0, 0, 0, 5"
IsEnabled="{Binding AllPlayersAreValid}" />
</Grid>
</ContentView>
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
xmlns:behaviors="clr-namespace:Slugrace.Behaviors"
xmlns:converters="clr-namespace:Slugrace.Converters"
x:DataType="viewmodels:PlayerViewModel"
IsVisible="{Binding IsInGame}"
x:Class="Slugrace.Controls.PlayerBet">
<ContentView.Resources>
...
Android={StaticResource androidLabelBaseStyle}}" />
<converters:ZeroToEmptyStringConverter x:Key="zeroToEmptyConverter" />
<converters:SelectedSlugToBoolConverter x:Key="slugToBoolConverter" />
<toolkit:InvertedBoolConverter x:Key="invertedBoolConverter" />
</ContentView.Resources>
<Grid>
<Grid
IsVisible="{Binding IsBankrupt,
Converter={StaticResource invertedBoolConverter}}"
BackgroundColor="{OnPlatform
Android={AppThemeBinding
315
Light={StaticResource backgroundColorLight},
Dark={StaticResource backgroundColorDark}}}"
Margin="0, 0, 0, 2">
<Grid.RowDefinitions>
...
<Label
Text="{Binding Name}" />
...
<Entry
x:Name="betAmountEntry"
Grid.Column="3"
WidthRequest="{OnPlatform 200, Android=80}"
Placeholder="{Binding CurrentMoney, StringFormat='1 - {0}'}"
BackgroundColor="{OnPlatform Android=Transparent}"
TextColor="{AppThemeBinding Dark=White}"
Keyboard="Numeric"
Text="{Binding BetAmount, Converter={StaticResource zeroToEmptyConverter}}"
TextChanged="OnBetAmountTextChanged">
<Entry.Behaviors>
<behaviors:NumericInputBehavior />
</Entry.Behaviors>
</Entry>
<Slider
Grid.Column="4"
Margin="0, 0, 30, 0"
Value="{Binding BetAmount}"
Minimum="0"
Maximum="{Binding CurrentMoney}" />
...
<Grid
...
<RadioButton
IsChecked="{Binding SelectedSlug, Mode=OneWay,
Converter={StaticResource slugToBoolConverter},
ConverterParameter=Speedster}"
GroupName="{Binding Name}">
<RadioButton.Content>
...
</RadioButton.Content>
<RadioButton.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SelectSlugCommand}"
CommandParameter="Speedster" />
</RadioButton.GestureRecognizers>
</RadioButton>
<RadioButton
Grid.Column="1"
IsChecked="{Binding SelectedSlug, Mode=OneWay,
Converter={StaticResource slugToBoolConverter},
ConverterParameter=Trusty}"
GroupName="{Binding Name}">
<RadioButton.Content>
...
</RadioButton.Content>
316
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<RadioButton.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SelectSlugCommand}"
CommandParameter="Trusty" />
</RadioButton.GestureRecognizers>
</RadioButton>
<RadioButton
Grid.Column="2"
IsChecked="{Binding SelectedSlug, Mode=OneWay,
Converter={StaticResource slugToBoolConverter},
ConverterParameter=Iffy}"
GroupName="{Binding Name}">
<RadioButton.Content>
...
</RadioButton.Content>
<RadioButton.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SelectSlugCommand}"
CommandParameter="Iffy" />
</RadioButton.GestureRecognizers>
</RadioButton>
<RadioButton
Grid.Column="3"
IsChecked="{Binding SelectedSlug, Mode=OneWay,
Converter={StaticResource slugToBoolConverter},
ConverterParameter=Slowpoke}"
GroupName="{Binding Name}">
<RadioButton.Content>
...
</RadioButton.Content>
<RadioButton.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SelectSlugCommand}"
CommandParameter="Slowpoke" />
</RadioButton.GestureRecognizers>
</RadioButton>
</Grid>
</Grid>
<Grid
IsVisible="{Binding IsBankrupt}"
RowDefinitions="*">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform 150, Android=80}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Name}"
TextColor="Red"
TextDecorations="Strikethrough"
Opacity=".4" />
</Grid>
</Grid>
</ContentView>
317
It’s lengthy, but pretty straightforward. The entries where the bet amount is to be typed are empty
at the beginning. We ensure this in the code-behind. Also in the code-behind we take care of the
TextChanged event that gets fired every time something is typed in the entries:
using Slugrace.ViewModels;
namespace Slugrace.Controls;
Now, back to the XAML file. We bind the radio buttons to the SelectedSlug property of
PlayerViewModel using a converter that we haven’t created yet.
So, add the SelectedSlugToBoolConverter to the Converters folder and implement it like so:
using Slugrace.ViewModels;
using System.Globalization;
namespace Slugrace.Converters;
318
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
As I mentioned before, the SelectSlug method in the PlayerViewModel is called when a radio
button is checked. In order to avoid calling this method twice, the first time when a radio button is
unchecked and the second time a radio button is checked, we use the TapGestureRecognizer.
This way the method is called only once, when the radio button is checked.
...
<ContentPage ...
x:Class="Slugrace.Views.RacePage"
Padding="5">
...
<!--Bets/Results panel-->
<Border
...
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:Bets />
</Border>
...
In the SettingsPage I set the names of two players. The other two names are generic ones. I set the
initial money of the first two players to 2000 and of the other two to 1000. These values are bound
to in the Bets panel. We can now only select one slug in each group. The Go button is disabled
because the players haven’t placed their bets yet. As soon as they do, it will be enabled:
Next, let’s have a look at the Results content view. It’s implemented in the same way as the Bets
view:
319
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Slugrace.Controls"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Controls.Results">
<ContentView.Resources>
<Style TargetType="Label" BasedOn="{StaticResource labelBaseStyle}" />
<toolkit:InvertedBoolConverter x:Key="invertedBoolConverter" />
</ContentView.Resources>
<Grid>
...
<VerticalStackLayout
Grid.Row="1"
Spacing="{OnPlatform 0, Android=15}">
<Button
Grid.Row="2"
Text="Next Race"
Margin="0, 0, 0, 5" />
</Grid>
</ContentView>
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:PlayerViewModel"
IsVisible="{Binding IsInGame}"
x:Class="Slugrace.Controls.PlayerResult">
<ContentView.Resources>
<Color x:Key="backgroundColorLight">#FFF4E5</Color>
<Color x:Key="backgroundColorDark">Black</Color>
<Style TargetType="Label"
BasedOn="{OnPlatform {StaticResource labelBaseStyle},
Android={StaticResource androidLabelBaseStyle}}" />
<toolkit:InvertedBoolConverter x:Key="invertedBoolConverter" />
</ContentView.Resources>
320
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid>
<Grid
IsVisible="{Binding IsBankrupt, Converter={StaticResource invertedBoolConverter}}"
BackgroundColor="{OnPlatform
Android={AppThemeBinding
Light={StaticResource backgroundColorLight},
Dark={StaticResource backgroundColorDark}}}"
RowSpacing="{OnPlatform 0, Android=10}"
Margin="0, 0, 0, 5">
<Grid.RowDefinitions>
...
<Label
Text="{Binding Name}" />
<Label
Grid.Column="1"
Text="{Binding PreviousMoney, StringFormat='had ${0},'}" />
<Label
Grid.Column="2"
Text="{Binding BetAmount, StringFormat='bet ${0}'}" />
<Label
Grid.Column="3"
Text="{Binding SelectedSlug.Name, StringFormat='on {0},'}" />
<Label
Grid.Row="{OnPlatform 0, Android=1}"
Grid.Column="{OnPlatform 4, Android=0}"
Text="{Binding ResultMessage, StringFormat='{0},'}" />
<Label
Grid.Row="{OnPlatform 0, Android=1}"
Grid.Column="{OnPlatform 5, Android=1}"
Text="{OnPlatform {Binding CurrentMoney, StringFormat='now has ${0}.'},
Android={Binding CurrentMoney, StringFormat='has ${0}.'}}" />
<Label
Grid.Row="{OnPlatform 0, Android=2}"
Grid.Column="{OnPlatform 6, Android=2}"
Grid.ColumnSpan="{OnPlatform 1, Android=2}"
Text="{Binding SelectedSlug.PreviousOdds,
StringFormat='The odds were {0:F2}'}" />
</Grid>
<Grid
IsVisible="{Binding IsBankrupt}"
RowDefinitions="*">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform *, Android=1.2*}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Name}"
TextColor="Red"
TextDecorations="Strikethrough"
Opacity=".4" />
</Grid>
</Grid>
</ContentView>
321
Let’s now use the Results panel in the RacePage to test it:
...
<ContentPage ...
x:Class="Slugrace.Views.RacePage"
Padding="5">
...
<!--Bets/Results panel-->
<Border
...
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:Results />
</Border>
...
I think this code is pretty straightforward. Now that we have most of the pieces of the puzzle, let’s
start a game simulation.
Game Simulation
Before we discuss animation, we can only simulate the races. So, for now, the winners will be
picked randomly right after a race begins. In the final version each race will be animated and it will
take some time to complete.
The game begins when all players have placed valid bets on the slugs and the Go button is pressed.
This is when the first race begins. Depending on which ending condition was chosen, there may be
a different number of races. Anyway, we have to bind the Go button in the Bets content view to a
StartRace command and implement the command in the GameViewModel. Here’s the Bets panel:
...
<ContentView ...
x:Class="Slugrace.Controls.Bets">
...
<Button
...
IsEnabled="{Binding AllPlayersAreValid}"
Command="{Binding StartRaceCommand}" />
</Grid>
</ContentView>
322
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class GameViewModel : ObservableObject
{
...
partial void OnGameChanged(Game? value)
...
[RelayCommand]
void StartRace()
{
// Start race
RaceStatus = RaceStatus.Started;
RunRace();
}
}
This method just sets the RaceStatus to Started and calls the RunRace method (yet to be
implemented). Additionally, if we chose the Time ending condition, it starts the gameTimer before
the first race. Speaking of which…
Yes, we need a timer to count down the time of the game. When the time is up, the game will be
over. We’ll add a timer of type IDispatcherTimer like so:
...
public partial class GameViewModel : ObservableObject
{
readonly IDispatcherTimer gameTimer;
...
public GameViewModel()
{
gameTimer = Application.Current!.Dispatcher.CreateTimer();
gameTimer.Interval = TimeSpan.FromSeconds(1);
gameTimer.Tick += (sender, e) =>
{
if (TimeRemaining > TimeSpan.Zero)
{
TimeElapsed += TimeSpan.FromSeconds(1);
}
if (TimeRemaining == TimeSpan.Zero
&& RaceStatus == RaceStatus.Finished)
{
CheckForGameOver();
}
};
323
...
WeakReferenceMessenger.Default.Register<PlayerBetAmountChangedMessage>(this, (r, m) =>
OnBetAmountChangedMessageReceived(m.Value));
...
}
...
[RelayCommand]
void StartRace()
...
}
The timer object is created in the constructor. The Tick event will fire every second (we set the
Interval property to this amount of time) and will increase the TimeElapsed property, which will
automatically decrease the TimeRemaining property.
When the time is up, so when the TimeRemaining property equals zero and the RaceStatus is
Finished, the CheckForGameOver method is called. We’ll implement this method in a moment.
For now, let’s see what happens next.
...
public partial class GameViewModel : ObservableObject
{
...
void StartRace()
...
void RunRace()
{
// random winner slug
var random = new Random();
RaceWinnerSlug = Slugs[random.Next(Slugs.Count)];
RaceWinnerSlug.IsRaceWinner = true;
HandleSlugsAfterRace();
HandlePlayersAfterRace();
FinishRace();
}
}
In this method, naturally only for now, a random slug is selected to be the winner. The
IsRaceWinner property of the RaceWinnerSlug property is set to true. Then three methods are
called to handle the slugs and the players after the race and to finish the race. Let’s have a look at
how they are implemented:
324
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class GameViewModel : ObservableObject
{
...
void RunRace()
...
void HandleSlugsAfterRace()
{
foreach (var slug in Slugs)
{
if (slug != RaceWinnerSlug)
{
slug.IsRaceWinner = false;
}
slug.RecalculateStats(RaceNumber);
}
}
void HandlePlayersAfterRace()
{
foreach (var player in Players)
{
player.CalculateMoney(RaceWinnerSlug!);
if (player.CurrentMoney == 0)
{
player.IsBankrupt = true;
}
PlayersStillInGame = PlayersStillInGame
.Where(p => !p.IsBankrupt).ToObservableCollection();
}
}
void FinishRace()
{
RaceStatus = RaceStatus.Finished;
CheckForGameOver();
}
}
The slug stats are recalculated. The players’ money is calculated and if all money is lost, the
IsBankrupt property is set to true. The ObservableCollection of PlayersStillInGame is
populated with just the players who did not go bankrupt.
In the FinishRace method, the RaceStatus is set to Finished. When this happens, the
WinnerInfo content view is displayed. Here’s the WinnerInfo view:
...
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Slugrace"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Controls.WinnerInfo">
325
<ContentView.Resources>
...
<toolkit:EnumToBoolConverter x:Key="raceStatusConverter" />
</ContentView.Resources>
<Grid
IsVisible="{Binding RaceStatus,
Converter={StaticResource raceStatusConverter},
ConverterParameter={x:Static local:RaceStatus.Finished}}"
RowDefinitions=".6*, *, 3*">
<Label
Text="The winner is"
FontSize="{OnPlatform 28, Android=10}" />
<Label
Grid.Row="1"
Text="{Binding RaceWinnerSlug.Name}"
FontSize="{OnPlatform 36, Android=12}" />
<Image
Grid.Row="2"
Source="{Binding RaceWinnerSlug.ImageUrl}" />
</Grid>
</ContentView>
From now on, the real winner slug will be shown, not just Speedster like up to now.
Then a method is called to check whether, by any chance, the conditions are met to end the game.
And these conditions will be different for each ending condition. Here’s how the
CheckForGameOver method is implemented:
...
public partial class GameViewModel : ObservableObject
{
...
void FinishRace()
...
void CheckForGameOver(bool endedManually = false)
{
// This is for the case when the game is ended manually
if (endedManually)
{
GetWinners();
GameOverReason = "You ended the game manually.";
EndGame();
}
326
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here we have a GameOverReason property that will store the text that will be displayed in the
GameOverPage. This text will tell us why the game is over. Naturally, there may be different
reasons.
The endedManually parameter is for the case when we end the game manually by clicking the End
Game button in the race screen. We’ll see to this soon.
We also have two methods, GetWinners and EndGame. Let’s implement the GameOverReason
property and the two methods:
...
public partial class GameViewModel : ObservableObject
{
...
public bool? AllPlayersAreValid => Players?.All(p => p.PlayerIsValid);
[ObservableProperty]
private string gameOverReason = string.Empty;
...
327
void GetWinners()
{
int maxMoney = PlayersStillInGame.Max(p => p.CurrentMoney);
void EndGame()
{
gameTimer.Stop();
IsShowingFinalResults = true;
gameOverPageDelayTimer.Start();
IsShowingFinalResults = false;
// Navigate to GameOverPage
await Shell.Current.GoToAsync($"{nameof(GameOverPage)}",
new Dictionary<string, object>
{
{"Game", this }
});
};
}
}
The GetWinners method just picks the player or players who won the most money and adds them
to the Winners list.
The EndGame method seems more interesting. Here the gameTimer is stopped and the
IsShowingFinalResults property is set to true. Let’s add this property now:
...
public partial class GameViewModel : ObservableObject
{
...
private string gameOverReason = string.Empty;
[ObservableProperty]
private bool isShowingFinalResults;
public GameViewModel()
...
328
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Then another timer is started (we’ll implement it in a minute) and stopped after a couple seconds.
Finally, we navigate to the GameOverPage.
In order for the navigation to work, let’s register the route in the AppShell.xaml.cs file:
...
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(RacePage), typeof(RacePage));
Routing.RegisterRoute(nameof(GameOverPage), typeof(GameOverPage));
}
}
For the GameOverPage we’ll create a GameOverViewModel in the ViewModels folder. Here’s the
code:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Slugrace.Views;
using System.Text;
namespace Slugrace.ViewModels;
[QueryProperty(nameof(Game), "Game")]
public partial class GameOverViewModel : ObservableObject
{
[ObservableProperty]
private GameViewModel? game;
[ObservableProperty]
private string gameOverReason = string.Empty;
[ObservableProperty]
private int originalNumberOfPlayers;
[ObservableProperty]
private List<PlayerViewModel> winners = [];
[ObservableProperty]
public string winnerInfo = string.Empty;
329
WinnerInfo = Winners.Count switch
{
0 => OriginalNumberOfPlayers == 1
? "There are no winners in 1-player mode."
: "There is no winner!",
1 => OriginalNumberOfPlayers == 1
? $"You were playing in 1-player mode.\n"
+ $"You started with ${Winners[0].InitialMoney}, "
+ $"and you're leaving with ${Winners[0].CurrentMoney}."
: $"The winner is {Winners[0].Name}, "
+ $"having started with ${Winners[0].InitialMoney}, "
+ $"leaving with ${Winners[0].CurrentMoney}.",
_ => $"There's a tie. The joint winners are:\n\n"
+ $"{DisplayWinners()}"
};
}
return displayMessage.ToString();
}
[RelayCommand]
async Task RestartGame()
{
// Navigate to SettingsPage
await Shell.Current.GoToAsync($"//{nameof(SettingsPage)}");
}
}
This is the page we’re navigating to from the RacePage. We pass the GameViewModel object, so we
need to add the QueryProperty attribute, just like before. As the GameViewModel object is not
available in the constructor, we’re using it in the OnGameChanged method. We also define the
DisplayWinners method and the RestartGame method that will be bound to from the Play Again
button.
330
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
using Slugrace.ViewModels;
...
public partial class GameOverPage : ContentPage
{
public GameOverPage(GameOverViewModel gameOverViewModel)
{
InitializeComponent();
BindingContext = gameOverViewModel;
}
}
Let’s not forget to register the the GameOverPage and the GameOverViewModel with the
dependency service in the MauiProgram.cs file:
...
public static class MauiProgram
{
...
builder.Services.AddTransient<GameViewModel>();
builder.Services.AddTransient<GameOverPage>();
builder.Services.AddTransient<GameOverViewModel>();
return builder.Build();
}
}
331
About the other timer. It’s used to delay the navigation to the GameOverPage so that you can view
the results of the last race. Without it, you wouldn’t be able to see the results because the
GameOverPage would be navigated to immediately.
...
public partial class GameViewModel : ObservableObject
{
...
readonly IDispatcherTimer gameTimer;
readonly IDispatcherTimer gameOverPageDelayTimer;
...
public GameViewModel()
{
...
gameOverPageDelayTimer = Application.Current!.Dispatcher.CreateTimer();
gameOverPageDelayTimer.Interval = TimeSpan.FromSeconds(3);
...
OK, we know what happens if the ending conditions are met. But if they’re not, we can click the
Next Race button in the Results view to call a NextRace method in GameViewModel. Here it is:
...
public partial class GameViewModel : ObservableObject
{
...
void EndGame()
...
[RelayCommand]
void NextRace()
{
RaceStatus = RaceStatus.NotYetStarted;
RaceNumber++;
Here the race number is increased, the race status is reset to NotYetStarted and each player is
reset.
332
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentView ...
x:Class="Slugrace.Controls.Results">
...
<Button
Grid.Row="2"
Text="Next Race"
Margin="0, 0, 0, 5"
IsEnabled="{Binding IsShowingFinalResults,
Converter={StaticResource invertedBoolConverter}}"
Command="{Binding NextRaceCommand}" />
</Grid>
</ContentView>
The button is disabled after the last race and before the GameOverPage appears. This is to avoid
clicking on the Next Race button when there shouldn’t be a next race anymore.
Before we start the simulation, let’s take care of a couple minor issues in the RacePage. First of all,
let’s make sure the Bets and Results panels are swapped correctly. Actually, there are going to be
three panels, of which only one can be displayed at any given time. Besides the Bets and Results
panels, there will also be the Race Started panel. It will be visible while the race is going on. At this
moment the race won’t take any time and we’ll see the results immediately, but later it will take a
few seconds while the slugs are animated. This is when this panel will be visible.
Anyway, let’s implement all the three panels in the RacePage now:
...
<ContentPage ...
x:Class="Slugrace.Views.RacePage"
Padding="5">
...
<!--Racetrack-->
...
<!--Bets panel-->
<Border
IsVisible="{Binding RaceStatus,
Converter={StaticResource raceStatusConverter},
ConverterParameter={x:Static local:RaceStatus.NotYetStarted}}"
Grid.Row="{OnPlatform 2, Android=0}"
Grid.ColumnSpan="4"
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:Bets />
</Border>
333
HorizontalOptions="{OnPlatform Android=Fill}">
<Label
Text="RACE STARTED"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="50"
CharacterSpacing="2" />
</Border>
<!--Results panel-->
<Border
IsVisible="{Binding RaceStatus,
Converter={StaticResource raceStatusConverter},
ConverterParameter={x:Static local:RaceStatus.Finished}}"
Grid.Row="{OnPlatform 2, Android=0}"
Grid.ColumnSpan="4"
HorizontalOptions="{OnPlatform Android=Fill}">
<controls:Results />
</Border>
</Grid>
</ContentPage>
Secondly, the End Game and Instructions buttons should be disabled unless the RaceStatus is
Finished. Here’s how it’s implemented:
...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:local="clr-namespace:Slugrace"
xmlns:controls="clr-namespace:Slugrace.Controls"
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:GameViewModel"
x:Class="Slugrace.Views.RacePage"
Padding="5">
...
<!--the buttons-->
...
<Button
Text="End Game"
IsEnabled="{Binding RaceStatus,
Converter={StaticResource raceStatusConverter},
ConverterParameter={x:Static local:RaceStatus.Finished}}" />
<Button
Text="Instructions"
IsEnabled="{Binding RaceStatus,
Converter={StaticResource raceStatusConverter},
ConverterParameter={x:Static local:RaceStatus.Finished}}" />
<Button
...
Let’s now run the app and see if everything works as expected. First on Windows, then on
Android. Let’s say we’ll have three players and the game should end not later than after 1 minute.
Here’s the SettingsPage:
334
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
If you now hit the Ready button, you’ll navigate to the RacePage:
The Go button is disabled because we have to set the bet amount for each player and select the
slugs, for example like this:
335
If we now hit the Go button, we’ll immediately see the slug that won and the stats will be updated.
Also, the timer will start counting down. We can run as many races as we can within one minute:
When the time is up, we automatically navigate to the GameOverPage and see the results:
If we now click the Play Again button, we’ll navigate to the SettingsPage and all the settings
there will be exactly the same as before. This is good if you want to save some time. Usually the
same people are going to play again, so they may want to keep their names and other settings. But
it’s totally up to you. If you want, you can change the settings.
336
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
337
Ending the Game Manually
Sometimes you may want to end the game earlier than set at the beginning. All you have to do is
hit the End Game button in the RacePage.
...
public partial class GameViewModel : ObservableObject
{
...
private bool isShowingFinalResults;
[ObservableProperty]
private bool gameEndedManually;
...
void NextRace()
...
[RelayCommand]
void EndGameManually()
{
CheckForGameOver(true);
}
...
}
This method just calls the CheckForGameOver method with the endedManually argument set to
true. Now we have to bind the Command parameter of the button in RacePage to this method:
...
<ContentPage ...>
...
<!--the buttons-->
<VerticalStackLayout
...
<Button
Text="End Game"
IsEnabled="{Binding RaceStatus,
Converter={StaticResource raceStatusConverter},
ConverterParameter={x:Static local:RaceStatus.Finished}}"
Command="{Binding EndGameManuallyCommand}" />
<Button
...
The button will be enabled after each race, so there must be at least one race. Now if we run the app
and press the button, the game will end before the ending condition we decided on is actually met.
338
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
InstructionsPage
We haven’t created the InstructionsPage yet. Let’s create a very basic one for now so that we can
navigate to it. Add a new ContentPage to the Views folder, name it InstructionsPage, and
implement it like so:
<Shell.BackButtonBehavior>
<BackButtonBehavior IsEnabled="False" IsVisible="False" />
</Shell.BackButtonBehavior>
<VerticalStackLayout>
<Label
Text="INSTRUCTIONS"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button
Text="Back"
Command="{Binding NavigateBackCommand}" />
</VerticalStackLayout>
</ContentPage>
using Slugrace.ViewModels;
namespace Slugrace.Views;
Let’s also register the page with the dependency service in MauiProgram.cs:
...
public static class MauiProgram
{
...
builder.Services.AddTransient<GameOverViewModel>();
builder.Services.AddTransient<InstructionsPage>();
return builder.Build();
...
339
In the GameViewModel, let’s define two methods, one for navigating to the InstructionsPage and
one for navigating back:
...
public partial class GameViewModel : ObservableObject
{
...
void EndGameManually()
...
[RelayCommand]
async Task SeeInstructions()
{
// Navigate to InstructionsPage
await Shell.Current.GoToAsync($"{nameof(InstructionsPage)}");
}
[RelayCommand]
async Task NavigateBack()
{
await Shell.Current.GoToAsync("..");
}
}
using Slugrace.Views;
namespace Slugrace;
The last piece of the puzzle is to bind the Instructions button in RacePage to the SeeInstructions
method:
340
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
The button will be enabled after each race. Let’s run the app and hit the Instructions button. This
will take us to the new page:
So, our game simulation works. But it’s just a simulation. In the real game the slugs will be
animated. In order to implement this, we need to learn about animations in .NET MAUI. This is the
topic of the next chapter.
341
Chapter 19 – Animation
code: https://github.com/prospero-apps/Slugrace/tree/chapter19
In the previous chapter we created a simple game simulation where the slugs win randomly
without moving at all. But in real races, slugs (or, more commonly, horses or greyhounds) do have
to move to win, or even to finish the race. Our slugs are no exception to this rule. In this chapter
we’ll implement animations and make the slugs run.
We’re going to create a couple animations. Not only are the slugs going to move from one end of
the racetrack to the other, but they’re also going to have some moving parts, like the tentacles with
the eyes.
But before we implement any of that, let’s have a look at some theory. Let’s see how animation
works in .NET MAUI and what we can do with it. We’ll make use of the TestPage at first, so that
we can practice the new stuff. Then we’ll implement the animations in our app.
Basic Animations
Animations consist in a gradual change of a property between two values over a period of time.
The four basic animations supported by .NET MAUI are:
- FadeTo
- TranslateTo
- RotateTo
- ScaleTo
But there are more, like RelScaleTo, RotateXTo or ScaleYTo, to mention just a few. They can be
used with VisualElement objects. The animations listed above are extension methods of the
ViewExtensions class. The methods are asynchronous and return a Task<bool> object. If an
animation completes, the return value is false. If it’s canceled, the return value is true.
We can use the await keyword to combine animations sequentially. These are so-called compound
animations. If we don’t use the await keyword, we can combine the animations to run
simultaneously. These are so-called composite animations.
We have to keep in mind, too, that on Android animations can be disabled to save power. If this is
the case, the animations immediately jump to their finished state.
342
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Let’s modify the TestPage to contain just a Label, a BoxView and a Button:
<VerticalStackLayout>
<Label x:Name="label"
Text="Test Page"
HorizontalOptions="Center"
Margin="0, 0, 0, 100"
FontSize="40" />
<BoxView x:Name="box"
Color="Red"
WidthRequest="150"
HeightRequest="100" />
<Button
x:Name="button"
Text="Start Animation"
Margin="0, 100, 0, 0"
Clicked="Button_Clicked" />
</VerticalStackLayout>
</ContentPage>
We’ll use the button to start our animations. We can now simplify the TestViewModel class like so:
using CommunityToolkit.Mvvm.ComponentModel;
namespace Slugrace.ViewModels;
We’re going to use the code-behind file to implement the animations. And now let’s have a look at
some of the basic animations one by one.
Fading
Most animation methods take two parameters. The first parameter is the target value and the
second parameter is the duration of the animation in milliseconds.
Let’s make the box fade out in three seconds. The method takes the current value of the Opacity
343
property (which is 1) for the start of the animation and fades out from this value to 0:
using Slugrace.ViewModels;
namespace Slugrace.Views;
Translation
To move an element from one location to another we use the TranslateTo method. It takes three
parameters. The first two are for the TranslationX and TranslationY properties, and the last one
is the duration of the animation.
Let’s translate the box 200 device-independent units to the right and 50 units down over a period of
4 seconds:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await box.TranslateTo(200, 50, 4000);
}
}
344
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
So, let’s rotate the label 15 degrees in clockwise direction over a period of 2 seconds:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await label.RotateTo(15, 2000);
}
}
In order to rotate around the X and Y axes, we respectively use the RotateXTo and RotateYTo
methods. Let’s rotate around the X axis first:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await label.RotateXTo(45, 2000);
}
}
345
Here’s the result:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await label.RotateYTo(45, 2000);
}
}
The methods above rotate from the current value of Rotation, RotationX or RotationY to the
target values. Let’s set the Rotation property in the XAML file to 45:
...
<ContentPage ...>
<VerticalStackLayout>
<Label ...
Rotation="45" />
...
346
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Let’s use the RotateTo method and pass the angle of 90 degrees as the first argument:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await label.RotateTo(90, 2000);
}
}
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await label.RelRotateTo(90, 2000);
}
}
347
Scaling and Relative Scaling
The scaling methods work pretty much the same as the rotating ones. We have the ScaleTo
method to scale the element uniformly in both directions, the ScaleXTo and ScaleYTo methods to
scale it along just one axis, and RelScaleTo for relative scaling.
All these methods take the scaling factor as the first parameter and duration as the second.
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await box.ScaleTo(.5, 2000);
}
}
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await box.ScaleXTo(2, 2000);
}
}
348
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the rotating and scaling animations above, the transformation is always performed relative to the
center of an element. But it doesn’t have to be the case.
Anchors
We can set the center of rotation or scaling by using the AnchorX and AnchorY properties. The
values should be between 0 (left for AnchorX and top for AnchorY) to 1 (right for AnchorX and
bottom for AnchorY). The default value of either property is 0.5, which corresponds to the center of
the visual element.
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
box.AnchorX = 0;
box.AnchorY = 0;
await box.RotateTo(45, 2000);
}
}
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
button.AnchorX = 1;
button.AnchorY = 1;
await button.ScaleTo(2, 2000);
}
}
349
Here’s what we should see when the
animation completes.
Compound Animations
Multiple animations can be combined sequentially. We just have to use the await keyword. Let’s
create a compound animation on the box:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await box.TranslateTo(-100, 0, 1000);
await box.RotateTo(45, 2000);
await box.TranslateTo(0, -50, 1000);
await box.RelRotateTo(45, 2000);
await box.ScaleTo(2, 500);
}
}
The whole compound animation takes 6.5 seconds to complete. First the box moves to the left.
When this movement is complete, the rotation starts, and so on.
Composite Animations
Animations can also run simultaneously. This will happen if we omit the await keyword. Let’s
translate, rotate and scale the box all at the same time:
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
box.TranslateTo(-100, 0, 3000);
box.RotateTo(45, 3000);
box.ScaleTo(2, 3000);
}
}
350
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
If you run the app now and press the button, all three animations will start simultaneously. The
whole composite animation will take 3 seconds to complete.
We can also mix and match. Let’s create a composite animation where some single animations are
awaited and others are not:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
label.ScaleXTo(3, 4000);
await label.RotateTo(15, 2000);
await label.RotateTo(0, 2000);
}
}
Here the label will be scaled and rotated simultaneously. The two rotation animations will be run
sequentially at the same time as the scaling animation.
We can also create composite animations where multiple asynchronous methods run concurrently.
Let’s have a look at it next.
The Task.WhenAny method completes when any of the methods in its collection completes. Have a
look:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await Task.WhenAny
(
label.TranslateTo(400, 0, 5000),
label.ScaleTo(2, 2000)
);
351
Here we have a Task.WhenAny method with two tasks. The first task starts translating the label to
the right, the second task starts scaling it. The second task needs two seconds to complete. When it
completes, the Task.WhenAny method also completes. While the translation is still running (it
needs three more seconds to complete), the second ScaleTo method can start running. The second
scaling and the translation will complete at the same time.
Whereas the Task.WhenAny method completes when any of its tasks completes, the Task.WhenAll
method completes when all the tasks it contains complete. Have a look:
...
public partial class TestPage : ContentPage
{
...
private async void Button_Clicked(object sender, EventArgs e)
{
await Task.WhenAll
(
box.ScaleXTo(3, 3000),
box.RotateTo(360, 500),
box.TranslateTo(100, 0, 7000)
);
Here we have three tasks with different durations. The box first completes the rotation, then the
scaling on the X axis, and then the translation. Only when the last task completes, the
Task.WhenAll method completes. As last runs the scaling to zero.
Canceling Animations
We can easily cancel animations by calling the CancelAnimations method. Let’s add another
button to the TestPage:
352
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
box.TranslateTo(100, 0, 5000);
box.RotateTo(90, 4000);
box.ScaleTo(2, 4000);
}
When you press the first button, all three animations start running simultaneously. When you press
the second button, they are immediately canceled.
Easing Functions
Animations can run with constant speed or with different speed and acceleration as they proceed.
They can start slow and then accelerate. They can start fast and then decelerate. You can create
your own patterns or use the ones we get out of the box.
There’s a class that contains easing functions that enable just that. The class is called Easing and it
contains the following easing functions: BounceIn, BounceOut, CubicIn, CubicInOut, CubicOut,
Linear, SinIn, SinInOut, SinOut, SpringIn and SpringOut. The -In suffix is used if the effect
should be visible at the beginning of the animation. The suffix -Out - if at the end. If both suffixes
are used, the effect is visible at both the beginning and the end.
To use an easing function, we just add it as the last argument to the animation method. Let’s check
it out.
We’ll create a simple translation for our box. By default the Linear easing function is used. All the
animations we’ve created so far use this default value. Let’s bounce the box at the beginning of the
animation. To this end, we can use the BounceIn easing function:
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
box.TranslateTo(300, 0, 3000, Easing.BounceIn);
}
...
353
Run the animation to see how it works.
Or let’s smoothly accelerate the box at the beginning and decelerate it at the end of the animation:
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
box.TranslateTo(300, 0, 3000, Easing.SinInOut);
}
...
Feel free to check out the other easing functions. They also work with rotation or scaling.
Custom Animations
We can use the Animation class to create custom animations. We use the Commit method to run an
animation. Let’s create and run an animation in the TestPage. Let’s first remove the second button
and the related code in the code-behind. The code should now look like so:
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
var animation = new Animation(v => box.Rotation = v, 0, 45);
animation.Commit(
this,
"RotationAnimation",
16,
3000,
Easing.SinInOut,
(v, c) => box.Rotation = 0,
() => true);
}
}
We pass a callback, the start value and the end value as arguments to the Animation class’s
constructor. As the callback we use a lambda expression that sets the box’s Rotation property to
values between the start value (0 degrees) and the end value (45 degrees).
354
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Then we run the animation. The Commit method takes a few parameters. The first parameter is the
owner of the animation. This is the visual element the animation is set on or another visual element,
such as the page. In our case we pass this for the page.
The second parameter is the name of the animation. The name and the owner are used jointly to
uniquely identify the animation.
The third parameter is the rate of the animation. It’s expressed in milliseconds. In our case the rate
is 16, so 16 milliseconds must elapse between calls to the callback method we passed to the
Animation object’s constructor.
The fourth parameter is the duration of the animation. In our case it’s 3000 milliseconds, so 3
seconds.
The fifth parameter is the easing function we want to use for the
animation.
Now run the app and hit the button. You’ll see the box rotate, then go
to the state with Rotation equal to zero. And this pattern will repeat.
Child Animations
We can combine and synchronize multiple animations in a parent-child relationship. To do that, we
create a parent animation and add child animations to it. For the child animations we specify the
time frame in which they are supposed to run relative to the duration of the parent animation. Let’s
create an example to demonstrate how it works.
We’ll create a parent animation using the parameterless constructor. Next, we’ll create three child
animations using the constructor with parameters. By the way, this time we’ll define the easing
functions in the constructor, which is an alternative way of doing it. Finally, we’ll add the child
animations to the parent animation. Here’s the code:
355
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
var parentAnimation = new Animation();
parentAnimation.Add(0, 1, childAnimation1);
parentAnimation.Add(0, .5, childAnimation2);
parentAnimation.Add(.5, 1, childAnimation3);
parentAnimation.Commit(
this,
"LabelAnimation",
16,
5000,
null,
(v, c) => label.Text += " |",
() => true);
}
}
The first two arguments passed to the Add method are the time frames
for the child animations. They must be between 0 and 1. So, the first
animation will run for the entire duration of the parent animation. The
second child animation will run during the first half of the parent
animation, and the third child animation will run during the second
half.
If you now run the app, you’ll see the label scale up for 5 seconds.
During the first 2.5 seconds it will rotate in clockwise direction, during
the next 2.5 seconds it will rotate back. Additionally, we change the
Text property of the label after the animation is finished (before the
next repetition).
356
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
new Animation
{
{ 0, 1, new Animation(
v => label.ScaleY = v, 1, 2, Easing.CubicIn) },
{ 0, .5, new Animation(
v => label.Rotation = v, 0, 30, Easing.SpringIn) },
{ .5, 1, new Animation(
v => label.Rotation = v, 30, 0, Easing.SpringOut) }
}.Commit(
this,
"LabelAnimation",
16,
5000,
null,
(v, c) => label.Text += " |",
() => true);
}
}
Animating Properties
Visual elements implement the IAnimatable interface, which contains the Animate method. We
can use this method to create and start an animation.
For example, we can animate the label’s FontSize property like so:
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
label.Animate(
"Move",
new Animation(x => label.FontSize = x, 5, 50),
16,
5000);
}
}
357
The Animate method takes a couple arguments. The first argument is the name, which is used as a
unique key. The second argument is the animation that we want to run. Here we’re animating the
FontSize property of the label from 5 to 50. The two remaining arguments are the rate and the
duration of the animation. There are also other overloaded versions of the Animate method.
...
public partial class TestPage : ContentPage
{
...
private void Button_Clicked(object sender, EventArgs e)
{
this.Animate(
"Rotate",
new Animation(x => Rotation = x, 0, 360),
16,
3000);
}
}
Animations in MVVM
Our project uses the MVVM pattern. Let’s have a look at how to create animations in an app that
uses that pattern. First, make sure your TestViewModel class looks like so:
358
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Slugrace.ViewModels;
[ObservableProperty]
private bool isRunning;
[RelayCommand]
async Task Animate()
{
IsRunning = true;
await Task.Delay(5000);
IsRunning = false;
}
}
Here we have two properties and a method. The first property will indicate whether an element
(like the box in our case) should be visible or hidden. The second property will be used to indicate
whether a simulated task is running.
The method will just set the IsRunning property to true, wait for 5 seconds and then set it to
false. It will be used as a command.
...
<ContentPage ...>
<VerticalStackLayout>
<Label x:Name="label"
Text="0"
...
<BoxView x:Name="box"
...
<Button
x:Name="button"
Text="Start Animation"
Margin="0, 100, 0, 0"
Command="{Binding AnimateCommand}" />
</VerticalStackLayout>
</ContentPage>
It hasn’t changed much. The label’s Text is now initially set to “0” and we don’t have the Clicked
event anymore. The button’s Command property now binds to the method we created in the view
model.
359
Finally, let’s modify the code-behind:
using Slugrace.ViewModels;
namespace Slugrace.Views;
BindingContext = testViewModel;
vm = (TestViewModel)BindingContext;
vm.PropertyChanged += Vm_PropertyChanged;
}
label.Text = ((int)value).ToString();
if ((int)value % 20 == 0)
{
if (vm.IsHidden)
{
ShowBox();
}
else
{
HideBox();
}
}
}
void HideBox()
{
box.Opacity = 0;
vm.IsHidden = true;
}
void ShowBox()
{
box.Opacity = 1;
vm.IsHidden = false;
}
360
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here we have an instance of our view model. In the constructor we create an animation. We define
a callback and the start and end values.
If we now run the app and press the button, we’ll see the box rotate
and the label text change. Every 20 degrees the box will disappear or
reappear.
361
We’re also going to animate the slugs’ tentacles. Rotating them will definitely bring some life to the
slugs.
Finally, we’re going to implement some animations related to accidents that may happen to the
slugs.
...
<ContentView ...>
<AbsoluteLayout>
<!--Racetrack-->
<Image
x:Name="track"
...
<!--Speedster-->
<controls:SlugImage
x:Name="speedster"
...
<!--Trusty-->
<controls:SlugImage
x:Name="trusty"
...
<!--Iffy-->
<controls:SlugImage
x:Name="iffy"
...
<!--Slowpoke-->
<controls:SlugImage
x:Name="slowpoke"
...
The slug running animations will be implemented in the code behind because they transform the
graphics. But we’ll need a link to the GameViewModel in order to correlate the states of the slugs
and of the game with the animated graphics. Let’s create that link by adding an instance of
GameViewModel in the code-behind:
using Slugrace.ViewModels;
namespace Slugrace.Controls;
public TrackImage()
{
InitializeComponent();
}
}
362
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
We’ll create four animations, one for each slug. The slugs will run along the racetrack. This is why
we need to know the length of the racetrack. However, the Width property of the track is
unavailable in the constructor because at the time when the constructor is run, the children are not
yet laid out.
To make sure that the Width property is set, we have to override the LayoutChildren method. We
create a trackLength variable to hold the distance the slugs should cover.
Also in the LayoutChildren method we instantiate the four animations. As the first argument, we
pass methods to them that we define below. These methods just take care of translating the
SlugImage objects. As the start and end values we pass 0 and trackLength respectively:
...
namespace Slugrace.Controls;
Animation? speedsterMovement;
Animation? trustyMovement;
Animation? iffyMovement;
Animation? slowpokeMovement;
public TrackImage()
{
InitializeComponent();
}
363
private void SlowpokeMoveForward(double value)
{
slowpoke.TranslationX = value;
}
}
We have the animations, but we haven’t run them yet. When do we want them to run? Well, they
should start when the race begins. And this happens when the race status changes to Started.
So, we need a way to tell when the status changes. To this end, we’ll use the PropertyChanged
event handler. Let’s add a BindingContextChanged event handler to the TrackImage class:
...
<ContentView ...
x:DataType="viewmodels:GameViewModel"
BindingContextChanged="ContentView_BindingContextChanged"
x:Class="Slugrace.Controls.TrackImage">
...
In the code-behind, let’s assign the binding context to the view model instance and let’s assign a
Vm_PropertyChanged method to the event handler:
...
namespace Slugrace.Controls;
...
public partial class SlugViewModel : ObservableObject
{
...
public double PreviousOdds
...
[ObservableProperty]
private uint runningTime;
public SlugViewModel()
...
}
364
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class GameViewModel : ObservableObject
{
...
private bool gameEndedManually;
[ObservableProperty]
private uint raceTime;
[ObservableProperty]
private uint minTime;
[ObservableProperty]
private uint finishTime;
public GameViewModel()
...
The first property, RaceTime, is the time of the entire race, so until the last slug finishes the race.
The second property, MinTime, is the time the fastest slug needs to finish the race.
The FinishTime property is the time when the race should be considered resolved because the
winner is already known, but the slugs are still running.
And now let’s take care of all the different scenarios as far as the change of the race status is
concerned.
So, if the status changes to Started, we first define the running times of all the slugs. The shorter
the time, the faster the slug will run. The first slug, for example, will have a random running time
between 3000 and 7000 milliseconds. This is going to be the statistically fastest slug. No other slug
will be able to cover the distance in 3 seconds.
Then we create an array of the running times. Next, we set the three time properties we just added
to the GameViewModel.
We’re going to use these properties in the RunRace method in the view model. With this in place,
we start the animations.
365
If the race status changes to NotYetStarted, which happens after each race when we hit the Next
Race button, the slugs’ TranslationX property is reset and the images move to where they started
off.
If the status changes to Finished, the animations are canceled and the slugs stop running. Here
you can see all three cases:
using Slugrace.ViewModels;
using System.ComponentModel;
namespace Slugrace.Controls;
uint[] runningTimes = [
vm.Slugs[0].RunningTime,
vm.Slugs[1].RunningTime,
vm.Slugs[2].RunningTime,
vm.Slugs[3].RunningTime
];
vm.RaceTime = runningTimes.Max();
vm.MinTime = runningTimes.Min();
vm.FinishTime = (uint)(.79 * vm.MinTime);
speedsterMovement?.Commit(
this,
"moveSpeedster",
16,
vm.Slugs[0].RunningTime,
Easing.Linear,
null,
() => false);
trustyMovement?.Commit(
this,
"moveTrusty",
16,
vm.Slugs[1].RunningTime,
Easing.Linear,
null,
() => false);
366
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
iffyMovement?.Commit(
this,
"moveIffy",
16,
vm.Slugs[2].RunningTime,
Easing.Linear,
null,
() => false);
slowpokeMovement?.Commit(
this,
"moveSlowpoke",
16,
vm.Slugs[3].RunningTime,
Easing.Linear,
null,
() => false);
}
else if (vm.RaceStatus == RaceStatus.NotYetStarted)
{
speedster.TranslationX = 0;
trusty.TranslationX = 0;
iffy.TranslationX = 0;
slowpoke.TranslationX = 0;
}
else
{
this.AbortAnimation("moveSpeedster");
this.AbortAnimation("moveTrusty");
this.AbortAnimation("moveIffy");
this.AbortAnimation("moveSlowpoke");
}
}
}
}
...
public partial class GameViewModel : ObservableObject
{
...
async Task RunRace()
{
await Task.Delay((int)FinishTime);
if (RaceWinnerSlug != null)
{
RaceWinnerSlug.IsRaceWinner = true;
}
HandleSlugsAfterRace();
HandlePlayersAfterRace();
FinishRace();
}
367
private void HandleSlugsAfterRace()
...
The method is now asynchronous. Its return type is no longer void, but async Task. This means
the method must be awaited everywhere in the code where it’s called. So, the StartRace method,
in which it’s called, must be asynchronous, too:
...
public partial class GameViewModel : ObservableObject
{
...
await RunRace();
}
In the RunRace method, we introduce a delay of a couple seconds. During this time, the race status
is Started and the slugs are running. The RaceWinnerSlug is now set to the fastest slug. We have
the winner, but the slugs are still running until the slowest one finishes. Then we continue like
before.
There is also one change in the WinnerInfo class. We want the winner info to be visible not when
all slugs finish the race, but rather when the winner is known, so after the fastest slug crosses the
finish line.
So, we bind the IsVisible property to RaceWinnerSlug. We also use the IsNotNullConverter
from the Community Toolkit, so that the WinnerInfo view should be visible only when there is a
RaceWinnerSlug:
...
<ContentView ...>
<ContentView.Resources>
...
<toolkit:IsNotNullConverter x:Key="raceResolvedConverter" />
</ContentView.Resources>
368
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
<Grid
IsVisible="{Binding RaceWinnerSlug,
Converter={StaticResource raceResolvedConverter}}"
...
We also must make sure that the RaceWinnerSlug is set to null in the NextRace method in
GameViewModel:
...
public partial class GameViewModel : ObservableObject
{
...
void NextRace()
{
RaceStatus = RaceStatus.NotYetStarted;
RaceNumber++;
RaceWinnerSlug = null;
This way the WinnerInfo view will be invisible when the next race begins.
If we now run the app and then start the first race, the slugs start running:
369
The moment the first slug crosses the finish line, the WinnerInfo is displayed:
The slugs continue running until all of them have completed the race:
Here’s what you should see if you ran the app on Android.
Now the race is finished and we can hit the Next Race button.
This will move the slugs to the beginning of the track.
We’re going to implemenet this animation using child animations. Each slug has two tentacles.
Let’s give them names so that we can reference them in code. Here’s the SlugImage class in XAML:
370
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentView ...>
<AbsoluteLayout>
<Image
x:Name="leftEye"
Source="{Binding EyeImageUrl}"
...
<Image
x:Name="rightEye"
Source="{Binding EyeImageUrl}"
...
<Image
Source="{Binding BodyImageUrl}"
...
namespace Slugrace.Controls;
new Animation()
{
{0, .5, new Animation(v => leftEye.Rotation = v, 0, -30) },
{0, .5, new Animation(v => rightEye.Rotation = v, 0, 30) },
{.5, 1, new Animation(v => leftEye.Rotation = v, -30, 0) },
{.5, 1, new Animation(v => rightEye.Rotation = v, 30, 0) }
}.Commit(this, "eyeRotation", 16, rotationSpeed, null, null, () => true);
}
}
Here we have four child animations, two for the left eye and two
for the right eye. Each eye rotates in one direction during the first
half of the animation and then back. This animation is repeated.
Now, as soon as the slugs appear on the screen, they rotate their
tentacles.
Great, we have animations in our app now. But how cool would
it be if we had some background music and sound effects as well? Well, this is what we’re going to
see to in the next chapter.
371
Chapter 20 – Sound
code: https://github.com/prospero-apps/Slugrace/tree/chapter20
A game without audio seems incomplete. We’re going to implement background music and sound
effects in our app. To do that, we need a plugin.
Plugin.Maui.Audio
The plugin is called Plugin.Maui.Audio. Let’s install the NuGet package:
We have to register it in the dependency injection container, so go to the MauiProgram.cs file and
add the following code:
...
using Plugin.Maui.Audio;
namespace Slugrace;
builder.Services.AddSingleton(AudioManager.Current);
return builder.Build();
}
}
Let’s create a view model that will handle sound in our app.
372
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
SoundViewModel
Create a new class in the ViewModels folder and name it SoundViewModel. We inject the
AudioManager in the constructor:
using CommunityToolkit.Mvvm.ComponentModel;
using Plugin.Maui.Audio;
namespace Slugrace.ViewModels;
Let’s also register the view model with the dependency service in the MauiProgram class:
...
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
...
builder.Services.AddTransient<InstructionsPage>();
builder.Services.AddTransient<SoundViewModel>();
builder.Services.AddSingleton(AudioManager.Current);
return builder.Build();
}
}
Sound Files
Before we move on, we have to add the sound files we need to the Raw folder inside the Resources
folder. You will find these files on Github.
373
In the Accidents folder we have sounds that will be associated
with the accidents that may happen to the slugs. We’re going to
implement them in the next chapter.
In the Game folder there are sounds that will be used when a
race begins, while the slugs are running and when the game is
over. There’s also a file that will be used as background music.
In the Slugs Winning folder there are files that will be played
when the slugs win a race. Each slug will use a different sound
to express his joy after winning a race.
All sounds that I’m using for the project were downloaded from
Zapsplat. I renamed them so that you can easily see what each
file is going to be used for.
In the table below you can see both the names that we are going
to use in the project and the original ones.
Go.mp3 esm_8bit_explosion_medium_with_voice_bomb_boom_blast_cannon_retro_old_school_classic_cartoon.mp3
The Background Music.mp3 file is going to be used for the background music.
The Slugs Running.mp3 file will start playing when a race begins and loop until all slugs have
finished the race.
The Game Over.mp3 file will play just once when the Game Over screen appears.
The Go.mp3 file will be played just once the moment a race starts.
374
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Each of these sounds will be played just once when the corresponding slug wins a race, so you will
hear just one of them in each race.
Asleep.mp3 zapsplat_human_man_elderly_snore_20087.mp3
Blind.mp3 horror_eyes_gouged_out_001.mp3
Devoured.mp3 fork_media_horror_monster_roar_growl_reverberant.mp3
Drown.mp3 zapsplat_nature_water_bubble_rise_underwater_002_20093.mp3
Electroshock.mp3 sound_design_effect_electricity_electric_arc.mp3
Grass.mp3 zapsplat_animals_slug_eat_designed_001_42438.mp3
Overheat.mp3 Blastwave_FX_AcidBurnSizzle_S011SF.3.mp3
Each of these sounds will be associated with one of the accidents. The names of the files are pretty
self-explanatory. But, as mentioned above, we’re not going to use these sounds for now.
Playing Sounds
We have one audio manager, but we can have as many audio players as we need. We can create a
375
separate audio player for each sound.
An audio player is an object that implements the IAudioPlayer interface. We use it to play the
sound, stop it, check if it’s playing, set its volume, make it loop, and so on.
Audio players are disposable objects. We should dispose of them after they’re done playing their
sound. This way we’ll avoid memory leaks.
In our app we’ll create an audio player to handle the background music. We’ll name it just
audioPlayer. We’ll also create a list of audio players for the sound effects. We’ll name the list
effectPlayers. Each time a sound should be played, a new audio player will be created and
added to that list. It will be disposed of when it’s done its job.
So, let’s add the audio players to the SoundViewModel. Let’s also create the methods to play the
sounds:
...
public partial class SoundViewModel : ObservableObject
{
private readonly IAudioManager audioManager;
private IAudioPlayer? audioPlayer;
private List<IAudioPlayer> effectPlayers;
audioPlayer = audioManager.CreatePlayer(
await FileSystem.OpenAppPackageFileAsync(path));
audioPlayer.Loop = true;
audioPlayer.Volume = .3;
audioPlayer.Play();
}
effectPlayers.Add(player);
376
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
player.Volume = volume;
player.Loop = loop;
player.Play();
}
player.Dispose();
}
effectPlayers.Clear();
}
}
We create an audio player for each sound. We pass a stream to the CreatePlayer method. To
create a stream from a file we use the FileSystem.OpenAppPackageFileAsync method. We also
set the Loop property of the player.
The Stop and Clean methods, among other things, dispose of the audio players. We’ll use the
former in a minute. We use the Clean method inside the PlaySound method to dispose of any
audio players that might still be there and clear the list.
Background Music
Let’s create an instance of the SoundViewModel in the GameViewModel:
...
public partial class GameViewModel : ObservableObject
{
SoundViewModel soundViewModel;
377
...
public GameViewModel(SoundViewModel soundViewModel)
{
...
gameOverPageDelayTimer.Interval = TimeSpan.FromSeconds(3);
this.soundViewModel = soundViewModel;
The background music should start playing in a loop the moment the game starts, so after we click
the Ready button in the SettingsPage. So, let’s call the PlayBackgroundMusic method in the
OnGameChanged method:
...
public partial class GameViewModel : ObservableObject
{
...
partial void OnGameChanged(Game value)
{
...
PlayersStillInGame = Players.ToObservableCollection();
[RelayCommand]
async Task StartRace()
...
The background music will now play throughout the entire game. It will stop playing only when
the game is over. During the short period of time after the last race and before the GameOverPage is
shown, the sound should attenuate. Let’s create the Attenuate method in the SoundViewModel:
...
public partial class SoundViewModel : ObservableObject
{
...
public void Clean()
...
378
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
This method will gradually decrease the volume until we can’t hear anything.
Now, let’s call this method in the EndGame method in the GameViewModel. We’ll also call the Stop
method we defined in the SoundViewModel to stop the background music if it is still playing for
some reason and to dispose of the audio player:
...
public partial class GameViewModel : ObservableObject
{
...
await soundViewModel.Attenuate();
soundViewModel.Stop();
}
[RelayCommand]
void NextRace()
...
}
We’ve introduced some asynchrony to the code, so, consequently, we have to make some of the
methods we defined before asynchronous:
...
public partial class GameViewModel : ObservableObject
{
...
public GameViewModel(SoundViewModel soundViewModel)
{
...
gameTimer.Tick += async (sender, e) =>
{
...
if (TimeRemaining == TimeSpan.Zero
&& RaceStatus == RaceStatus.Finished)
{
await CheckForGameOver();
}
};
...
async Task FinishRace()
{
RaceStatus = RaceStatus.Finished;
379
await CheckForGameOver();
}
If you now run the app and hit the Ready button, you’ll be able to hear the background music.
380
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Sound Effects
We’re going to implement some sound effects in the game. There are going to be sounds associated
with accidents that happen to the slugs, but we’ll take care of them in the next chapter. For now,
let’s add the other sound effects.
The first effect we want to implement is the sound that we hear when the game is over. Let’s play
the sound in the EndGame method:
...
public partial class GameViewModel : ObservableObject
{
...
// Navigate to GameOverPage
await Shell.Current.GoToAsync($"{nameof(GameOverPage)}",
...
};
soundViewModel.Stop();
}
[RelayCommand]
void NextRace()
...
}
We’ll still hear the sound when the GameOverPage shows up.
The next two sound effects are the shot that should be heard when a race begins and the sound that
can be heard while the slugs are running. The latter sound should be looped. We’ll implement
them in the StartRace and RunRace methods:
...
public partial class GameViewModel : ObservableObject
{
...
async Task StartRace()
{
...
if (RaceNumber == 1 && GameEndingCondition == EndingCondition.Time)
381
...
await RunRace();
}
await Task.Delay((int)FinishTime);
...
soundViewModel.Clean();
HandleSlugsAfterRace();
...
}
We also call the Clean method in the RunRace method because we don’t want the running sound
to continue in the next race.
Finally, we’ll add a victory sound for each slug. Each slug will rejoice in a different way when they
win.
namespace Slugrace.Models;
Next, in the SettingsViewModel, where the slugs are created in the StartGame method, let’s
assign the sound file names to this new property:
...
public partial class SettingsViewModel : ObservableObject
{
...
async Task StartGame()
{
// Populate the Game object
382
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
game.Slugs =
[
new Slug
{
...
BodyImageUrl = "speedster_body.png",
WinSound = "Speedster Win.mp3"
},
new Slug
{
...
BodyImageUrl = "trusty_body.png",
WinSound = "Trusty Win.mp3"
},
new Slug
{
...
BodyImageUrl = "iffy_body.png",
WinSound = "Iffy Win.mp3"
},
new Slug
{
...
BodyImageUrl = "slowpoke_body.png",
WinSound = "Slowpoke Win.mp3"
}
];
...
...
public partial class SlugViewModel : ObservableObject
{
...
public double PreviousOdds
...
383
[ObservableProperty]
private uint runningTime;
...
Now, back in the GameViewModel, let’s revisit the OnGameChanged method and assign the sounds:
...
public partial class GameViewModel : ObservableObject
{
...
partial void OnGameChanged(Game value)
{
...
Slugs =
[
new()
{
...
PreviousOdds = value.Slugs[0].BaseOdds,
WinSound = value.Slugs[0].WinSound
},
new()
{
...
PreviousOdds = value.Slugs[1].BaseOdds,
WinSound = value.Slugs[1].WinSound
},
new()
{
...
PreviousOdds = value.Slugs[2].BaseOdds,
WinSound = value.Slugs[2].WinSound
},
new()
{
...
PreviousOdds = value.Slugs[3].BaseOdds,
WinSound = value.Slugs[3].WinSound
},
];
...
}
...
This victory joy sound should be played when we have a slug winner, so in the RunRace method:
...
public partial class GameViewModel : ObservableObject
{
...
384
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
if (RaceWinnerSlug != null)
{
_ = soundViewModel.PlaySound("Slugs Winning", RaceWinnerSlug.WinSound);
}
Now play the game and wait until each of the slugs wins at least once so that you can hear all the
different sounds.
If you like the sound effects, that’s great. These effects will be always turned on. If you really can’t
stand them, just mute your device.
But you will be able to turn the background music on and off. There’s one button in the RacePage
we haven’t talked about so far. Let’s have a look at it now.
We’ll create a command for this button in the GameViewModel. However, as we want to keep all the
sound-related code in one place, which is the SoundViewModel, let’s start by adding some
properties and a method there.
...
public partial class SoundViewModel : ObservableObject
{
...
private List<IAudioPlayer> effectPlayers;
[ObservableProperty]
private double volume;
[ObservableProperty]
private bool muted;
385
public SoundViewModel(IAudioManager audioManager)
{
this.audioManager = audioManager;
effectPlayers = [];
Volume = .3;
}
...
public async Task Attenuate()
...
public void MuteUnmute()
{
double defaultVolume = Volume > 0 ? Volume : .3;
Muted = !Muted;
if (audioPlayer != null)
{
audioPlayer.Volume = Volume;
}
}
}
So, here we can see two new properties: Volume and Muted. The former is used for the background
music. We also have a new method, MuteUnmute. As the name suggests, it will mute the
background music if it’s on and turn it back on if it’s off.
...
public partial class GameViewModel : ObservableObject
{
...
private uint finishTime;
[ObservableProperty]
private bool muted;
386
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
[RelayCommand]
public void MuteUnmute()
{
soundViewModel.MuteUnmute();
Muted = soundViewModel.Muted;
}
}
Now we can bind them in the RacePage XAML file. For simplicity’s sake, we’ll move most of the
Sound button’s properties to a style. We’ll duplicate the Sound button. One will be visible when the
background music is on, the other when it’s off. The image on either button will be different.
...
<ContentPage ...>
<ContentPage.Resources>
<toolkit:EnumToBoolConverter x:Key="raceStatusConverter" />
<toolkit:InvertedBoolConverter x:Key="invertedBoolConverter" />
<Style x:Key="soundStyle" TargetType="Button">
<Setter Property="WidthRequest"
Value="{OnPlatform 125, Android=150}" />
<Setter Property="HorizontalOptions"
Value="{OnPlatform Default=End, Android=Center}" />
<Setter Property="Command"
Value="{Binding MuteUnmuteCommand}" />
</Style>
</ContentPage.Resources>
...
<!--the buttons-->
<VerticalStackLayout
...
<Button
Text="Instructions"
...
<Button
Style="{StaticResource soundStyle}"
ImageSource="{AppThemeBinding Light=sound_on.png,
Dark=sound_on_dark.png}"
IsVisible="{Binding Muted,
Converter={StaticResource invertedBoolConverter}}" />
<Button
Style="{StaticResource soundStyle}"
ImageSource="{AppThemeBinding Light=sound_off.png,
Dark=sound_off_dark.png}"
IsVisible="{Binding Muted}" />
</VerticalStackLayout>
...
Let’s run the app. When we leave the SettingsPage and enter the RacePage, the background
music is on. The Sound button looks like before (the image on the left).
387
When you press the button, the
music will be off and the other
Sound button will be visible
(the image on the right).
If you now decide the music wasn’t that bad, you can always unmute it.
In the next chapter we’ll implement even more sound effects. They will be associated with
accidents that can happen to the slugs. We’ll also see how to use popups to deliver a short message
and offer some options to the user.
388
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Chapter 21 - Popups
code: https://github.com/prospero-apps/Slugrace/tree/chapter21
You may sometimes want to show a popup message to the user. This can be just a simple message,
or you can add a button or two to let the user do something, like confirm, cancel, etc.
In our app a popup will be displayed when the user clicks the Quit button in the GameOverPage. It
will ask the user if they are sure they want to quit the game. There will be two buttons. One of them
will be used to cancel and close the popup. The other will actually quit the app.
We’re also going to show a popup when an accident happens. We haven’t implemented accidents
yet, so we’ll take care of it first. Then we’ll implement the popups.
There are a couple ways to create a popup. We’ll be using the Community Toolkit Popup class. As
our popups are going to be rather simple, we won’t create view models for them. Instead we’ll
implement the logic in the code-behind.
Quit Popup
We’re going to create a couple popups in our app, so let’s put them in a folder. Create a new folder
in the app root and name it Popups. In it, create a new ContentView and name it QuitPopup. Here’s
the code:
389
WidthRequest="300"
Margin="20"
Clicked="CancelButtonClicked"/>
<Button
Text="Yes, I'm sure. Quit."
WidthRequest="300"
Margin="20"
Clicked="QuitButtonClicked"/>
</FlexLayout>
</VerticalStackLayout>
</toolkit:Popup>
We also set the CanBeDismissedByTappingOutsideOfPopup property to False. For this and the
Color property to work, the class must inherit from the CommunityToolkit.Maui.Views.Popup
class, which we’re going to implement in a moment.
This way we won’t be able to close the popup by clicking anywhere outside it. We want to close it
only when a button is clicked.
Speaking of which… There are two buttons. We define the Clicked event for each of them. Let’s
now have a look at the code-behind to see what should happen when the buttons are clicked:
using CommunityToolkit.Maui.Views;
namespace Slugrace.Popups;
So, as I just mentioned, the class now inherits from Popup. The first button just closes the popup.
The second button quits the app.
Our popup is ready to use. We now have to make the Quit button in the GameOverPage open it:
390
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
<ContentPage ...
x:Class="Slugrace.Views.GameOverPage">
...
<Button
Text="Play Again"
...
<Button
Text="Quit"
Clicked="QuitButtonClicked" />
</FlexLayout>
...
using CommunityToolkit.Maui.Views;
using Slugrace.Popups;
using Slugrace.ViewModels;
namespace Slugrace.Views;
This is how we display a popup. Let’s run the app, finish one race and hit the End Game button to
navigate to the GameOverPage. Now let’s click the Quit button. We should see the popup:
391
This is a modal popup. If you now click the first button, it will
close the popup and you’ll see the GameOverPage again. If you
click the second button, you will actually quit the app.
Let’s now run the app on Android. Here it looks like so (look
left).
Accidents
Accidents happen. Also to slugs. During a race an accident may
happen to one of the slugs. It’s not going to be very frequent, but
still, it may influence the result of the race when it happens. I’m
using the name ‘accident’ here to keep things simple, but some
of them may be convenient for the slugs and even help them
win.
Each accident will have a graphical representation, which you can see below. Later, we’ll also add
sound effects to the accidents.
So, here are the accidents that may happen to the slugs:
Broken Leg
If this accident happens, the slug stops moving and doesn’t even
make it to the finish line. This race is lost.
Overheating
Heart Attack
392
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Power Grass
Falling Asleep
Falling asleep during a race is never a good idea. The slug stops
and starts breathing slowly. You can hear him snore and see some
changes in his size when the sleeping animation is played.
Going Blind
If a slug goes blind, he doesn’t stop running, but without his eyes he
starts staggering, so winning the race becomes pretty difficult.
Drowning in a Puddle
Electroshock
Turning Back
Slug Monster
393
So, these are all the accidents that may happen in the game. Now
we’ll have a look at the graphical assets we need for the accidents
and then we’ll implement the classes that we need to handle the
accidents.
Assets
As you can see, we’ll need some graphical assets for the
accidents. You can grab them from the Github repository. Create
an Accidents folder inside the Images folder and add them there.
Most of the images will be shared by all the slugs. The broken leg
images, however, will be different for each slug.
To use the images, just like we did before, we have to select them
all and set their Build Action property to MauiImage.
namespace Slugrace.Models;
394
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
We also defined an enumaration that will be used by this class. There are ten accident types that
may happen. The Name property will be used to set a name for each accident. The Headline
property will be set to a succinct description of the accident. The Sound property will be set to the
path to the sound file associated with a particular accident. The TimePosition property will store
information about the time when the accident is supposed to happen, counting from the race start.
Next, let’s create a view model.
AccidentViewModel
Let’s create a new class in the ViewModels folder and name it AccidentViewModel. Here’s the code:
using CommunityToolkit.Mvvm.ComponentModel;
using Slugrace.Models;
namespace Slugrace.ViewModels;
395
{ AccidentType.Grass, [
"just found magic grass. It's famous for powering slugs up!",
"just about to speed up after eating magic grass!",
"powered up by magic grass found unexpectedly on the track!",
"seems to be full of beans after having eaten the magic grass on his way!",
"heading perhaps even for victory after his magic grass meal!"
] },
{ AccidentType.Asleep, [
"just fell asleep for a while after the long and wearisome running!",
"having a nap. He again has chosen just the perfect time for that!",
"sleeping instead of running. It's getting one of his bad habits!",
"always takes a short nap at this time of the day, no matter what he's doing!",
"knows how important sleep is. Even if it's not the best time for that!"
] },
{ AccidentType.Blind, [
"gone blind. Now staggering to find his way!",
"shouldn't have been reading in dark. Now it's hard to find the way!",
"temporarily lost his eyesight. Now it's difficult for him to follow the track!",
"trying hard to find his way after going blind on track!",
"staggering to finish the race after going blind because of an infection!"
] },
{ AccidentType.Puddle, [
"drowning in a puddle of water!",
"beaten by yesterday's heavy rainfalls. Just drowning in a puddle!",
"shouldn't have skipped his swimming lessons. Drowning in a puddle now!",
"has always neglected his swimming lessons. How wrong he’s been!",
"disappearing in a puddle of water formed afted heavy rainfall!"
] },
{ AccidentType.Electroshock, [
"speeding up after being struck by lightning!",
"powered up by lightning. Now running really fast!",
"hit by electric discharge. Seems to have been powered up by it!",
"accelerated by a book of electric discharges!",
"now running much faster after being struck by lightning!"
] },
{ AccidentType.TurningBack, [
"has forgotten to turn off the gas. Must hurry home before it's too late!",
"just received a phone call. His house is on fire. No time to lose!",
"seems to have more interesting stuff to do than racing.",
"seems to have lost orientation. Well, how these little brains work!",
"has left his snack in the kitchen. He won't race when he's hungry!"
] },
{ AccidentType.Devoured, [
"devoured by the infamous slug monster. Bad luck!",
"just swallowed by the terrible slug monster!",
"next on the long list of the slug monster's victims!",
"has never suspected he's gonna end up as a snack!",
"devoured by the legendary slug monster from the nearby swamps!"
] }
};
396
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Name))]
[NotifyPropertyChangedFor(nameof(Headline))]
[NotifyPropertyChangedFor(nameof(Sound))]
private AccidentType accidentType;
[ObservableProperty]
private SlugViewModel? affectedSlug;
397
In the constructor we create a new instance of Accident of a specified type. We use a couple
dictionaries with the accident type as the key. They contain accident names, headlines, sounds and
accident durations. Then we can easily define the properties. These are readonly properties where a
value from a dictionary is returned by key. The Headline property is set to a random string from
the Headlines dictionary, so that we don’t see the same message over and over again.
We also define a couple properties that are not readonly. One of them is TimePosition. Another is
AffectedSlug. The latter will store a SlugViewModel instance representing the slug to which the
accident is going to happen.
We’re going to use the AccidentViewModel in the GameViewModel to control accidents. Let’s do it
next.
Controlling Accidents
We’ll need access to AccidentViewModel and some other stuff in the GameViewModel, so let’s
modify it:
...
public partial class GameViewModel : ObservableObject
{
SoundViewModel soundViewModel;
private readonly IPopupService? popupService;
public AccidentViewModel? AccidentViewModel;
[ObservableProperty]
private uint secondTime;
[ObservableProperty]
private bool muted;
[ObservableProperty]
private bool accidentShouldHappen;
[ObservableProperty]
private IAudioPlayer? accidentSoundPlayer;
[ObservableProperty]
private uint afterAccidentTime = 0;
...
}
398
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Here we added a couple properties we’re going to need. Some of them are related to different times
associated with accidents. We also have properties that will be used for accident sounds and
popups. We’ll create a special popup to be used with accidents in a moment.
We’ll discuss all these properties in more detail when we need them.
Let’s have a look at the StartRace method first. At this moment the slugs run pretty fast. This is
good for testing purposes, but they shouldn’t be that fast in the actual game, so I doubled the slugs’
running times to make them run slower. Now it’s time to implement the accident logic:
...
public partial class GameViewModel : ObservableObject
{
...
async Task StartRace()
{
Slugs[0].RunningTime = (uint)new Random().Next(6000, 14000);
Slugs[1].RunningTime = (uint)new Random().Next(8000, 14000);
Slugs[2].RunningTime = (uint)new Random().Next(8000, 14000);
Slugs[3].RunningTime = (uint)new Random().Next(10000, 16000);
// Which one?
AccidentType[] accidentTypes = (AccidentType[])Enum.GetValues(
typeof(AccidentType));
AccidentViewModel.AffectedSlug.RunningTime =
AccidentViewModel.TimePosition
+ AccidentViewModel.Duration
+ AfterAccidentTime;
}
399
if (AccidentViewModel.AccidentType == AccidentType.Electroshock)
{
AfterAccidentTime = AccidentViewModel.AffectedSlug.RunningTime / 4;
AccidentViewModel.AffectedSlug.RunningTime =
AccidentViewModel.TimePosition
+ AccidentViewModel.Duration
+ AfterAccidentTime;
}
}
}
else
{
thereIsAnAccident = false;
}
RaceTime = runningTimes.Max();
MinTime = runningTimes.Min();
FinishTime = (uint)(.79 * MinTime);
SecondTime = runningTimes.Order().ToArray()[1];
AccidentShouldHappen = thereIsAnAccident;
// Start race
RaceStatus = RaceStatus.Started;
...
The particular parts of the code above are commented in a clear way, so that you know what
they’re for.
So, first of all, we must decide whether there should be an accident at all. We don’t want any
accidents to happen in the first couple races so that the user can get used to the game. After the fifth
race, an accident will happen if the accidentExpected variable is set to true. This variable is set
randomly by comparing a random integer between 0 and 3 to 0. If it’s true, an accident is expected
to happen. Otherwise, no accident will happen in a given race.
If an accident is to happen, we must decide which one. This is also set randomly. And then the
AccidentViewModel object of the specified type is created.
The time when the accident should happen is also set randomly, within a certain range.
For some accidents, the running time of the affected slug is adjusted.
400
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Then we gather all the running times of the slugs and set the RaceTime property to the max
running time. We also set the MinTime property, the FinishTime property and the SecondTime
property. The SecondTime property will be needed when the fastest slug is stopped by an accident
and can’t continue the race.
After all these properties are set, the RunRace method is called. Let’s have a look at it next:
...
public partial class GameViewModel : ObservableObject
{
...
private async Task RunRace()
{
_ = soundViewModel.PlaySound("Game", "Slugs Running.mp3", .5, true);
await Task.Delay((int)FinishTime);
RaceWinnerSlug = Slugs.Where(
s => s.RunningTime == MinTime).FirstOrDefault();
if (AccidentShouldHappen
&& AccidentViewModel != null
&& RaceWinnerSlug == AccidentViewModel.AffectedSlug
&& AccidentViewModel.Duration == 0)
{
RaceWinnerSlug = Slugs.Where(
s => s.RunningTime == SecondTime).FirstOrDefault();
}
if (RaceWinnerSlug != null)
{
_ = soundViewModel.PlaySound("Slugs Winning", RaceWinnerSlug.WinSound);
...
First, we modify the finish time if the fastest slug has an accident, and in particular, if it’s an
accident where the Duration property is set to zero. These are accidents in which the slug stops
401
running and never reaches the finish line, so the winner is the slug with the second time.
Fine, but where are the accidents handled actually? Well, let’s see.
...
<ContentView ...>
<AbsoluteLayout x:Name="layout">
<!--Racetrack-->
<Image
...
And now let’s open the code-behind. This is where the actual accident animations will be handled.
We’ll need some variables for accident handling. Let’s define and initialize them now:
...
public partial class TrackImage : ContentView
{
...
Animation? slowpokeMovement;
// accident handling
SlugImage? slugImage;
string? runningAnimationName;
string? brokenLegImage;
string overheatBodyImage;
string overheatEyeImage;
string heartImage;
string grassImage;
string puddleImage;
string boltImage;
string monsterImage;
Image? accidentImage;
Animation? accidentAnimation;
public TrackImage()
{
InitializeComponent();
overheatBodyImage = "overheat_body.png";
overheatEyeImage = "overheat_eye.png";
402
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
heartImage = "heart_attack.png";
grassImage = "grass.png";
puddleImage = "puddle.png";
boltImage = "electroshock.png";
monsterImage = "slug_monster.png";
}
Next, let’s modify the Vm_PropertyChanged method to also take accidents into account. We’ll do it
by defining a method, HandleAccident, and calling it inside Vm_PropertyChanged whenever the
AccidentShouldHappen property in the GameViewModel changes. Besides, we’ll add another
method, HandleRunning that will hold most of the current functionality, plus it will reset before
each race the properties modified by an accident, like ZIndex, Opacity or ScaleX and also remove
the accident image. It will also restart the affected slug’s eye rotation because in some accidents the
eyes stop moving.
Let’s start with the eye rotation actually. To make things easier, let’s add a StartEyeRotation
method to the SlugImage class:
namespace Slugrace.Controls;
public SlugImage()
{
InitializeComponent();
LeftEye = leftEye;
RightEye = rightEye;
StartEyeRotation();
}
403
public void StartEyeRotation()
{
rotationSpeed = (uint)new Random().Next(2000, 4000);
eyeAnimation.Commit(
this,
"eyeRotation",
16,
rotationSpeed,
null,
null,
() => true);
}
}
...
public partial class TrackImage : ContentView
{
...
private void Vm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(vm.RaceStatus))
{
HandleRunning();
}
else if (e.PropertyName == nameof(vm.AccidentShouldHappen))
{
_ = HandleAccident();
}
}
if (layout.Contains(accidentImage))
{
if (accidentImage != null && accidentImage.ZIndex != 0)
{
accidentImage.ZIndex = 0;
}
404
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
layout.Remove(accidentImage);
}
if (accidentAnimation != null)
{
this.AbortAnimation("accidentAnimation");
}
if (slugImage != null)
{
slugImage.StartEyeRotation();
if (slugImage.ZIndex != 0)
{
slugImage.ZIndex = 0;
}
if (slugImage.Opacity != 1)
{
slugImage.Opacity = 1;
}
if (slugImage.ScaleX != 1)
{
slugImage.ScaleX = 1;
}
}
}
else
{
this.AbortAnimation("moveSpeedster");
this.AbortAnimation("moveTrusty");
this.AbortAnimation("moveIffy");
this.AbortAnimation("moveSlowpoke");
}
}
405
else if (vm.AccidentViewModel.AffectedSlug == vm.Slugs[2])
{
slugImage = iffy;
runningAnimationName = "moveIffy";
brokenLegImage = "broken_leg_iffy.png";
}
else if (vm.AccidentViewModel.AffectedSlug == vm.Slugs[3])
{
slugImage = slowpoke;
runningAnimationName = "moveSlowpoke";
brokenLegImage = "broken_leg_slowpoke.png";
}
// accident type
switch (vm.AccidentViewModel.AccidentType)
{
case AccidentType.BrokenLeg:
await HandleBrokenLeg();
break;
case AccidentType.Overheat:
await HandleOverheat();
break;
case AccidentType.HeartAttack:
await HandleHeartAttack();
break;
case AccidentType.Grass:
await HandleGrass();
break;
case AccidentType.Asleep:
await HandleAsleep();
break;
case AccidentType.Blind:
await HandleBlind();
break;
case AccidentType.Puddle:
await HandlePuddle();
break;
case AccidentType.Electroshock:
await HandleElectroshock();
break;
case AccidentType.TurningBack:
await HandleTurningBack();
break;
case AccidentType.Devoured:
await HandleDevoured();
break;
default:
break;
}
}
}
}
As you can see, in the HandleAccident method we assign images and animations and call a
method to handle the accident, depending on which accident type is to happen.
Before we implement the particular methods to handle the accidents, we’ll have to create a popup
406
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Accident Popup
We’ll create a small popup that will appear if an accident happens. It will contain the image of the
slug affected by the accident and a headline. We’ll also create a separate view model for the popup.
Actually, let’s start with that. In the ViewModels folder add a new class and name it
AccidentPopupViewModel. Here’s the code:
using CommunityToolkit.Mvvm.ComponentModel;
namespace Slugrace.ViewModels;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HeadlineMessage))]
private string affectedSlugName = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HeadlineMessage))]
private string accidentHeadline = string.Empty;
Here we define a couple properties that will be required by the popup. The HeadlineMessage
property combines the name of the affected slug with the actual headline.
Next, let’s create the popup itself. Add a new class to the Popups folder and name it
AccidentPopup. Here’s the XAML file:
407
HorizontalOptions="Center"
VerticalOptions="Start"
Color="Black">
<VerticalStackLayout
Padding="10"
WidthRequest="500"
HeightRequest="240">
<Label
Text="BREAKING NEWS"
FontSize="30"
TextColor="Red" />
<Line
Stroke="CadetBlue"
StrokeThickness="1"
X1="10"
Y1="10"
X2="460"
Y2="10" />
<Grid
Margin="10"
ColumnDefinitions="1.5*, 3.5*">
<Image
Source="{Binding AffectedSlugImageUrl}" />
<Label
Grid.Column="1"
HorizontalTextAlignment="Start"
VerticalTextAlignment="Center"
Text="{Binding HeadlineMessage}"
FontSize="25"
FontAttributes="Bold,Italic"
TextColor="Red" />
</Grid>
</VerticalStackLayout>
</toolkit:Popup>
The popup will be centered horizontally and it will appear at the top of the window. There are a
couple elements: a label, a horizontal line and a grid with the image of the slug and the headline
message.
using CommunityToolkit.Maui.Views;
using Slugrace.ViewModels;
namespace Slugrace.Popups;
408
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
All we do here is set the binding context. Let’s also register the popup and the view model with the
dependency service in MauiProgram.cs:
...
public static class MauiProgram
{
...
builder.Services.AddSingleton(AudioManager.Current);
builder.Services.AddTransientPopup<AccidentPopup, AccidentPopupViewModel>();
return builder.Build();
}
}
We need a way to display the popup. To this end, we’ll add a DisplayAccidentPopup method to
the GaveViewModel:
...
public partial class GameViewModel : ObservableObject
{
...
public void MuteUnmute()
...
public void DisplayAccidentPopup()
{
if (popupService != null && AccidentViewModel != null)
{
popupService.ShowPopup<AccidentPopupViewModel>(
onPresenting: viewModel => viewModel.ShowAccidentInfo(AccidentViewModel));
}
}
}
We’re using an IPopupService here, which has the ShowPopup method that we can use. We have
to specify the view model we want to use. As we also want to pass data to the popup view model,
we have to use the onPresenting parameter, which is of the Action<TViewModel> delegate type.
Here the ShowAccidentInfo method is called that we defined in the view model.
Besides displaying the accident popup when an accident happens, we also should hear a sound
associated with the accident. Let’s take care of it next.
Accident Sound
As far as sound is concerned in our app, it’s the responsibility of the SoundViewModel, so let’s start
right there. We’ll modify the code slightly:
409
...
public partial class SoundViewModel : ObservableObject
{
...
private List<IAudioPlayer> effectPlayers;
private List<IAudioPlayer> loopingAccidentPlayers;
[ObservableProperty]
private double volume;
...
public SoundViewModel(IAudioManager audioManager)
{
this.audioManager = audioManager;
effectPlayers = [];
loopingAccidentPlayers = [];
Volume = .3;
}
...
public async Task PlaySound(
string folderName,
string fileName,
double volume = 1,
bool loop = false,
bool loopingAccidentSound = false)
{
...
var player = audioManager.CreatePlayer(
await FileSystem.OpenAppPackageFileAsync(path));
if (!loopingAccidentSound)
{
effectPlayers.Add(player);
}
else
{
loopingAccidentPlayers.Add(player);
}
player.Volume = volume;
...
}
...
public void Clean(bool loopingAccidentSound = false)
{
if (!loopingAccidentSound)
{
foreach (var player in effectPlayers)
{
if (player.IsPlaying)
{
player.Stop();
}
player.Dispose();
}
effectPlayers.Clear();
}
else
{
foreach (var player in loopingAccidentPlayers)
410
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
{
if (player.IsPlaying)
{
player.Stop();
}
player.Dispose();
}
loopingAccidentPlayers.Clear();
}
}
First, we add a list of IAudioPlayers called loopingAccidentPlayers to store sounds that should
be played continuously in a loop when an accident happens. This will be the case, for example,
with the HeartAttack accident when we will hear the heart beating until we hit the Next Race
button. The list is instantiated in the constructor.
We also modify the Clean method. It now also takes a loopingAccidentSound parameter.
Depending on its value, either the effectPlayers or the loopingAccidentPlayers list is cleared.
With that in place, let’s add two methods to the GameViewModel: one to play and the other to stop
the accident sound:
...
public partial class GameViewModel : ObservableObject
{
...
void NextRace()
{
soundViewModel.Clean();
soundViewModel.Clean(true);
RaceStatus = RaceStatus.NotYetStarted;
...
}
...
public void DisplayAccidentPopup()
...
public void PlayAccidentSound(bool loop = false, bool loopingAccidentSound = false)
{
if (AccidentViewModel != null)
{
_ = soundViewModel.PlaySound(
"Accidents",
AccidentViewModel.Sound,
411
loop: loop,
loopingAccidentSound: loopingAccidentSound);
}
}
We also call the Clean method in the NextRace method twice to clear both IAudioPlayer lists.
And now we’re ready to implement the particular accidents. However, accidents are supposed to
happen randomly and rather not too frequently, which makes it difficult and time-consuming to
test them. This is why we have to temporarily modify the accident-related code in the
GameViewModel.
Testing Accidents
We want to ensure two things to make testing accidents easier. First, the accident should happen in
each race. Secondly, we should be able to decide which accident happens.
To do the former, let’s comment out the line of code where the condition is checked whether an
accident should happen and use a condition that is always true, like 2 + 2 == 4.
To do the latter, let’s manually pass the index from the accidentTypes list. Let’s start with index 0,
which corresponds the Broken Leg accident:
...
public partial class GameViewModel : ObservableObject
{
...
async Task StartRace()
{
...
// Should there be an accident?
//if (RaceNumber > 5 && AccidentViewModel.Expected)
if (2 + 2 == 4)
{
// If so, then...
thereIsAnAccident = true;
// Which one?
AccidentType[] accidentTypes = (
AccidentType[])Enum.GetValues(typeof(AccidentType));
412
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
And now let’s implement the accidents one by one. Each accident will be implemented in a
separate asynchronous method in the TrackImage class. In order for the code to compile, let’s add
some temporary implementations of the methods (we’ll make them asynchronous later):
...
public partial class TrackImage : ContentView
{
...
private async Task HandleAccident()
{
...
// accident type
switch (vm.AccidentViewModel.AccidentType)
...
}
}
413
private Task HandleTurningBack()
{
throw new NotImplementedException();
}
When this time has elapsed, the running animation is canceled and the slug stops moving.
The BodyImageUrl property is set to brokenLegImage, which is different for each slug.
We also hear the accident sound and see the accident popup.
...
public partial class TrackImage : ContentView
{
...
private async Task HandleAccident()
...
private async Task HandleBrokenLeg()
{
if (vm != null
&& vm.AccidentViewModel != null
&& vm.AccidentViewModel.AffectedSlug != null
&& brokenLegImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
vm.AccidentViewModel.AffectedSlug.BodyImageUrl = brokenLegImage;
vm.PlayAccidentSound();
vm.DisplayAccidentPopup();
}
}
...
}
Let’s now run the app and start a race. After a while, one of the slugs will break his leg. Here’s
what it looks like with the popup on Windows and Android:
414
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
WINDOWS ANDROID
And here’s what it looks like when you dismiss the popup on Windows:
With the other accidents, I’ll show you what it looks like without the popup on Windows. On
Android it looks pretty much the same.
If we now hit the Next Race button, Iffy will start the next race with a broken leg, which isn’t what
we want. Remember: Whatever happens to the slugs in a race, they don’t suffer and they always
start the next race fully healed.
As the body image and the eye images of the slugs will be replaced in some accidents, like the body
image here, let’s add two properties to the Slug model to store the default values, so the ones the
slugs should always start with.
415
namespace Slugrace.Models;
...
public partial class SlugViewModel : ObservableObject
{
...
public string BodyImageUrl
...
public string DefaultEyeImageUrl
{
get => slug.DefaultEyeImageUrl;
set
{
if (slug.DefaultEyeImageUrl != value)
{
slug.DefaultEyeImageUrl = value;
OnPropertyChanged();
}
}
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(WinPercentage))]
private int currentRaceNumber;
...
Next, let’s go to the SettingsViewModel and set the properties for each slug there:
416
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
public partial class SettingsViewModel : ObservableObject
{
...
async Task StartGame()
{
// Populate the Game object
game.Slugs =
[
new Slug
{
...
EyeImageUrl = "speedster_eye.png",
BodyImageUrl = "speedster_body.png",
DefaultEyeImageUrl = "speedster_eye.png",
DefaultBodyImageUrl = "speedster_body.png",
WinSound = "Speedster Win.mp3"
},
new Slug
{
...
EyeImageUrl = "trusty_eye.png",
BodyImageUrl = "trusty_body.png",
DefaultEyeImageUrl = "trusty_eye.png",
DefaultBodyImageUrl = "trusty_body.png",
WinSound = "Trusty Win.mp3"
},
new Slug
{
...
EyeImageUrl = "iffy_eye.png",
BodyImageUrl = "iffy_body.png",
DefaultEyeImageUrl = "iffy_eye.png",
DefaultBodyImageUrl = "iffy_body.png",
WinSound = "Iffy Win.mp3"
},
new Slug
{
...
EyeImageUrl = "slowpoke_eye.png",
BodyImageUrl = "slowpoke_body.png",
DefaultEyeImageUrl = "slowpoke_eye.png",
DefaultBodyImageUrl = "slowpoke_body.png",
WinSound = "Slowpoke Win.mp3"
}
];
We also have to set the appropriate properties in the OnGameChanged method of the
GameViewModel:
417
...
public partial class GameViewModel : ObservableObject
...
partial void OnGameChanged(Game? value)
{
...
Slugs =
[
new()
{
...
EyeImageUrl = value.Slugs[0].EyeImageUrl,
BodyImageUrl = value.Slugs[0].BodyImageUrl,
DefaultEyeImageUrl = value.Slugs[0].DefaultEyeImageUrl,
DefaultBodyImageUrl = value.Slugs[0].DefaultBodyImageUrl,
WinNumber = 0,
...
},
new()
{
...
EyeImageUrl = value.Slugs[1].EyeImageUrl,
BodyImageUrl = value.Slugs[1].BodyImageUrl,
DefaultEyeImageUrl = value.Slugs[1].DefaultEyeImageUrl,
DefaultBodyImageUrl = value.Slugs[1].DefaultBodyImageUrl,
WinNumber = 0,
...
},
new()
{
...
EyeImageUrl = value.Slugs[2].EyeImageUrl,
BodyImageUrl = value.Slugs[2].BodyImageUrl,
DefaultEyeImageUrl = value.Slugs[2].DefaultEyeImageUrl,
DefaultBodyImageUrl = value.Slugs[2].DefaultBodyImageUrl,
WinNumber = 0,
...
},
new()
{
...
EyeImageUrl = value.Slugs[3].EyeImageUrl,
BodyImageUrl = value.Slugs[3].BodyImageUrl,
DefaultEyeImageUrl = value.Slugs[3].DefaultEyeImageUrl,
DefaultBodyImageUrl = value.Slugs[3].DefaultBodyImageUrl,
WinNumber = 0,
...
},
];
418
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Finally, let’s restore the images in the NextRace method in the GameViewModel to the default
values so that the default images are used in the next race. In case of the Broken Leg accident, we
didn’t replace the eye images, but in some other accidents they will be replaced, that’s why we
reset both images here. We also set AccidentViewModel to null because if there should be an
accident in the next race, a new instance will be created anyway. By default there is no accident in a
race, so we set AccidentShouldHappen to false:
...
public partial class GameViewModel : ObservableObject
{
...
void NextRace()
{
...
RaceWinnerSlug = null;
AccidentViewModel = null;
AccidentShouldHappen = false;
if (slug.EyeImageUrl != slug.DefaultEyeImageUrl)
{
slug.EyeImageUrl = slug.DefaultEyeImageUrl;
}
}
}
...
}
Now we’re done. The slug is healed. We can now move on to the next accident.
Overheat Accident
First of all, remember to change the accident type index in GameViewModel, so that the Overheat
accident is selected:
...
public partial class GameViewModel : ObservableObject
{
...
async Task StartRace()
{
...
var type = accidentTypes[1];
...
419
This is how we’ll be changing the accidents to test them one by one. Just remember to change the
index as we discuss each of the remaining accidents.
...
public partial class TrackImage : ContentView
{
...
private async Task HandleBrokenLeg()
...
private async Task HandleOverheat()
{
if (vm != null
&& vm.AccidentViewModel != null
&& vm.AccidentViewModel.AffectedSlug != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
slugImage.StopEyeRotation();
vm.AccidentViewModel.AffectedSlug.BodyImageUrl = overheatBodyImage;
vm.AccidentViewModel.AffectedSlug.EyeImageUrl = overheatEyeImage;
vm.PlayAccidentSound();
vm.DisplayAccidentPopup();
}
}
...
}
Just like before, when the time position is reached, the running animation is canceled. But this time,
we also want the eye rotation animation to stop. To this end, we define a StopEyeRotation
method in the SlugImage class:
...
public partial class SlugImage : ContentView
{
...
public void StartEyeRotation()
...
public void StopEyeRotation()
{
this.AbortAnimation("eyeRotation");
}
}
Then the body and eye images are replaced so that the slug really looks as if he was burned. We
also hear the sound and see the popup.
If we now run the app and start a race, we should see the accident in action:
420
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Naturally, the images will be reset in the following race. And now let’s move on to a more
complicated accident.
...
public partial class TrackImage : ContentView
{
...
private async Task HandleOverheat()
...
private async Task HandleHeartAttack()
{
if (vm != null
&& vm.AccidentViewModel != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
#if ANDROID
layout.SetLayoutBounds(accidentImage, new Rect(
slugImage.X + slugImage.TranslationX - .2 * slugImage.Width,
slugImage.Y - slugImage.Height,
accidentImage.Width,
accidentImage.Height));
#if WINDOWS
layout.SetLayoutBounds(accidentImage, new Rect(
slugImage.X + slugImage.TranslationX + .4 * slugImage.Width,
slugImage.Y,
accidentImage.Width,
accidentImage.Height));
421
accidentAnimation = new Animation()
{
{0, .24, new Animation(v => accidentImage.Scale = v, .8, .6) },
{.24, 1, new Animation(v => accidentImage.Scale = v, .6, .8) }
};
#endif
accidentAnimation?.Commit(
this,
"accidentAnimation",
16,
820,
Easing.CubicInOut,
null,
() => true);
vm.PlayAccidentSound(true, true);
vm.DisplayAccidentPopup();
}
}
...
}
So, again, when the time position is reached, the running animation is canceled. Then the accident
image representing a heart is added and positioned. Also, a heart beating animation is created. The
positioning of the image and the animation are different for each platform. But then the animation
is started the same way for both platforms. As always, there’s a sound (a looping one this time) and
we can see the popup.
Fine, we’re done with this accident. The next one is going to be even more complicated.
Grass Accident
The Grass accident consists of a couple parts. First the slug notices some grass and stops to eat. He
spends some time eating. The grass gives him more strength, so after the meal he starts running
faster than before. Here we have the grass image, which will be scaled and positioned differently
for each platform:
...
public partial class TrackImage : ContentView
{
...
private async Task HandleHeartAttack()
422
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
...
private async Task HandleGrass()
{
if (vm != null
&& vm.AccidentViewModel != null
&& vm.AccidentViewModel.AffectedSlug != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
#if ANDROID
accidentImage.ScaleX = .5;
accidentImage.ScaleY = .25;
accidentImage.Width,
accidentImage.Height));
#endif
#if WINDOWS
layout.SetLayoutBounds(accidentImage, new Rect(
slugImage.X + slugImage.TranslationX + slugImage.Width,
.9 * slugImage.Y,
accidentImage.Width,
accidentImage.Height));
#endif
accidentAnimation.Commit(
this,
"accidentAnimation",
16,
500,
Easing.CubicInOut,
null,
() => true);
vm.PlayAccidentSound(true, true);
vm.DisplayAccidentPopup();
await Task.Delay((int)vm.AccidentViewModel.Duration);
this.AbortAnimation("accidentAnimation");
if (layout.Contains(accidentImage))
{
layout.Remove(accidentImage);
}
423
vm.StopAccidentSound();
accidentAnimation.Commit(
this,
"accidentAnimation",
16,
vm.AccidentViewModel.AffectedSlug.RunningTime / 4,
Easing.Linear, null, () => false);
}
}
...
}
So, after the running animation is canceled, the grass image appears, the eating sound starts
playing, the popup appears, and a new animation is started. This animation scales the slug image
horizontally up and down so that it looks like the slug is eating.
Next, the grass image is removed and the eating sound is stopped. A running animation is started
so that the slug can finish the race. He now runs faster, but still isn’t sure (although more probable)
to win.
Here’s what the accident looks like halfway, when the grass image is visible:
Here the slug speeds up after a nice meal. But sometimes he just falls asleep…
Asleep Accident
Here’s the Asleep accident implementation:
...
public partial class TrackImage : ContentView
{
...
private async Task HandleGrass()
...
private async Task HandleAsleep()
{
if (vm != null
&& vm.AccidentViewModel != null
424
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
slugImage.StopEyeRotation();
accidentAnimation.Commit(
this,
"accidentAnimation",
16,
5600,
Easing.CubicInOut,
null,
() => true);
vm.PlayAccidentSound(true, true);
vm.DisplayAccidentPopup();
}
}
...
}
When this accident happens, the slug stops running and stops moving his tentacles. A new
animation is started in which the slug is scaled up and down, which looks like he’s breathing
steadily in his sleep. We can also hear a looping snoring sound.
This accident isn’t very spectacular. Here you can see the affected slug (Iffy) is slightly bigger than
the other slugs, but it’s hard to notice in a static image like below:
Blind Accident
From time to time, a slug may lose his tentacles and go blind. Here’s how this accident is
implemented:
425
...
public partial class TrackImage : ContentView
{
...
private async Task HandleAsleep()
...
private async Task HandleBlind()
{
if (vm != null
&& vm.AccidentViewModel != null
&& vm.AccidentViewModel.AffectedSlug != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
vm.AccidentViewModel.AffectedSlug.EyeImageUrl = string.Empty;
vm.PlayAccidentSound();
vm.DisplayAccidentPopup();
await Task.Delay((int)vm.AccidentViewModel.Duration);
slugImage.Rotation = 0;
this.AbortAnimation("accidentAnimation");
}
}
...
}
The slug doesn’t stop running when the accident happens. He just loses his eyes (the eye image is
set to an empty string) and starts moving in a seemingly chaotic way, just like Slowpoke in the
image below:
426
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Unfortunately, a slug may also fall into a puddle and drown. Let’s have a look at this accident next.
Puddle Accident
Let’s now implement the next accident. In this accident the slug drowns in a puddle of water:
...
public partial class TrackImage : ContentView
{
...
private async Task HandleBlind()
...
private async Task HandlePuddle()
{
if (vm != null
&& vm.AccidentViewModel != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
accidentImage = new Image
{
Source = puddleImage,
ZIndex = 1
};
slugImage.ZIndex = 2;
layout.Add(accidentImage);
#if ANDROID
accidentImage.ScaleX = .25;
accidentImage.ScaleY = .25;
#if WINDOWS
layout.SetLayoutBounds(accidentImage, new Rect(
slugImage.X + slugImage.TranslationX - slugImage.Width / 3,
slugImage.Y - .3 * slugImage.Height,
accidentImage.Width,
accidentImage.Height));
#endif
accidentAnimation.Commit(
this,
"accidentAnimation",
427
16,
vm.RaceTime,
Easing.CubicInOut,
null,
() => true);
vm.PlayAccidentSound(true, true);
vm.DisplayAccidentPopup();
}
}
...
}
The image is scaled and positioned per platform. We set the ZIndex property so that the puddle
image appears under the slug image instead of on top of it. We then animate the Opacity property
so that the slug disappears in the water, then reappears, and so on.
Electroshock Accident
The next accident, Electroshock, is implemented like so:
...
public partial class TrackImage : ContentView
{
...
private async Task HandlePuddle()
...
private async Task HandleElectroshock()
{
if (vm != null
&& vm.AccidentViewModel != null
&& vm.AccidentViewModel.AffectedSlug != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
accidentImage.Rotation = -15;
428
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
#if ANDROID
accidentImage.Scale = .2;
#if WINDOWS
accidentImage.Scale = .7;
accidentAnimation.Commit(
this,
"accidentAnimation",
16,
500,
Easing.Linear,
null,
() => true);
vm.PlayAccidentSound(true, true);
vm.DisplayAccidentPopup();
await Task.Delay((int)vm.AccidentViewModel.Duration);
this.AbortAnimation("accidentAnimation");
if (layout.Contains(accidentImage))
{
layout.Remove(accidentImage);
}
vm.StopAccidentSound();
accidentAnimation.Commit(
this,
"accidentAnimation",
16,
vm.AccidentViewModel.AffectedSlug.RunningTime / 4,
Easing.Linear,
null,
() => false);
}
}
...
}
429
The running animation is canceled and a bolt image appears. We animate its Opacity to imitate
lightning. After that, there’s another running animation, but this time the slug runs faster.
Sometimes nothing special happens to a slug, but they just turn back. Let’s implement this.
...
public partial class TrackImage : ContentView
{
...
private async Task HandleElectroshock()
...
private async Task HandleTurningBack()
{
if (vm != null
&& vm.AccidentViewModel != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
accidentAnimation.Commit(
this,
"accidentAnimation",
16,
5000,
Easing.Linear,
null,
() => false);
430
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
vm.PlayAccidentSound();
vm.DisplayAccidentPopup();
}
}
...
}
The running animation is stopped and a new animation is started that consists of two simple
animations: one where the ScaleX property changes from 1 to -1, which flips the slug horizontally,
and one where the slug runs from his current position to the left.
Devoured Accident
The worst thing, at least theoretically, that may happen to a slug, is being devoured by the terrible
slug monster that occasionally haunts the area where the races take place. The slug monster catches
the slug and eats it. Here’s how this is implemented in our app:
...
public partial class TrackImage : ContentView
{
...
private async Task HandleTurningBack()
...
private async Task HandleDevoured()
{
if (vm != null
&& vm.AccidentViewModel != null
&& slugImage != null)
{
await Task.Delay((int)vm.AccidentViewModel.TimePosition);
this.AbortAnimation(runningAnimationName);
#if ANDROID
accidentImage.ScaleX = .25;
accidentImage.ScaleY = .25;
431
layout.SetLayoutBounds(accidentImage, new Rect(
-100,
slugImage.Y - 2 * slugImage.Height,
accidentImage.Width,
accidentImage.Height));
accidentAnimation = new Animation(
v => accidentImage.TranslationX = v,
0,
slugImage.TranslationX);
#endif
#if WINDOWS
layout.SetLayoutBounds(accidentImage, new Rect(
-accidentImage.Width,
slugImage.Y - .3 * slugImage.Height,
accidentImage.Width,
accidentImage.Height));
accidentAnimation = new Animation(
v => accidentImage.TranslationX = v,
0,
slugImage.X + slugImage.TranslationX + accidentImage.Width);
#endif
accidentAnimation?.Commit(
this,
"accidentAnimation",
16,
1000,
Easing.CubicInOut,
null,
() => false);
vm.PlayAccidentSound();
vm.DisplayAccidentPopup();
}
}
}
The slug monster comes from the left-hand side of the window. Here’s what it looks like:
Poor Trusty. But don’t worry, he’ll be up and running in the next race.
And that’s it. We’ve covered all the accidents that may happen to a slug. But we don’t want them to
happen too frequently, and for sure not in every race, so restore the original code in
GameViewModel that is responsible for deciding whether an accident should happen and picking its
type.
Great, looks like our app is almost finished. There are just a couple final touches I’d like to take care
of before we deploy our app. They are the topic of the next chapter.
432
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
We’re almost done with our app. In this chapter we’ll add some final touches to it so that it is
complete. In particular, we’ll disable app window resizing and implement the InstructionsPage.
We can set the size of the window programmatically. To do that, we have to override the
CreateWindow method in the App class and set the size there. The Width and Height properties are
used for the size when the window first appears after launching the app. We can also set the
minimum and maximum dimensions. In our case, we’ll set all dimensions, inluding the maximum
and minimum ones, to the same values so that it’s no longer possible to resize the window.
namespace Slugrace;
433
#if WINDOWS
protected override Window CreateWindow(IActivationState? activationState)
{
var window = base.CreateWindow(activationState);
return window;
}
#endif
}
If you now run the app, you won’t be able to resize the window.
InstructionsPage
We’re going to use screenshots with annotations on them in the
InstructionsPage. You can grab the images from Github. Put
them in a Screenshots folder inside the Images folder in Resources
folder. Select all the images (A) and in the Properties window set
Build Action to MauiImage (B).
...
public partial class GameViewModel : ObservableObject
{
...
private uint afterAccidentTime = 0;
#if WINDOWS
Screenshots =
[
"settings_windows.png",
434
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
"race_bets_windows.png",
"race_results_windows.png",
"gameover_windows.png"
];
#endif
#if ANDROID
Screenshots =
[
"settings_android.png",
"race_bets_android.png",
"race_results_android.png",
"gameover_android.png"
];
#endif
}
Next, let’s implement the InstructionsPage. We’ll use a CarouselView in it, so we will be able to
use the arrow keys (on Windows) or swipe (on Android) to loop through the screenshots:
...
<ContentPage ...>
...
</Shell.BackButtonBehavior>
<IndicatorView
x:Name="indicatorView"
Grid.Row="2"
IndicatorColor="LightBlue"
SelectedIndicatorColor="DarkBlue"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Label
Grid.Row="3"
HorizontalOptions="Center"
FontSize="15"
Text="{OnPlatform 'Use arrows to navigate between slides.',
Android='Swipe to navigate between pages.'}"/>
435
<Button
Grid.Row="4"
Text="Back"
Command="{Binding NavigateBackCommand}" />
</Grid>
</ContentPage>
The ItemsSource property is bound to the list of screenshots that we just defined in the
GameViewModel. We also use an IndicatorView to see our position in the collection of images.
If we now hit the Instructions button in the RacePage, we’ll navigate to the InstructionsPage.
Here’s what it looks like on Windows:
Here we can see the first screenshot out of four. We can use the arrow keys or the scroll wheel on
our mouse to loop through all of them. Here’s the second screenshot, for example:
436
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
437
Chapter 23 - Deployment
code: https://github.com/prospero-apps/Slugrace/tree/chapter23
Our Slugrace app is ready to be published. Well, to be exact, it wouldn’t hurt to polish it a bit
before, maybe run some tests to see if everything works and if there aren’t any dangerous bugs, but
I’ll leave it as is. For our purposes, let’s say we’ve already made all the necessary corrections and
our app is really ready for production.
As you know, we can deploy .NET MAUI apps to a lot of platforms. We decided to deploy our app
to two platforms, Windows and Android. As we can only build for one platform at a time in Visual
Studio, let’s build the app for Windows first and then for Android.
There are a couple different ways to publish an app for each platform. For Windows we’ll publish
the app with Visual Studio to a folder, from which it can be installed on your machine. For Android
we’ll publish the app for ad-hoc distribution so that it can be downloaded from a website or server.
438
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
We need a certificate to publish the app. Select the second option (A) and hit Create (B):
We’re going to create a temporary self-signed certificate that can be used for testing. We shouldn’t
use it to distribute the package, though. For our purposes this will do.
439
You will see a box with some information about the certificate. Hit OK:
You want to use this certificate, so check the Yes, us the current certificate option and hit Next:
440
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
You can leave the package version as is (A). By checking the Automatically increment box (B), you
make sure the version will be incremented each time the package is published. In the Publishing
profile drop-down select <New…> (C):
In the window that appears, set Configuration to Release | Any CPU and Target Runtime
depending on which version of Windows you want to publish for. Then hit OK:
In the next dialog enter the path to the folder in which you
want to keep the installer (A). Also check one of the radio
buttons to specify how often the app should look for
updates (B). In our case, we’ll check for updates every time
441
the app runs. Whenever a new version of the app is published, it will overwrite the old version.
When done, hit the Create button:
Now the installer is being created. It may take some time. You can see it proceed in the Output
window in Visual Studio. After that, you’ll see the following dialog with a package summary:
Hit the Copy and Close button to copy the package to the installer location you selected before.
The package is ready to be installed. Go to the folder where the installer was copied. In my case
there’s the Slugrace_1.0.0.1_Test folder. It contains the MSIX file.
Let’s install the app. If you double-click the MSIX file, you’ll see the following dialog:
442
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
443
Hit View Certificate.
444
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
Allow the changes on your device if you’re prompted to do so. In the next dialog check the Place
all certificates in the following store radio button (A), click the Browse… button (B), select Trusted
People from the list (C) and hit OK (D):
Hit Next and then Finish. You should see a message confirming that the import was successful.
Click OK on each opened window.
445
If you now double-click the MSIX file, the Install button will be enabled:
Hit it to install the app. When the app is installed, just play and enjoy.
For Android deployment we need the Android Package (APK) format. Right-click the project in the
Solution Explorer and select Properties. Then, under Android, select Options (A) and make sure
apk is selected in the Release drop-down (B):
446
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
As the debug target select your Android emulator (A) and select the Release configuration (B):
Right-click the project in Solution Explorer and select Publish… This will open the Archive
Manager in a new tab. Visual Studio will start to archive the app bundle:
447
This may take a while. After that, make sure the archive is selected (A) and hit the Distribute…
button (B):
448
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
In the next dialog select the + button to create a new signing identity:
449
Select the signing identity you just created (A) and hit the Save As button (B):
Select a location for your file and hit Save. When the
Signing Password dialog shows up, enter the
password you used before and hit the OK button:
450
Desktop and Mobile Programming with .NET MAUI and C# by Kamil Pakula
This will open the folder where the published app is. You can then distribute the app through a
website or server.
You can also install it directly on your device. To do that, just copy the APK file to your device. In
the file explorer select the file and in the menu or popup that shows up select Install (you may need
to allow installing apps from external sources in the settings first). When the app is installed, tap
Done or Open and enjoy the races.
451
Conclusion
This is it. We just created our first fully functional .NET MAUI app. I hope you enjoyed my book.
Now we’re ready to create other beautiful apps for all the available platforms. .NET MAUI is a
relatively new framework and it’s developing rapidly. You can build nearly anything with it - the
sky is the limit, so just go wild and show off your skills. See you in another book or on my Prospero
Coder blog.
452
Index
.xaml extension, 16 Multi-bindings, 243
AbsoluteLayout, 75 MVVM, 247
Android Emulator, 15 namespace declarations, 218
attached properties, 80 non-default namespaces, 219
behaviors, 271 Numeric Keyboard, 207
bindable property, 223 Object Element Syntax, 31
binding context, 223 OnIdiom, 187
Button, 36 OnPlatform, 185
CheckBox, 45 Padding, 209
children, 31 property attributes, 31, 79
clr-namespace, 219 property elements, 79
CollectionView, 182 RadioButton, 47
CommonStates, 153 Relative bindings, 240
composite animations, 349 ResourceDictionary, 125
compound animations, 349 Shell, 19
content properties, 81 Slider, 39
ContentPage, 91 source, 223
ContentView, 92 StackLayout, 55
CreateMauiApp method, 23 StaticResource, 129, 173
data binding, 222 StringFormat, 226
default namespace, 218 Style, 125
DynamicResource, 129 target, 223
Editor, 53 themes, 211
Entry, 50 value converters, 235
explicit types, 127 VerticalStackLayout, 55
FlexLayout, 70 view, 248
Grid, 58 view model, 248
HorizontalStackLayout, 55 views, 30
Hot Reload, 32 visual state groups, 153
Implicit styles, 127 Visual State Manager, 153
InitializeComponent method, 18 visual states, 152
Layouts, 55 x:Array, 182
Margin, 209 x:Static, 179
markup extensions, 173 x:Type, 182
model, 248 XAML, 28
453
ABOUT THE AUTHOR
My name is Kamil Pakula. In 2011 I graduated from the Warsaw University of Technology where I had studied
computer science. Since then I’ve worked on many projects using such programming languages as C++, C#,
Java, JavaScript and Python. Recently I’ve been using mostly .NET technologies to create desktop, mobile and
web applications, as well as games. I also work as a content writer for Code Maze and I run my own
programming blog that you can find at prosperocoder.com. I’m also employed as a full-time .NET developer.
454